From 17418d750ade1348c0bf548e6c81b68200808177 Mon Sep 17 00:00:00 2001 From: mkurczewski Date: Fri, 28 Jun 2024 14:25:26 +0200 Subject: [PATCH 1/7] Added support for cancelling data migration process --- .../renderer/locales/default/en-US.json | 11 +- .../data-migration/perform-data.migration.ts | 23 ++-- .../ui/src/lib/icon/get-icon.helper.tsx | 2 + .../ui/src/lib/icon/svg/exclamation.svg | 9 ++ .../components/cancel-confirm-modal.tsx | 112 ++++++++++++++++++ .../components/progress-modal.tsx | 20 +++- .../components/transfer-cancelled-modal.tsx | 53 +++++++++ .../data-migration/data-migration.tsx | 12 +- .../utils/src/lib/models/icons.types.ts | 1 + 9 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 libs/generic-view/ui/src/lib/icon/svg/exclamation.svg create mode 100644 libs/generic-view/ui/src/lib/predefined/data-migration/components/cancel-confirm-modal.tsx create mode 100644 libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-cancelled-modal.tsx diff --git a/libs/core/__deprecated__/renderer/locales/default/en-US.json b/libs/core/__deprecated__/renderer/locales/default/en-US.json index 6bd92663f2..2142155dac 100644 --- a/libs/core/__deprecated__/renderer/locales/default/en-US.json +++ b/libs/core/__deprecated__/renderer/locales/default/en-US.json @@ -960,5 +960,14 @@ "module.genericViews.dataMigration.progress.genericMessage": "This might take a few minutes", "module.genericViews.dataMigration.success.title": "Data transfer complete", "module.genericViews.dataMigration.success.description": "We transferred your data successfully.\nIt's safe to use your devices again.", - "module.genericViews.dataMigration.success.buttonLabel": "Ok" + "module.genericViews.dataMigration.success.buttonLabel": "Ok", + "module.genericViews.dataMigration.cancelConfirm.title": "Cancel data transfer?", + "module.genericViews.dataMigration.cancelConfirm.description": "We’ll stop the transfer but some data may already be on your Kompakt.", + "module.genericViews.dataMigration.cancelConfirm.cancelButtonLabel": "Cancel transfer", + "module.genericViews.dataMigration.cancelConfirm.backButtonLabel": "Back", + "module.genericViews.dataMigration.cancelConfirm.progress.title": "Cancelling, please wait...", + "module.genericViews.dataMigration.cancelled.title": "Data transfer cancelled", + "module.genericViews.dataMigration.cancelled.noChanges.description": "We didn't transfer any data.", + "module.genericViews.dataMigration.cancelled.partialChanges.description": "We transferred some data before you cancelled but we didn’t transfer these items:", + "module.genericViews.dataMigration.cancelled.closeButtonLabel": "Close" } diff --git a/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts b/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts index ef0acd2610..8ddeeaf40e 100644 --- a/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts +++ b/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts @@ -19,7 +19,6 @@ import { transferDataToDevice, } from "../data-transfer/transfer-data-to-device.action" import logger from "Core/__deprecated__/main/utils/logger" -import { DataMigrationStatus } from "./reducer" export enum DataMigrationPercentageProgress { CollectingData = 1, @@ -39,6 +38,7 @@ export const performDataMigration = createAsyncThunk< const abortListener = async () => { aborted = true + dispatch(setDataMigrationStatus("CANCELLED")) abortTransfer() signal.removeEventListener("abort", abortListener) } @@ -46,12 +46,9 @@ export const performDataMigration = createAsyncThunk< const { dataMigration } = getState() - const handleError = ( - message: string, - reason: Extract = "FAILED" - ) => { + const handleError = (message: string) => { logger.error(message) - dispatch(setDataMigrationStatus(reason)) + dispatch(setDataMigrationStatus("FAILED")) abortTransfer() return rejectWithValue(undefined) } @@ -67,7 +64,7 @@ export const performDataMigration = createAsyncThunk< } if (aborted) { - return handleError("Data migration aborted", "CANCELLED") + return rejectWithValue(undefined) } dispatch( setTransferProgress(DataMigrationPercentageProgress.CollectingData) @@ -83,7 +80,7 @@ export const performDataMigration = createAsyncThunk< } if (aborted) { - return handleError("Data migration aborted", "CANCELLED") + return rejectWithValue(undefined) } const deviceDatabaseIndexed = await indexAllRequest({ serialNumber: deviceInfo.data.serialNumber, @@ -94,7 +91,7 @@ export const performDataMigration = createAsyncThunk< return handleError("Error indexing device database") } if (aborted) { - return handleError("Data migration aborted", "CANCELLED") + return rejectWithValue(undefined) } const databaseResponse = await dispatch(readAllIndexes()) @@ -109,7 +106,7 @@ export const performDataMigration = createAsyncThunk< for (const feature of features) { if (aborted) { - return handleError("Data migration aborted", "CANCELLED") + return rejectWithValue(undefined) } switch (feature) { @@ -131,14 +128,16 @@ export const performDataMigration = createAsyncThunk< ) if (aborted) { - return handleError("Data migration aborted", "CANCELLED") + return rejectWithValue(undefined) } const transferPromise = dispatch(transferDataToDevice(domainsData)) abortTransfer = () => transferPromise.abort() const response = await transferPromise if (response.meta.requestStatus === "rejected") { - return handleError("Error transferring data") + return aborted + ? rejectWithValue(undefined) + : handleError("Error transferring data") } dispatch(setTransferProgress(DataMigrationPercentageProgress.Finished)) diff --git a/libs/generic-view/ui/src/lib/icon/get-icon.helper.tsx b/libs/generic-view/ui/src/lib/icon/get-icon.helper.tsx index 47d3ea318a..4f8ad86950 100644 --- a/libs/generic-view/ui/src/lib/icon/get-icon.helper.tsx +++ b/libs/generic-view/ui/src/lib/icon/get-icon.helper.tsx @@ -46,6 +46,7 @@ import Search from "./svg/search.svg" import Import from "./svg/import.svg" import DataMigration from "./svg/data-migration.svg" import Information from "./svg/information.svg" +import Exclamation from "./svg/exclamation.svg" import { IconType } from "generic-view/utils" @@ -90,6 +91,7 @@ const typeToIcon: Record = { [IconType.Import]: Import, [IconType.DataMigration]: DataMigration, [IconType.Information]: Information, + [IconType.Exclamation]: Exclamation, } export const getIcon = ( diff --git a/libs/generic-view/ui/src/lib/icon/svg/exclamation.svg b/libs/generic-view/ui/src/lib/icon/svg/exclamation.svg new file mode 100644 index 0000000000..1e286262bc --- /dev/null +++ b/libs/generic-view/ui/src/lib/icon/svg/exclamation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/cancel-confirm-modal.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/cancel-confirm-modal.tsx new file mode 100644 index 0000000000..dca6229967 --- /dev/null +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/cancel-confirm-modal.tsx @@ -0,0 +1,112 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React, { FunctionComponent, useEffect, useState } from "react" +import { ButtonAction, IconType } from "generic-view/utils" +import { intl } from "Core/__deprecated__/renderer/utils/intl" +import { defineMessages } from "react-intl" +import { modalTransitionDuration } from "generic-view/theme" +import { ButtonSecondary } from "../../../buttons/button-secondary" +import { ButtonPrimary } from "../../../buttons/button-primary" +import { Modal } from "../../../interactive/modal" +import styled, { keyframes } from "styled-components" + +const messages = defineMessages({ + title: { + id: "module.genericViews.dataMigration.cancelConfirm.title", + }, + description: { + id: "module.genericViews.dataMigration.cancelConfirm.description", + }, + cancelButtonLabel: { + id: "module.genericViews.dataMigration.cancelConfirm.cancelButtonLabel", + }, + backButtonLabel: { + id: "module.genericViews.dataMigration.cancelConfirm.backButtonLabel", + }, + progressTitle: { + id: "module.genericViews.dataMigration.cancelConfirm.progress.title", + }, +}) + +interface Props { + onBackButtonClick?: VoidFunction + onCancelButtonClick?: VoidFunction +} + +export const CancelConfirmModal: FunctionComponent = ({ + onBackButtonClick, + onCancelButtonClick, +}) => { + const title = intl.formatMessage(messages.title) + const progressTitle = intl.formatMessage(messages.progressTitle) + const description = intl.formatMessage(messages.description) + const [cancelRequested, setCancelRequested] = useState(false) + + const backButtonAction: ButtonAction = { + type: "custom", + callback: () => { + onBackButtonClick?.() + }, + } + + const cancelButtonAction: ButtonAction = { + type: "custom", + callback: () => { + setCancelRequested(true) + setTimeout(() => { + onCancelButtonClick?.() + }, modalTransitionDuration) + }, + } + + useEffect(() => { + setCancelRequested(false) + }, []) + + if (cancelRequested) { + return ( + <> + + {progressTitle} + + ) + } + + return ( + <> + + {title} +

{description}

+ + + + + + ) +} + +const spinAnimation = keyframes({ + "0%": { + transform: "rotate(0deg)", + }, + "100%": { + transform: "rotate(360deg)", + }, +}) + +export const ModalIconSpinner = styled(Modal.TitleIcon)` + animation: ${spinAnimation} 1s steps(12) infinite; +` diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/progress-modal.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/progress-modal.tsx index 682592328b..dd1a11f503 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/components/progress-modal.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/progress-modal.tsx @@ -3,18 +3,18 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { FunctionComponent, useMemo } from "react" +import React, { FunctionComponent, useMemo, useState } from "react" import { useSelector } from "react-redux" import { Modal } from "../../../interactive/modal/modal" import { ButtonAction, IconType } from "generic-view/utils" import { ButtonSecondary } from "../../../buttons/button-secondary" import { selectDataMigrationProgress } from "generic-view/store" -import { modalTransitionDuration } from "generic-view/theme" import { defineMessages } from "react-intl" import { intl } from "Core/__deprecated__/renderer/utils/intl" import { ProgressBar } from "../../../interactive/progress-bar/progress-bar" import styled from "styled-components" import { DataMigrationFeature } from "generic-view/models" +import { CancelConfirmModal } from "./cancel-confirm-modal" const messages = defineMessages({ title: { @@ -43,16 +43,17 @@ interface Props { export const ProgressModal: FunctionComponent = ({ onCancel }) => { const dataMigrationProgress = useSelector(selectDataMigrationProgress) + const [cancelRequested, setCancelRequested] = useState(false) const cancelAction: ButtonAction = { type: "custom", callback: () => { - setTimeout(() => { - onCancel?.() - }, modalTransitionDuration) + setCancelRequested(true) }, } + const abortCancelRequest = () => setCancelRequested(false) + const label = useMemo(() => { const feature = dataMigrationProgress.label?.split("-")[0] if (isDataMigrationFeature(feature)) { @@ -63,6 +64,15 @@ export const ProgressModal: FunctionComponent = ({ onCancel }) => { return intl.formatMessage(messages.genericMessage) }, [dataMigrationProgress.label]) + if (cancelRequested) { + return ( + + ) + } + return ( <> diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-cancelled-modal.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-cancelled-modal.tsx new file mode 100644 index 0000000000..649ccad283 --- /dev/null +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-cancelled-modal.tsx @@ -0,0 +1,53 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React, { FunctionComponent } from "react" +import { Modal } from "../../../interactive/modal/modal" +import { ButtonAction, IconType } from "generic-view/utils" +import { ButtonSecondary } from "../../../buttons/button-secondary" +import { defineMessages } from "react-intl" +import { intl } from "Core/__deprecated__/renderer/utils/intl" + +const messages = defineMessages({ + title: { + id: "module.genericViews.dataMigration.cancelled.title", + }, + noChangesDescription: { + id: "module.genericViews.dataMigration.cancelled.noChanges.description", + }, + partialChangesDescription: { + id: "module.genericViews.dataMigration.cancelled.partialChanges.description", + }, + closeButtonLabel: { + id: "module.genericViews.dataMigration.cancelled.closeButtonLabel", + }, +}) + +interface Props { + onClose?: VoidFunction +} + +export const CancelledModal: FunctionComponent = ({ onClose }) => { + const closeAction: ButtonAction = { + type: "custom", + callback: () => onClose?.(), + } + + return ( + <> + + {intl.formatMessage(messages.title)} +

{intl.formatMessage(messages.noChangesDescription)}

+ + + + + ) +} diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx index 5369c3c834..48a0aadedd 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx @@ -48,6 +48,7 @@ import { PurePasscodeModal } from "./components/pure-passcode-modal" import { ProgressModal } from "./components/progress-modal" import { SuccessModal } from "./components/success-modal" import { Modal } from "../../interactive/modal" +import { CancelledModal } from "./components/transfer-cancelled-modal" const messages = defineMessages({ header: { @@ -123,12 +124,10 @@ const DataMigrationUI: FunctionComponent = ({ if (dataMigrationStatus === "IN-PROGRESS") { startTransfer() } - if ( + setModalOpened( dataMigrationStatus !== "IDLE" && - dataMigrationStatus !== "PURE-PASSWORD-REQUIRED" - ) { - setModalOpened(true) - } + dataMigrationStatus !== "PURE-PASSWORD-REQUIRED" + ) }, [dataMigrationStatus, startTransfer]) return ( @@ -162,6 +161,9 @@ const DataMigrationUI: FunctionComponent = ({ {dataMigrationStatus === "IN-PROGRESS" && ( )} + {dataMigrationStatus === "CANCELLED" && ( + + )} {dataMigrationStatus === "COMPLETED" && ( )} diff --git a/libs/generic-view/utils/src/lib/models/icons.types.ts b/libs/generic-view/utils/src/lib/models/icons.types.ts index 97442fd452..8926c17c4c 100644 --- a/libs/generic-view/utils/src/lib/models/icons.types.ts +++ b/libs/generic-view/utils/src/lib/models/icons.types.ts @@ -44,4 +44,5 @@ export enum IconType { Import = "import", DataMigration = "data-migration", Information = "information", + Exclamation = "exclamation", } From a4dd74e287401a8f6fdf6db4d3167f07fa1866cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurczewski?= Date: Wed, 10 Jul 2024 15:12:13 +0200 Subject: [PATCH 2/7] [CP-2854] Support for disconnecting devices during data migration process (#1957) --- .../renderer/locales/default/en-US.json | 11 +- .../controllers/data-sync.controller.ts | 2 +- .../api-device-initialization-modal-flow.tsx | 4 +- .../constants/modal-layers.enum.ts | 3 +- .../harmony-overview.component.tsx | 82 +++++++----- .../pure-overview/pure-overview.component.tsx | 126 ++++++++++-------- .../feature/src/lib/api-device-modals.tsx | 2 + .../src/lib/modals/backup-error-modal.tsx | 16 ++- .../lib/modals/data-migration-error-modal.tsx | 53 ++++++++ .../modals/import-contacts-error-modal.tsx | 16 ++- .../src/lib/modals/restore-error-modal.tsx | 16 ++- libs/generic-view/models/src/lib/modal.ts | 1 + libs/generic-view/store/src/index.ts | 3 + .../store/src/lib/action-names.ts | 8 +- .../data-migration/abort-data.migration.ts | 36 +++++ .../store/src/lib/data-migration/actions.ts | 12 +- .../clear-data-migration.action.ts | 27 ---- ...migration-percentage-progress.interface.ts | 1 + .../data-migration/perform-data.migration.ts | 55 ++++---- .../store/src/lib/data-migration/reducer.ts | 22 +-- .../abort-data-transfer.action.ts | 21 +++ .../store/src/lib/data-transfer/actions.ts | 4 + .../store/src/lib/data-transfer/reducer.ts | 6 + .../transfer-data-to-device.action.ts | 38 ++++-- .../hooks/use-api-serial-port-listeners.ts | 28 +++- .../store/src/lib/modals/actions.ts | 3 + .../store/src/lib/modals/reducer.ts | 6 + .../data-migration-pure-db-indexing.ts | 12 ++ .../lib/selectors/data-transfer-progress.ts | 11 +- .../selectors/device-error-modal-opened.ts | 12 ++ .../store/src/lib/selectors/index.ts | 2 + libs/generic-view/ui/src/index.ts | 3 +- .../modal/helpers/modal-content.tsx | 3 +- .../src/lib/interactive/modal/modal-base.tsx | 11 +- .../ui/src/lib/interactive/modal/modal.tsx | 1 + .../components/device-selector.tsx | 4 +- .../data-migration/components/error-modal.tsx | 11 +- .../components/transfer-cancelled-modal.tsx | 5 +- .../components/transfer-error-modal.tsx | 24 ++-- .../components/transfer-fail-message.tsx | 89 +++++++++++++ .../data-migration/data-migration.tsx | 93 ++++++++----- .../data-migration/transfer-setup.tsx | 41 +++++- .../ui/src/lib/shared/spinner-loader.tsx | 10 +- libs/generic-view/utils/src/index.ts | 2 +- .../{custom-modal-error.ts => modal.types.ts} | 2 + .../lib/use-data-migration-device-selector.ts | 4 +- 46 files changed, 679 insertions(+), 263 deletions(-) create mode 100644 libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx create mode 100644 libs/generic-view/store/src/lib/data-migration/abort-data.migration.ts delete mode 100644 libs/generic-view/store/src/lib/data-migration/clear-data-migration.action.ts create mode 100644 libs/generic-view/store/src/lib/data-transfer/abort-data-transfer.action.ts create mode 100644 libs/generic-view/store/src/lib/selectors/data-migration-pure-db-indexing.ts create mode 100644 libs/generic-view/store/src/lib/selectors/device-error-modal-opened.ts create mode 100644 libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx rename libs/generic-view/utils/src/lib/models/{custom-modal-error.ts => modal.types.ts} (80%) diff --git a/libs/core/__deprecated__/renderer/locales/default/en-US.json b/libs/core/__deprecated__/renderer/locales/default/en-US.json index 2142155dac..f051ecb042 100644 --- a/libs/core/__deprecated__/renderer/locales/default/en-US.json +++ b/libs/core/__deprecated__/renderer/locales/default/en-US.json @@ -945,6 +945,7 @@ "module.genericViews.dataMigration.transferSetup.unlockInfo": "You may need to unlock your Pure before data transfer can start.", "module.genericViews.dataMigration.transferSetup.deviceSelector.sourceLabel": "Source Device", "module.genericViews.dataMigration.transferSetup.deviceSelector.targetLabel": "Destination Device", + "module.genericViews.dataMigration.transferSetup.pureNotReady": "Preparing Pure for data transfer, please wait...", "module.genericViews.dataMigration.pureError.title": "Can’t start transfer", "module.genericViews.dataMigration.pureError.closeButtonLabel": "Close", "module.genericViews.dataMigration.pureError.onboardingRequired": "Please accept the MuditaOS license agreement on your device and complete the ondoarding process to continue.", @@ -952,8 +953,7 @@ "module.genericViews.dataMigration.pureError.criticalBattery": "Please charge your Pure to start the transfer.", "module.genericViews.dataMigration.pureError.connection.title": "Data transfer failed", "module.genericViews.dataMigration.transferError.title": "Data transfer failed", - "module.genericViews.dataMigration.transferError.connectionFailed": "Data cannot be transfered.\nTry reconnecting pure to your computer.", - "module.genericViews.dataMigration.transferError.closeButtonLabel": "Close", + "module.genericViews.dataMigration.transferError.partialChangesDescription": "We transferred some data before the process failed, but we didn't transfer these items:", "module.genericViews.dataMigration.progress.title": "Transferring data", "module.genericViews.dataMigration.progress.description": "Please don't unplug your devices from your computer.", "module.genericViews.dataMigration.progress.cancelButtonLabel": "Cancel", @@ -967,7 +967,8 @@ "module.genericViews.dataMigration.cancelConfirm.backButtonLabel": "Back", "module.genericViews.dataMigration.cancelConfirm.progress.title": "Cancelling, please wait...", "module.genericViews.dataMigration.cancelled.title": "Data transfer cancelled", - "module.genericViews.dataMigration.cancelled.noChanges.description": "We didn't transfer any data.", - "module.genericViews.dataMigration.cancelled.partialChanges.description": "We transferred some data before you cancelled but we didn’t transfer these items:", - "module.genericViews.dataMigration.cancelled.closeButtonLabel": "Close" + "module.genericViews.dataMigration.cancelled.partialChangesDescription": "We transferred some data before you cancelled but we didn't transfer these items:", + "module.genericViews.dataMigration.failure.genericDescription": "The transfer process was interrupted.", + "module.genericViews.dataMigration.failure.noChangesDescription": "We didn’t transfer any data.", + "module.genericViews.dataMigration.failure.closeButtonLabel": "Close" } diff --git a/libs/core/data-sync/controllers/data-sync.controller.ts b/libs/core/data-sync/controllers/data-sync.controller.ts index 249da67633..025393bb2d 100644 --- a/libs/core/data-sync/controllers/data-sync.controller.ts +++ b/libs/core/data-sync/controllers/data-sync.controller.ts @@ -7,7 +7,7 @@ import { SerialisedIndexData } from "elasticlunr" import { IpcEvent } from "Core/core/decorators" import { IndexStorage } from "Core/index-storage/types" import { DataSyncService } from "Core/data-sync/services/data-sync.service" -import { IpcDataSyncEvent, DataIndex } from "Core/data-sync/constants" +import { DataIndex, IpcDataSyncEvent } from "Core/data-sync/constants" import { InitializeOptions } from "Core/data-sync/types" export class DataSyncController { diff --git a/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx b/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx index f74739d4a1..53923d056f 100644 --- a/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx +++ b/libs/core/device-initialization/components/devices-initialization-modal-flows/api-device-initialization-modal-flow.tsx @@ -19,7 +19,7 @@ import { selectApiError, selectDataMigrationSourceDevice, selectDataMigrationTargetDevice, - setSourceDevice, + setDataMigrationSourceDevice, } from "generic-view/store" import { Modal } from "generic-view/ui" import { GenericThemeProvider } from "generic-view/theme" @@ -99,7 +99,7 @@ export const APIDeviceInitializationModalFlow: FunctionComponent = () => { dataMigrationSourceDevice.serialNumber, URL_MAIN.dataMigration ) - dispatch(setSourceDevice(undefined)) + dispatch(setDataMigrationSourceDevice(undefined)) } else if (devices.length > 1) { await deactivateDeviceAndRedirect() } else { diff --git a/libs/core/modals-manager/constants/modal-layers.enum.ts b/libs/core/modals-manager/constants/modal-layers.enum.ts index 0b69139f0e..dcad329ddb 100644 --- a/libs/core/modals-manager/constants/modal-layers.enum.ts +++ b/libs/core/modals-manager/constants/modal-layers.enum.ts @@ -14,5 +14,6 @@ export enum ModalLayers { UpdateApp, PrivacyPolicy, Drawer, - ConnectingLoader + ConnectingLoader, + DisconnectedDeviceError } diff --git a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx index 3095de6841..d921601cc9 100644 --- a/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx +++ b/libs/core/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx @@ -17,6 +17,8 @@ import { ipcRenderer } from "electron-better-ipc" import React from "react" import { CheckForUpdateState } from "Core/update/constants/check-for-update-state.constant" import { useWatchDeviceDataEffect } from "Core/overview/components/overview-screens/helpers/use-watch-device-data-effect" +import { useSelector } from "react-redux" +import { selectDeviceErrorModalOpened } from "generic-view/store" export const HarmonyOverview: FunctionComponent = ({ batteryLevel = 0, @@ -44,6 +46,9 @@ export const HarmonyOverview: FunctionComponent = ({ closeForceUpdateFlow, caseColour, }) => { + const genericDeviceErrorModalOpened = useSelector( + selectDeviceErrorModalOpened + ) useWatchDeviceDataEffect() const goToHelp = (): void => { void ipcRenderer.callMain(HelpActions.OpenWindow) @@ -87,44 +92,49 @@ export const HarmonyOverview: FunctionComponent = ({ return ( <> - {!forceUpdateNeeded && ( - - )} + {!genericDeviceErrorModalOpened && ( + <> + {!forceUpdateNeeded && ( + + )} - {flags.get(Feature.ForceUpdate) && ( - + {flags.get(Feature.ForceUpdate) && ( + + )} + )} - = ({ batteryLevel = 0, @@ -80,6 +81,9 @@ export const PureOverview: FunctionComponent = ({ useWatchDeviceDataEffect() const activeDeviceAttached = useSelector(isActiveDeviceAttachedSelector) const deactivateDeviceAndRedirect = useDeactivateDeviceAndRedirect() + const genericDeviceErrorModalOpened = useSelector( + selectDeviceErrorModalOpened + ) const [openModal, setOpenModal] = useState({ backupStartModal: false, @@ -229,65 +233,71 @@ export const PureOverview: FunctionComponent = ({ return ( <> - {!forceUpdateNeeded && ( - - )} + {!genericDeviceErrorModalOpened && ( + <> + {!forceUpdateNeeded && ( + + )} - {flags.get(Feature.ForceUpdate) && ( - - )} - {backupDeviceFlowState && ( - - )} - {restoreDeviceFlowState && ( - - )} - {shouldErrorSyncModalVisible() && ( - + {flags.get(Feature.ForceUpdate) && ( + + )} + {backupDeviceFlowState && ( + + )} + {restoreDeviceFlowState && ( + + )} + {shouldErrorSyncModalVisible() && ( + + )} + )} { return ( @@ -15,6 +16,7 @@ export const ApiDeviceModals: FunctionComponent = () => { + ) } diff --git a/libs/generic-view/feature/src/lib/modals/backup-error-modal.tsx b/libs/generic-view/feature/src/lib/modals/backup-error-modal.tsx index 5b70209ed1..923ed7c235 100644 --- a/libs/generic-view/feature/src/lib/modals/backup-error-modal.tsx +++ b/libs/generic-view/feature/src/lib/modals/backup-error-modal.tsx @@ -3,14 +3,16 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { FunctionComponent } from "react" +import React, { FunctionComponent, useEffect } from "react" import { BackupError, Modal } from "generic-view/ui" import { useDispatch, useSelector } from "react-redux" import { cleanBackupProcess, selectActiveApiDeviceId, selectBackupProcessStatus, + setDeviceErrorModalOpened, } from "generic-view/store" +import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum" const BackupErrorModal: FunctionComponent = () => { const dispatch = useDispatch() @@ -20,10 +22,20 @@ const BackupErrorModal: FunctionComponent = () => { const onClose = () => { dispatch(cleanBackupProcess()) + dispatch(setDeviceErrorModalOpened(false)) } + + useEffect(() => { + dispatch(setDeviceErrorModalOpened(opened)) + }, [dispatch, opened]) + return ( diff --git a/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx b/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx new file mode 100644 index 0000000000..f42b5347f3 --- /dev/null +++ b/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx @@ -0,0 +1,53 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React, { FunctionComponent, useEffect } from "react" +import { Modal, TransferErrorModal } from "generic-view/ui" +import { useDispatch, useSelector } from "react-redux" +import { + DataMigrationPercentageProgress, + selectActiveApiDeviceId, + selectDataMigrationStatus, + setDataMigrationFeatures, + setDataMigrationProgress, + setDataMigrationPureDbIndexing, + setDataMigrationSourceDevice, + setDataMigrationStatus, + setDeviceErrorModalOpened, +} from "generic-view/store" +import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum" + +export const DataMigrationErrorModal: FunctionComponent = () => { + const dispatch = useDispatch() + const activeDevice = useSelector(selectActiveApiDeviceId) + const dataMigrationStatus = useSelector(selectDataMigrationStatus) + const opened = dataMigrationStatus === "FAILED" && !activeDevice + + const onClose = () => { + dispatch(setDataMigrationStatus("IDLE")) + dispatch(setDataMigrationProgress(DataMigrationPercentageProgress.None)) + dispatch(setDataMigrationFeatures([])) + dispatch(setDataMigrationSourceDevice(undefined)) + dispatch(setDataMigrationPureDbIndexing(false)) + dispatch(setDeviceErrorModalOpened(false)) + } + + useEffect(() => { + dispatch(setDeviceErrorModalOpened(opened)) + }, [dispatch, opened]) + + return ( + + + + ) +} diff --git a/libs/generic-view/feature/src/lib/modals/import-contacts-error-modal.tsx b/libs/generic-view/feature/src/lib/modals/import-contacts-error-modal.tsx index 2dd7167c03..1ecefa80e0 100644 --- a/libs/generic-view/feature/src/lib/modals/import-contacts-error-modal.tsx +++ b/libs/generic-view/feature/src/lib/modals/import-contacts-error-modal.tsx @@ -3,14 +3,16 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { FunctionComponent } from "react" +import React, { FunctionComponent, useEffect } from "react" import { ImportContactsError, Modal } from "generic-view/ui" import { useDispatch, useSelector } from "react-redux" import { cleanImportProcess, importStatusSelector, selectActiveApiDeviceId, + setDeviceErrorModalOpened, } from "generic-view/store" +import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum" const ImportContactsErrorModal: FunctionComponent = () => { const dispatch = useDispatch() @@ -20,10 +22,20 @@ const ImportContactsErrorModal: FunctionComponent = () => { const onClose = () => { dispatch(cleanImportProcess()) + dispatch(setDeviceErrorModalOpened(false)) } + + useEffect(() => { + dispatch(setDeviceErrorModalOpened(opened)) + }, [dispatch, opened]) + return ( { const dispatch = useDispatch() @@ -20,10 +22,20 @@ const RestoreErrorModal: FunctionComponent = () => { const onClose = () => { dispatch(cleanRestoreProcess()) + dispatch(setDeviceErrorModalOpened(false)) } + + useEffect(() => { + dispatch(setDeviceErrorModalOpened(opened)) + }, [dispatch, opened]) + return ( diff --git a/libs/generic-view/models/src/lib/modal.ts b/libs/generic-view/models/src/lib/modal.ts index fe41737e5f..76e9498f8f 100644 --- a/libs/generic-view/models/src/lib/modal.ts +++ b/libs/generic-view/models/src/lib/modal.ts @@ -26,6 +26,7 @@ const modalConfigValidator = z.object({ size: modalSizeValidator.optional(), defaultOpened: z.boolean().optional(), overlayHidden: z.boolean().optional(), + modalLayer: z.number().optional(), }) const configValidator = modalBaseConfigValidator.and(modalConfigValidator) diff --git a/libs/generic-view/store/src/index.ts b/libs/generic-view/store/src/index.ts index 0f6803dc7a..34a44c5ff6 100644 --- a/libs/generic-view/store/src/index.ts +++ b/libs/generic-view/store/src/index.ts @@ -45,5 +45,8 @@ export * from "./lib/data-migration/reducer" export * from "./lib/data-migration/actions" export * from "./lib/data-migration/start-data.migration" export * from "./lib/data-migration/perform-data.migration" +export * from "./lib/data-migration/abort-data.migration" export * from "./lib/data-transfer/reducer" export * from "./lib/data-transfer/actions" +export * from "./lib/data-transfer/abort-data-transfer.action" +export * from "./lib/data-migration/data-migration-percentage-progress.interface" diff --git a/libs/generic-view/store/src/lib/action-names.ts b/libs/generic-view/store/src/lib/action-names.ts index 450e2cc0c3..c3aec3c48b 100644 --- a/libs/generic-view/store/src/lib/action-names.ts +++ b/libs/generic-view/store/src/lib/action-names.ts @@ -20,6 +20,7 @@ export enum ActionName { CloseAllModals = "generic-modals/close-all-modals", ReplaceModal = "generic-modals/replace-modal", CloseDomainModals = "generic-modals/close-domain-modals", + SetDeviceErrorModalOpened = "generic-modals/set-device-error-modal-opened", SetBackupProcess = "generic-backups/set-backup-process", CleanBackupProcess = "generic-backups/clean-backup-process", BackupProcessStatus = "generic-backups/backup-process-status", @@ -49,6 +50,8 @@ export enum ActionName { StartDataTransferToDevice = "generic-imports/start-data-transfer-to-device", SetDataTransferProcessStatus = "generic-imports/set-data-transfer-process-status", SetDataTransferProcessFileStatus = "generic-imports/set-data-transfer-process-file-status", + SetDataTransferAbort = "generic-imports/set-data-transfer-abort", + AbortDataTransfer = "generic-imports/abort-data-transfer", CleanImportProcess = "generic-imports/clean-import-process", StartContactsFileImport = "generic-imports/start-contacts-file-import", GetContactsFromCSV = "generic-imports/get-contacts-from-csv", @@ -60,11 +63,12 @@ export enum ActionName { SetOutlookAuthDataProcess = "generic-imports/set-outlook-auth-data-process", SetDataMigrationSourceDevice = "data-migration/set-source-device", SetDataMigrationFeatures = "data-migration/set-features", + setDataMigrationPureDbIndexing = "data-migration/set-pure-db-indexing", ClearDataMigrationDevice = "data-migration/clear-device", - ClearDataMigrationProgress = "data-migration/clear-progress", SetDataMigrationStatus = "data-migration/set-status", StartDataMigration = "data-migration/start", PerformDataMigration = "data-migration/perform", + AbortDataMigration = "data-migration/abort", SetDataMigrationTransferProgress = "data-migration/set-transfer-progress", - SetDataMigrationTransferProgressLabel = "data-migration/set-transfer-progress-label", + SetDataMigrationAbort = "data-migration/set-abort", } diff --git a/libs/generic-view/store/src/lib/data-migration/abort-data.migration.ts b/libs/generic-view/store/src/lib/data-migration/abort-data.migration.ts new file mode 100644 index 0000000000..9bdaec2f46 --- /dev/null +++ b/libs/generic-view/store/src/lib/data-migration/abort-data.migration.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createAsyncThunk } from "@reduxjs/toolkit" +import { ActionName } from "../action-names" +import { ReduxRootState } from "Core/__deprecated__/renderer/store" +import { DataMigrationStatus } from "./reducer" +import { + setDataMigrationAbort, + setDataMigrationProgress, + setDataMigrationStatus, +} from "./actions" +import { DataMigrationPercentageProgress } from "./data-migration-percentage-progress.interface" + +export const abortDataMigration = createAsyncThunk< + void, + { reason?: Extract } | undefined, + { state: ReduxRootState } +>( + ActionName.AbortDataMigration, + async ({ reason } = {}, { dispatch, getState }) => { + const { dataMigration } = getState() + + dataMigration.abortController?.abort?.() + dispatch(setDataMigrationAbort(undefined)) + dispatch(setDataMigrationProgress(DataMigrationPercentageProgress.None)) + + if (reason) { + dispatch(setDataMigrationStatus(reason)) + } + + return + } +) diff --git a/libs/generic-view/store/src/lib/data-migration/actions.ts b/libs/generic-view/store/src/lib/data-migration/actions.ts index b4e935dbdb..1a8db634d9 100644 --- a/libs/generic-view/store/src/lib/data-migration/actions.ts +++ b/libs/generic-view/store/src/lib/data-migration/actions.ts @@ -9,7 +9,7 @@ import { DataMigrationFeature } from "generic-view/models" import { DataMigrationStatus } from "./reducer" import { DataMigrationPercentageProgress } from "./data-migration-percentage-progress.interface" -export const setSourceDevice = createAction( +export const setDataMigrationSourceDevice = createAction( ActionName.SetDataMigrationSourceDevice ) @@ -21,11 +21,15 @@ export const setDataMigrationStatus = createAction( ActionName.SetDataMigrationStatus ) -export const setTransferProgress = +export const setDataMigrationProgress = createAction( ActionName.SetDataMigrationTransferProgress ) -export const clearDataMigrationProgress = createAction( - ActionName.ClearDataMigrationProgress +export const setDataMigrationAbort = createAction( + ActionName.SetDataMigrationAbort +) + +export const setDataMigrationPureDbIndexing = createAction( + ActionName.setDataMigrationPureDbIndexing ) diff --git a/libs/generic-view/store/src/lib/data-migration/clear-data-migration.action.ts b/libs/generic-view/store/src/lib/data-migration/clear-data-migration.action.ts deleted file mode 100644 index bf446296a3..0000000000 --- a/libs/generic-view/store/src/lib/data-migration/clear-data-migration.action.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { createAsyncThunk } from "@reduxjs/toolkit" -import { ActionName } from "../action-names" -import { DeviceId } from "Core/device/constants/device-id" -import { ReduxRootState } from "Core/__deprecated__/renderer/store" -import { - clearDataMigrationProgress, - setDataMigrationFeatures, - setSourceDevice, -} from "./actions" - -export const clearDataMigrationDevice = createAsyncThunk< - void, - DeviceId, - { state: ReduxRootState } ->(ActionName.ClearDataMigrationDevice, (deviceId, { dispatch, getState }) => { - const { sourceDevice } = getState().dataMigration - if (sourceDevice && sourceDevice === deviceId) { - dispatch(setSourceDevice(undefined)) - dispatch(setDataMigrationFeatures([])) - dispatch(clearDataMigrationProgress()) - } -}) diff --git a/libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts b/libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts index d62920e62e..2778bb8fcf 100644 --- a/libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts +++ b/libs/generic-view/store/src/lib/data-migration/data-migration-percentage-progress.interface.ts @@ -4,6 +4,7 @@ */ export enum DataMigrationPercentageProgress { + None = 0, CollectingData = 1, TransferringData = 10, Finished = 100, diff --git a/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts b/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts index 64f55bcb7e..9c7e29bc24 100644 --- a/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts +++ b/libs/generic-view/store/src/lib/data-migration/perform-data.migration.ts @@ -7,7 +7,12 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { ActionName } from "../action-names" import { ReduxRootState } from "Core/__deprecated__/renderer/store" import { DataMigrationFeature } from "generic-view/models" -import { setDataMigrationStatus, setTransferProgress } from "./actions" +import { + setDataMigrationAbort, + setDataMigrationPureDbIndexing, + setDataMigrationProgress, + setDataMigrationStatus, +} from "./actions" import { readAllIndexes } from "Core/data-sync/actions" import { indexAllRequest } from "Core/data-sync/requests" import { getDeviceInfoRequest } from "Core/device-info/requests" @@ -20,6 +25,7 @@ import { } from "../data-transfer/transfer-data-to-device.action" import logger from "Core/__deprecated__/main/utils/logger" import { DataMigrationPercentageProgress } from "./data-migration-percentage-progress.interface" +import { abortDataTransfer } from "../data-transfer/abort-data-transfer.action" export const performDataMigration = createAsyncThunk< void, @@ -27,24 +33,23 @@ export const performDataMigration = createAsyncThunk< { state: ReduxRootState } >( ActionName.PerformDataMigration, - async (_, { dispatch, getState, signal, rejectWithValue }) => { - let aborted = false - let abortTransfer = () => {} + async (_, { dispatch, getState, signal, abort, rejectWithValue }) => { + const { dataMigration } = getState() + + const dataMigrationAbortController = new AbortController() + dataMigrationAbortController.abort = abort + dispatch(setDataMigrationAbort(dataMigrationAbortController)) const abortListener = async () => { - aborted = true - dispatch(setDataMigrationStatus("CANCELLED")) - abortTransfer() + dispatch(abortDataTransfer()) signal.removeEventListener("abort", abortListener) } signal.addEventListener("abort", abortListener) - const { dataMigration } = getState() - const handleError = (message: string) => { logger.error(message) dispatch(setDataMigrationStatus("FAILED")) - abortTransfer() + dispatch(abortDataTransfer()) return rejectWithValue(undefined) } @@ -58,11 +63,11 @@ export const performDataMigration = createAsyncThunk< return handleError("No features selected") } - if (aborted) { + if (signal.aborted) { return rejectWithValue(undefined) } dispatch( - setTransferProgress(DataMigrationPercentageProgress.CollectingData) + setDataMigrationProgress(DataMigrationPercentageProgress.CollectingData) ) const deviceInfo = await getDeviceInfoRequest(sourceDeviceId) @@ -74,20 +79,22 @@ export const performDataMigration = createAsyncThunk< return handleError("Error getting device info") } - if (aborted) { + if (signal.aborted) { return rejectWithValue(undefined) } + dispatch(setDataMigrationPureDbIndexing(true)) const deviceDatabaseIndexed = await indexAllRequest({ serialNumber: deviceInfo.data.serialNumber, token: deviceInfo.data.token, }) + dispatch(setDataMigrationPureDbIndexing(false)) + if (signal.aborted) { + return rejectWithValue(undefined) + } if (!deviceDatabaseIndexed) { return handleError("Error indexing device database") } - if (aborted) { - return rejectWithValue(undefined) - } const databaseResponse = await dispatch(readAllIndexes()) if ( @@ -100,7 +107,7 @@ export const performDataMigration = createAsyncThunk< const domainsData: DomainData[] = [] for (const feature of features) { - if (aborted) { + if (signal.aborted) { return rejectWithValue(undefined) } @@ -119,22 +126,20 @@ export const performDataMigration = createAsyncThunk< } dispatch( - setTransferProgress(DataMigrationPercentageProgress.TransferringData) + setDataMigrationProgress(DataMigrationPercentageProgress.TransferringData) ) - if (aborted) { + if (signal.aborted) { return rejectWithValue(undefined) } - const transferPromise = dispatch(transferDataToDevice(domainsData)) - abortTransfer = () => transferPromise.abort() - const response = await transferPromise + const transferResponse = await dispatch(transferDataToDevice(domainsData)) - if (response.meta.requestStatus === "rejected") { - return aborted + if (transferResponse.meta.requestStatus === "rejected") { + return signal.aborted ? rejectWithValue(undefined) : handleError("Error transferring data") } - dispatch(setTransferProgress(DataMigrationPercentageProgress.Finished)) + dispatch(setDataMigrationProgress(DataMigrationPercentageProgress.Finished)) setTimeout(() => { dispatch(setDataMigrationStatus("COMPLETED")) diff --git a/libs/generic-view/store/src/lib/data-migration/reducer.ts b/libs/generic-view/store/src/lib/data-migration/reducer.ts index 168ae08d00..ddfecb585c 100644 --- a/libs/generic-view/store/src/lib/data-migration/reducer.ts +++ b/libs/generic-view/store/src/lib/data-migration/reducer.ts @@ -5,11 +5,12 @@ import { createReducer } from "@reduxjs/toolkit" import { - clearDataMigrationProgress, + setDataMigrationAbort, setDataMigrationFeatures, + setDataMigrationProgress, + setDataMigrationPureDbIndexing, setDataMigrationStatus, - setSourceDevice, - setTransferProgress, + setDataMigrationSourceDevice, } from "./actions" import { DeviceId } from "Core/device/constants/device-id" import { DataMigrationFeature } from "generic-view/models" @@ -31,16 +32,19 @@ interface DataMigrationState { selectedFeatures: DataMigrationFeature[] status: DataMigrationStatus transferProgress: number + abortController?: AbortController + pureDbIndexing: boolean } const initialState: DataMigrationState = { selectedFeatures: [], status: "IDLE", transferProgress: 0, + pureDbIndexing: false, } export const dataMigrationReducer = createReducer(initialState, (builder) => { - builder.addCase(setSourceDevice, (state, action) => { + builder.addCase(setDataMigrationSourceDevice, (state, action) => { state.sourceDevice = action.payload }) builder.addCase(setDataMigrationFeatures, (state, action) => { @@ -49,11 +53,13 @@ export const dataMigrationReducer = createReducer(initialState, (builder) => { builder.addCase(setDataMigrationStatus, (state, action) => { state.status = action.payload }) - builder.addCase(setTransferProgress, (state, action) => { + builder.addCase(setDataMigrationProgress, (state, action) => { state.transferProgress = action.payload ?? 0 }) - builder.addCase(clearDataMigrationProgress, (state) => { - state.status = "IDLE" - state.transferProgress = 0 + builder.addCase(setDataMigrationAbort, (state, action) => { + state.abortController = action.payload + }) + builder.addCase(setDataMigrationPureDbIndexing, (state, action) => { + state.pureDbIndexing = action.payload }) }) diff --git a/libs/generic-view/store/src/lib/data-transfer/abort-data-transfer.action.ts b/libs/generic-view/store/src/lib/data-transfer/abort-data-transfer.action.ts new file mode 100644 index 0000000000..22c4ac2daa --- /dev/null +++ b/libs/generic-view/store/src/lib/data-transfer/abort-data-transfer.action.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createAsyncThunk } from "@reduxjs/toolkit" +import { ReduxRootState } from "Core/__deprecated__/renderer/store" +import { ActionName } from "../action-names" +import { setDataTransferAbort } from "./actions" + +export const abortDataTransfer = createAsyncThunk< + void, + undefined, + { state: ReduxRootState } +>(ActionName.AbortDataTransfer, (_, { getState, dispatch }) => { + const { genericDataTransfer } = getState() + + genericDataTransfer.abortController?.abort?.() + dispatch(setDataTransferAbort(undefined)) + return +}) diff --git a/libs/generic-view/store/src/lib/data-transfer/actions.ts b/libs/generic-view/store/src/lib/data-transfer/actions.ts index 4c64f863ee..f5143876e1 100644 --- a/libs/generic-view/store/src/lib/data-transfer/actions.ts +++ b/libs/generic-view/store/src/lib/data-transfer/actions.ts @@ -16,3 +16,7 @@ export const setDataTransferStatus = createAction( ) export const clearDataTransfer = createAction(ActionName.ClearDataTransfer) + +export const setDataTransferAbort = createAction( + ActionName.SetDataTransferAbort +) diff --git a/libs/generic-view/store/src/lib/data-transfer/reducer.ts b/libs/generic-view/store/src/lib/data-transfer/reducer.ts index ebab24e376..3346b3542f 100644 --- a/libs/generic-view/store/src/lib/data-transfer/reducer.ts +++ b/libs/generic-view/store/src/lib/data-transfer/reducer.ts @@ -7,6 +7,7 @@ import { createReducer } from "@reduxjs/toolkit" import { clearDataTransfer, setDataTransfer, + setDataTransferAbort, setDataTransferStatus, } from "./actions" import { transferDataToDevice } from "./transfer-data-to-device.action" @@ -18,6 +19,7 @@ export type DomainTransferStatus = | "READY" | "IN-PROGRESS" | "PROCESSING" + | "FINISHED" export type DataTransferStatus = | "IDLE" @@ -30,6 +32,7 @@ export type DataTransfer = Record export interface DataTransferState { transfer: DataTransfer status: DataTransferStatus + abortController?: AbortController } const initialState: DataTransferState = { @@ -59,5 +62,8 @@ export const genericDataTransferReducer = createReducer( builder.addCase(setDataTransferStatus, (state, action) => { state.status = action.payload }) + builder.addCase(setDataTransferAbort, (state, action) => { + state.abortController = action.payload + }) } ) diff --git a/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts b/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts index 261a5b96b6..3c2fb82aa8 100644 --- a/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts +++ b/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts @@ -19,6 +19,7 @@ import { sendFile } from "../file-transfer/send-file.action" import { clearDataTransfer, setDataTransfer, + setDataTransferAbort, setDataTransferStatus, } from "./actions" import { isEmpty } from "lodash" @@ -36,10 +37,16 @@ export const transferDataToDevice = createAsyncThunk< { state: ReduxRootState } >( ActionName.TransferDataToDevice, - async (domainsData, { getState, dispatch, rejectWithValue, signal }) => { - let aborted = false + async ( + domainsData, + { getState, dispatch, abort, rejectWithValue, signal } + ) => { let abortFileRequest: VoidFunction + const transferAbortController = new AbortController() + transferAbortController.abort = abort + dispatch(setDataTransferAbort(transferAbortController)) + const handleError = () => { void clearTransfers?.() dispatch(clearDataTransfer()) @@ -48,7 +55,6 @@ export const transferDataToDevice = createAsyncThunk< const abortListener = async () => { signal.removeEventListener("abort", abortListener) - aborted = true abortFileRequest?.() await clearTransfers?.() if (dataTransferId && deviceId) { @@ -101,7 +107,7 @@ export const transferDataToDevice = createAsyncThunk< ) ) - if (aborted) { + if (signal.aborted) { return handleError() } @@ -129,7 +135,7 @@ export const transferDataToDevice = createAsyncThunk< } for (const [index, domain] of domainsPaths.entries()) { - if (aborted) { + if (signal.aborted) { return handleError() } @@ -161,7 +167,7 @@ export const transferDataToDevice = createAsyncThunk< } for (const domain of domainsPaths) { - if (aborted) { + if (signal.aborted) { return handleError() } @@ -176,7 +182,10 @@ export const transferDataToDevice = createAsyncThunk< ) abortFileRequest = sendFilePromise.abort const sendFileResponse = await sendFilePromise - if (sendFileResponse.meta.requestStatus === "rejected" || aborted) { + if ( + sendFileResponse.meta.requestStatus === "rejected" || + signal.aborted + ) { return handleError() } @@ -193,7 +202,7 @@ export const transferDataToDevice = createAsyncThunk< await clearTransfers() - if (aborted) { + if (signal.aborted) { return handleError() } dispatch(setDataTransferStatus("FINALIZING")) @@ -208,7 +217,7 @@ export const transferDataToDevice = createAsyncThunk< let progress = startDataTransferResponse.data.progress while (progress < 100) { - if (aborted) { + if (signal.aborted) { return handleError() } const checkPreRestoreResponse = await checkDataTransferRequest( @@ -221,7 +230,16 @@ export const transferDataToDevice = createAsyncThunk< progress = checkPreRestoreResponse.data.progress } - if (aborted) { + setDataTransfer( + domainsData.reduce((acc: DataTransfer, domainData) => { + acc[domainData.domain] = { + status: "FINISHED", + } + return acc + }, {}) + ) + + if (signal.aborted) { return handleError() } diff --git a/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts b/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts index f11a9e29c1..a3bf164cad 100644 --- a/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts +++ b/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts @@ -15,7 +15,12 @@ import { getAPIConfig } from "../get-api-config" import { setBackupProcessStatus } from "../backup/actions" import { closeAllModals } from "../modals/actions" import { selectBackupProcessStatus } from "../selectors/backup-process-status" -import { clearDataMigrationDevice } from "../data-migration/clear-data-migration.action" +import { abortDataMigration } from "../data-migration/abort-data.migration" +import { selectDataMigrationStatus } from "../selectors/data-migration-status" +import { + selectDataMigrationSourceDevice, + selectDataMigrationTargetDevice, +} from "../selectors/data-migration-devices" export const useAPISerialPortListeners = () => { const dispatch = useDispatch() @@ -62,11 +67,20 @@ export const useAPISerialPortListeners = () => { const useHandleDevicesDetached = () => { const dispatch = useDispatch() const backupProcess = useSelector(selectBackupProcessStatus) + const migrationStatus = useSelector(selectDataMigrationStatus) + const sourceDevice = useSelector(selectDataMigrationSourceDevice) + const targetDevice = useSelector(selectDataMigrationTargetDevice) return useCallback( async (deviceDetachedEvents: DeviceBaseProperties[]) => { for (const event of deviceDetachedEvents) { - dispatch(clearDataMigrationDevice(event.id)) + if ( + migrationStatus !== "IDLE" && + (sourceDevice?.serialNumber === event.serialNumber || + targetDevice?.serialNumber === event.serialNumber) + ) { + dispatch(abortDataMigration({ reason: "FAILED" })) + } } const apiEvents = deviceDetachedEvents.filter( @@ -80,12 +94,18 @@ const useHandleDevicesDetached = () => { for (const event of apiEvents) { dispatch(removeDevice(event)) } - dispatch(closeAllModals()) + if (backupProcess) { dispatch(setBackupProcessStatus("FAILED")) } }, - [dispatch, backupProcess] + [ + dispatch, + backupProcess, + migrationStatus, + sourceDevice?.serialNumber, + targetDevice?.serialNumber, + ] ) } diff --git a/libs/generic-view/store/src/lib/modals/actions.ts b/libs/generic-view/store/src/lib/modals/actions.ts index 7c37d0011d..924caf29e3 100644 --- a/libs/generic-view/store/src/lib/modals/actions.ts +++ b/libs/generic-view/store/src/lib/modals/actions.ts @@ -14,3 +14,6 @@ export const replaceModal = createAction(ActionName.ReplaceModal) export const closeDomainModals = createAction>>( ActionName.CloseDomainModals ) +export const setDeviceErrorModalOpened = createAction( + ActionName.SetDeviceErrorModalOpened +) diff --git a/libs/generic-view/store/src/lib/modals/reducer.ts b/libs/generic-view/store/src/lib/modals/reducer.ts index 34719a4cc4..4d21912834 100644 --- a/libs/generic-view/store/src/lib/modals/reducer.ts +++ b/libs/generic-view/store/src/lib/modals/reducer.ts @@ -10,6 +10,7 @@ import { closeModal, openModal, replaceModal, + setDeviceErrorModalOpened, } from "./actions" export interface Modal { @@ -20,10 +21,12 @@ export interface Modal { interface GenericState { queue: Modal[] + deviceErrorModalOpened: boolean } const initialState: GenericState = { queue: [], + deviceErrorModalOpened: false, } export const genericModalsReducer = createReducer(initialState, (builder) => { @@ -44,4 +47,7 @@ export const genericModalsReducer = createReducer(initialState, (builder) => { (modal) => modal.domain !== action.payload.domain ) }) + builder.addCase(setDeviceErrorModalOpened, (state, action) => { + state.deviceErrorModalOpened = action.payload + }) }) diff --git a/libs/generic-view/store/src/lib/selectors/data-migration-pure-db-indexing.ts b/libs/generic-view/store/src/lib/selectors/data-migration-pure-db-indexing.ts new file mode 100644 index 0000000000..83dd6691a6 --- /dev/null +++ b/libs/generic-view/store/src/lib/selectors/data-migration-pure-db-indexing.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ReduxRootState } from "Core/__deprecated__/renderer/store" +import { createSelector } from "@reduxjs/toolkit" + +export const selectDataMigrationPureDbIndexing = createSelector( + (state: ReduxRootState) => state.dataMigration.pureDbIndexing, + (indexing) => indexing +) diff --git a/libs/generic-view/store/src/lib/selectors/data-transfer-progress.ts b/libs/generic-view/store/src/lib/selectors/data-transfer-progress.ts index 7bca98df34..36269f7f96 100644 --- a/libs/generic-view/store/src/lib/selectors/data-transfer-progress.ts +++ b/libs/generic-view/store/src/lib/selectors/data-transfer-progress.ts @@ -7,7 +7,7 @@ import { ReduxRootState } from "Core/__deprecated__/renderer/store" import { createSelector } from "@reduxjs/toolkit" import { DomainTransferStatus } from "generic-view/store" -const domainStepsCount = 3 +const domainStepsCount = 4 const getDomainProgress = (status: DomainTransferStatus) => { switch (status) { @@ -18,6 +18,8 @@ const getDomainProgress = (status: DomainTransferStatus) => { case "IN-PROGRESS": return 2 case "PROCESSING": + return 3 + case "FINISHED": return domainStepsCount } } @@ -27,9 +29,14 @@ export const selectDataTransferStatus = createSelector( (status) => status ) +export const selectDataTransferDomains = createSelector( + (state: ReduxRootState) => state.genericDataTransfer.transfer, + (transfer) => transfer +) + export const selectDataTransferProgress = createSelector( selectDataTransferStatus, - (state: ReduxRootState) => state.genericDataTransfer.transfer, + selectDataTransferDomains, (mainStatus, dataTransfer) => { if (mainStatus === "IDLE") { return { progress: 0, currentDomain: undefined } diff --git a/libs/generic-view/store/src/lib/selectors/device-error-modal-opened.ts b/libs/generic-view/store/src/lib/selectors/device-error-modal-opened.ts new file mode 100644 index 0000000000..6b8cb493ef --- /dev/null +++ b/libs/generic-view/store/src/lib/selectors/device-error-modal-opened.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createSelector } from "@reduxjs/toolkit" +import { ReduxRootState } from "Core/__deprecated__/renderer/store" + +export const selectDeviceErrorModalOpened = createSelector( + (state: ReduxRootState) => state.genericModals.deviceErrorModalOpened, + (deviceErrorModalOpened) => deviceErrorModalOpened +) diff --git a/libs/generic-view/store/src/lib/selectors/index.ts b/libs/generic-view/store/src/lib/selectors/index.ts index f371e62e7f..e4ea501d41 100644 --- a/libs/generic-view/store/src/lib/selectors/index.ts +++ b/libs/generic-view/store/src/lib/selectors/index.ts @@ -28,3 +28,5 @@ export * from "./select-configured-devices" export * from "./select-devices" export * from "./select-failed-devices" export * from "./select-generic-view-state" +export * from "./data-migration-pure-db-indexing" +export * from "./device-error-modal-opened" diff --git a/libs/generic-view/ui/src/index.ts b/libs/generic-view/ui/src/index.ts index 6e2ee16d11..e23984b365 100644 --- a/libs/generic-view/ui/src/index.ts +++ b/libs/generic-view/ui/src/index.ts @@ -14,12 +14,13 @@ import { Icon } from "./lib/icon/icon" export * from "./lib/icon/icon" export * from "./lib/api-connection-demo" -export * from "./lib/interactive/modal/modal" +export * from "./lib/interactive/modal" export * from "./lib/interactive/modal/modal-base" export * from "./lib/shared/shared" export * from "./lib/predefined/backup/backup-error" export * from "./lib/predefined/backup-restore/backup-restore-error" export * from "./lib/predefined/import-contacts/import-contacts-error" +export * from "./lib/predefined/data-migration/components/transfer-error-modal" export { DataMigrationPage } from "./lib/predefined/data-migration/data-migration" const apiComponents = { diff --git a/libs/generic-view/ui/src/lib/interactive/modal/helpers/modal-content.tsx b/libs/generic-view/ui/src/lib/interactive/modal/helpers/modal-content.tsx index 01f6a731ac..5e70c2a9a5 100644 --- a/libs/generic-view/ui/src/lib/interactive/modal/helpers/modal-content.tsx +++ b/libs/generic-view/ui/src/lib/interactive/modal/helpers/modal-content.tsx @@ -14,8 +14,7 @@ import { import { TitleIcon } from "./modal-title-icon" import { ScrollableContent } from "./modal-scrollable-content" import { ModalVisibilityControllerHidden } from "./modal-visibility-controller" - -export type ModalSize = "small" | "medium" | "large" +import { ModalSize } from "generic-view/utils" export const getModalSize = (size: ModalSize) => { switch (size) { diff --git a/libs/generic-view/ui/src/lib/interactive/modal/modal-base.tsx b/libs/generic-view/ui/src/lib/interactive/modal/modal-base.tsx index e1ba28fc4b..69132ca66b 100644 --- a/libs/generic-view/ui/src/lib/interactive/modal/modal-base.tsx +++ b/libs/generic-view/ui/src/lib/interactive/modal/modal-base.tsx @@ -12,8 +12,9 @@ import React, { import ReactModal from "react-modal" import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum" import { theme } from "generic-view/theme" -import { getModalSize, ModalSize } from "./helpers/modal-content" -import { ModalBaseConfig } from "generic-view/models" +import { getModalSize } from "./helpers/modal-content" +import { ModalBaseConfig, ModalConfig } from "generic-view/models" +import { ModalSize } from "generic-view/utils" interface Props extends PropsWithChildren { opened: boolean @@ -21,6 +22,7 @@ interface Props extends PropsWithChildren { size?: ModalSize closeButton?: ReactElement overlayHidden?: boolean + modalLayer?: ModalConfig["modalLayer"] } export const ModalBase: FunctionComponent = ({ @@ -30,6 +32,7 @@ export const ModalBase: FunctionComponent = ({ size = "medium", closeButton, overlayHidden, + modalLayer = ModalLayers.Default, }) => { const modalMaxHeight = useMemo(() => { if (config?.maxHeight === undefined) { @@ -78,10 +81,10 @@ export const ModalBase: FunctionComponent = ({ isOpen={opened} style={{ overlay: { - zIndex: ModalLayers.Default, + zIndex: modalLayer, }, content: { - zIndex: ModalLayers.Default, + zIndex: modalLayer, // @ts-ignore "--modal-padding": modalPadding, "--modal-width": modalWidth, diff --git a/libs/generic-view/ui/src/lib/interactive/modal/modal.tsx b/libs/generic-view/ui/src/lib/interactive/modal/modal.tsx index 36132e7abf..016dad7869 100644 --- a/libs/generic-view/ui/src/lib/interactive/modal/modal.tsx +++ b/libs/generic-view/ui/src/lib/interactive/modal/modal.tsx @@ -43,6 +43,7 @@ export const Modal: BaseGenericComponent< padding: config.padding, gap: config.gap, }} + modalLayer={config.modalLayer} > {config.closeButtonAction && ( diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/device-selector.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/device-selector.tsx index c708b05860..feaed09b50 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/components/device-selector.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/device-selector.tsx @@ -15,7 +15,7 @@ import { useDispatch, useSelector } from "react-redux" import { selectDataMigrationSourceDevice, selectDataMigrationTargetDevice, - setSourceDevice, + setDataMigrationSourceDevice, } from "generic-view/store" import { DeviceId } from "Core/device/constants/device-id" import { defineMessages } from "react-intl" @@ -59,7 +59,7 @@ export const DeviceSelector: FunctionComponent = ({ type, devices }) => { const selectDevice = (serialNumber: DeviceId) => { setOpened(false) if (type === "source") { - dispatch(setSourceDevice(serialNumber)) + dispatch(setDataMigrationSourceDevice(serialNumber)) } } diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/error-modal.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/error-modal.tsx index 29c7d77ebc..7590ca0e96 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/components/error-modal.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/error-modal.tsx @@ -3,18 +3,16 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { FunctionComponent } from "react" -import { useDispatch } from "react-redux" +import React, { FunctionComponent, ReactElement } from "react" import { Modal } from "../../../interactive/modal/modal" import { ButtonAction, IconType } from "generic-view/utils" import { ButtonSecondary } from "../../../buttons/button-secondary" -import { clearDataMigrationProgress } from "generic-view/store" import { modalTransitionDuration } from "generic-view/theme" interface Props { modalIcon: IconType title: string - description: string + description: string | ReactElement buttonLabel: string onButtonClick?: VoidFunction } @@ -26,14 +24,11 @@ export const ErrorModal: FunctionComponent = ({ buttonLabel, onButtonClick, }) => { - const dispatch = useDispatch() - const buttonAction: ButtonAction = { type: "custom", callback: () => { setTimeout(() => { onButtonClick?.() - dispatch(clearDataMigrationProgress()) }, modalTransitionDuration) }, } @@ -42,7 +37,7 @@ export const ErrorModal: FunctionComponent = ({ <> {title} -

{description}

+ {typeof description === "string" ?

{description}

: description} = ({ onClose }) => { <> {intl.formatMessage(messages.title)} -

{intl.formatMessage(messages.noChangesDescription)}

+ { +interface Props { + onButtonClick?: VoidFunction +} + +export const TransferErrorModal: FunctionComponent = ({ + onButtonClick, +}) => { const title = intl.formatMessage(messages.title) - const description = intl.formatMessage(messages.connectionFailed) + const buttonLabel = intl.formatMessage(messages.closeButtonLabel) return ( } + buttonLabel={buttonLabel} + onButtonClick={onButtonClick} /> ) } diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx new file mode 100644 index 0000000000..31943c2763 --- /dev/null +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx @@ -0,0 +1,89 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React, { FunctionComponent, useEffect, useMemo, useRef } from "react" +import { defineMessages } from "react-intl" +import { useSelector } from "react-redux" +import { + selectDataMigrationFeatures, + selectDataMigrationStatus, + selectDataTransferDomains, +} from "generic-view/store" +import { intl } from "Core/__deprecated__/renderer/utils/intl" +import { isEmpty } from "lodash" + +const messages = defineMessages({ + genericDescription: { + id: "module.genericViews.dataMigration.failure.genericDescription", + }, + noChangesDescription: { + id: "module.genericViews.dataMigration.failure.noChangesDescription", + }, + cancelledPartialChangesDescription: { + id: "module.genericViews.dataMigration.cancelled.partialChangesDescription", + }, + failedPartialChangesDescription: { + id: "module.genericViews.dataMigration.transferError.partialChangesDescription", + }, +}) + +export const TransferFailMessage: FunctionComponent = () => { + const selectedFeatures = useSelector(selectDataMigrationFeatures) + const transferDomains = useSelector(selectDataTransferDomains) + const transferDomainsRef = useRef() + const dataMigrationStatus = useSelector(selectDataMigrationStatus) + + const description = useMemo(() => { + const domains = isEmpty(transferDomainsRef.current) + ? transferDomains + : transferDomainsRef.current + + if (isEmpty(domains)) { + return

{intl.formatMessage(messages.genericDescription)}

+ } + + const transferredDomains = Object.entries(domains) + .filter(([, { status }]) => status === "FINISHED") + .map(([domain]) => domain) + + if (isEmpty(transferredDomains)) { + return

{intl.formatMessage(messages.noChangesDescription)}

+ } + + const notTransferredFeatures = selectedFeatures.filter((feature) => + transferredDomains?.every((domain) => !domain.startsWith(feature)) + ) + + return ( +
+

+ {intl.formatMessage( + dataMigrationStatus === "CANCELLED" + ? messages.cancelledPartialChangesDescription + : messages.failedPartialChangesDescription + )} +

+
    + {notTransferredFeatures.map((feature) => ( +
  • + {intl.formatMessage({ + id: `module.genericViews.dataMigration.features.${feature}`, + })} +
  • + ))} +
+
+ ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataMigrationStatus, selectedFeatures, transferDomains]) + + useEffect(() => { + if (isEmpty(transferDomainsRef.current) && !isEmpty(transferDomains)) { + transferDomainsRef.current = transferDomains + } + }, [transferDomains]) + + return description +} diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx index a1bb5a2b67..e90fb5170e 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx @@ -9,7 +9,6 @@ import React, { FunctionComponent, useCallback, useEffect, - useRef, useState, } from "react" import { defineMessages } from "react-intl" @@ -18,14 +17,17 @@ import { APIFC } from "generic-view/utils" import { McDataMigrationConfig } from "generic-view/models" import { getActiveDevice } from "device-manager/feature" import { - clearDataMigrationProgress, + abortDataMigration, + DataMigrationPercentageProgress, performDataMigration, selectDataMigrationSourceDevice, selectDataMigrationSourceDevices, selectDataMigrationStatus, selectDataMigrationTargetDevices, setDataMigrationFeatures, - setSourceDevice, + setDataMigrationProgress, + setDataMigrationSourceDevice, + setDataMigrationStatus, startDataMigration, } from "generic-view/store" import { Instruction, InstructionWrapper } from "./instruction" @@ -69,10 +71,9 @@ const DataMigrationUI: FunctionComponent = ({ ) as Device[] const sourceDevice = useSelector(selectDataMigrationSourceDevice) const dataMigrationStatus = useSelector(selectDataMigrationStatus) - const dataMigrationAbortReference = useRef() const [modalOpened, setModalOpened] = useState(false) - const noSourceDeviceSelected = !sourceDevice + const noSourceDeviceSelected = dataMigrationStatus === "IDLE" && !sourceDevice const displayInstruction = Boolean(sourceDevices.length) !== Boolean(targetDevices.length) const displayTargetSelector = @@ -84,23 +85,18 @@ const DataMigrationUI: FunctionComponent = ({ } const startTransfer = useCallback(() => { - const promise = dispatch(performDataMigration()) - dataMigrationAbortReference.current = ( - promise as unknown as { - abort: VoidFunction - } - ).abort + dispatch(performDataMigration()) }, [dispatch]) - const cancelMigration = () => { - // TODO: add confirmation modal support - dataMigrationAbortReference.current?.() - } + const cancelMigration = useCallback(() => { + dispatch(abortDataMigration({ reason: "CANCELLED" })) + }, [dispatch]) const onFinish = () => { setModalOpened(false) setTimeout(() => { - dispatch(clearDataMigrationProgress()) + dispatch(setDataMigrationStatus("IDLE")) + dispatch(setDataMigrationProgress(DataMigrationPercentageProgress.None)) dispatch(setDataMigrationFeatures([])) }, modalTransitionDuration) } @@ -108,9 +104,9 @@ const DataMigrationUI: FunctionComponent = ({ useEffect(() => { if (activeDevice?.deviceType === "APIDevice" && noSourceDeviceSelected) { if (sourceDevices.length > 0) { - dispatch(setSourceDevice(sourceDevices[0].serialNumber)) + dispatch(setDataMigrationSourceDevice(sourceDevices[0].serialNumber)) } else { - dispatch(setSourceDevice(undefined)) + dispatch(setDataMigrationSourceDevice(undefined)) } } }, [ @@ -151,22 +147,53 @@ const DataMigrationUI: FunctionComponent = ({ onUnlock={startMigration} /> - {(dataMigrationStatus === "PURE-CRITICAL-BATTERY" || - dataMigrationStatus === "PURE-ONBOARDING-REQUIRED" || - dataMigrationStatus === "PURE-UPDATE-REQUIRED") && } - {dataMigrationStatus === "FAILED" && } - {dataMigrationStatus === "IN-PROGRESS" && ( - - )} - {dataMigrationStatus === "CANCELLED" && ( - - )} - {dataMigrationStatus === "COMPLETED" && ( - - )} + + + + + + + + + + + + + ) diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/transfer-setup.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/transfer-setup.tsx index a02f1a855f..3844991c7b 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/transfer-setup.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/transfer-setup.tsx @@ -10,9 +10,14 @@ import { ButtonPrimary } from "../../buttons/button-primary" import { DataMigrationFeature } from "generic-view/models" import styled from "styled-components" import { useSelector } from "react-redux" -import { selectDataMigrationFeatures } from "generic-view/store" +import { + selectDataMigrationFeatures, + selectDataMigrationPureDbIndexing, + selectDataMigrationStatus, +} from "generic-view/store" import { defineMessages } from "react-intl" import { intl } from "Core/__deprecated__/renderer/utils/intl" +import { SpinnerLoader } from "../../shared/shared" const messages = defineMessages({ unlockInfo: { @@ -21,6 +26,9 @@ const messages = defineMessages({ transferButton: { id: "module.genericViews.dataMigration.transferSetup.transferButton", }, + pureNotReady: { + id: "module.genericViews.dataMigration.transferSetup.pureNotReady", + }, }) interface Props { @@ -33,6 +41,8 @@ export const TransferSetup: FunctionComponent = ({ onStartMigration, }) => { const selectedFeatures = useSelector(selectDataMigrationFeatures) + const pureDbIndexing = useSelector(selectDataMigrationPureDbIndexing) + const dataMigrationStatus = useSelector(selectDataMigrationStatus) return ( <> @@ -41,7 +51,16 @@ export const TransferSetup: FunctionComponent = ({
-

{intl.formatMessage(messages.unlockInfo)}

+ {dataMigrationStatus === "IDLE" && pureDbIndexing ? ( + + +

{intl.formatMessage(messages.pureNotReady)}

+
+ ) : ( + +

{intl.formatMessage(messages.unlockInfo)}

+
+ )} = ({ type: "custom", callback: onStartMigration, }, - disabled: selectedFeatures.length === 0, + disabled: pureDbIndexing || selectedFeatures.length === 0, }} />
@@ -79,14 +98,24 @@ const Footer = styled.div` padding-right: 3.2rem; box-shadow: 0 1rem 5rem rgba(0, 0, 0, 0.08); + button { + min-width: 16.4rem; + } +` + +const FooterMessage = styled.div` + display: flex; + align-items: center; + gap: 0.9rem; + p { font-size: ${({ theme }) => theme.fontSize.labelText}; line-height: ${({ theme }) => theme.lineHeight.labelText}; letter-spacing: 0.04em; color: ${({ theme }) => theme.color.grey1}; } +` - button { - min-width: 16.4rem; - } +const FooterSpinner = styled(SpinnerLoader)` + margin-left: -4.1rem; ` diff --git a/libs/generic-view/ui/src/lib/shared/spinner-loader.tsx b/libs/generic-view/ui/src/lib/shared/spinner-loader.tsx index 3c89f4d996..b8f0ad34d0 100644 --- a/libs/generic-view/ui/src/lib/shared/spinner-loader.tsx +++ b/libs/generic-view/ui/src/lib/shared/spinner-loader.tsx @@ -8,10 +8,14 @@ import styled, { keyframes } from "styled-components" import { Icon } from "../icon/icon" import { IconType } from "generic-view/utils" -export const SpinnerLoader: FunctionComponent = () => { +interface Props { + dark?: boolean +} + +export const SpinnerLoader: FunctionComponent = ({ dark, ...props }) => { return ( - - + + ) } diff --git a/libs/generic-view/utils/src/index.ts b/libs/generic-view/utils/src/index.ts index 6527f290db..ac7fe08d3c 100644 --- a/libs/generic-view/utils/src/index.ts +++ b/libs/generic-view/utils/src/index.ts @@ -11,4 +11,4 @@ export * from "./lib/models/button.types" export * from "./lib/models/layout.types" export * from "./lib/view-generators/generate-view-config" export * from "./lib/map-layout-sizes/map-layout-sizes" -export * from "./lib/models/custom-modal-error" +export * from "./lib/models/modal.types" diff --git a/libs/generic-view/utils/src/lib/models/custom-modal-error.ts b/libs/generic-view/utils/src/lib/models/modal.types.ts similarity index 80% rename from libs/generic-view/utils/src/lib/models/custom-modal-error.ts rename to libs/generic-view/utils/src/lib/models/modal.types.ts index e660d31ea7..b4d1476b1d 100644 --- a/libs/generic-view/utils/src/lib/models/custom-modal-error.ts +++ b/libs/generic-view/utils/src/lib/models/modal.types.ts @@ -7,3 +7,5 @@ export interface CustomModalError { title?: string message?: string } + +export type ModalSize = "small" | "medium" | "large" diff --git a/libs/shared/feature/src/lib/use-data-migration-device-selector.ts b/libs/shared/feature/src/lib/use-data-migration-device-selector.ts index 45d0635bcb..13429d95a5 100644 --- a/libs/shared/feature/src/lib/use-data-migration-device-selector.ts +++ b/libs/shared/feature/src/lib/use-data-migration-device-selector.ts @@ -5,7 +5,7 @@ import { useCallback } from "react" import { DeviceId } from "Core/device/constants/device-id" -import { setSourceDevice } from "generic-view/store" +import { setDataMigrationSourceDevice } from "generic-view/store" import { deactivateDevice, getActiveDevice, @@ -27,7 +27,7 @@ export const useDataMigrationDeviceSelector = () => { pathToRedirect = URL_DEVICE_INITIALIZATION.root ) => { if (!activeDevice || !activeDevice.serialNumber) return - await dispatch(setSourceDevice(activeDevice.serialNumber)) + await dispatch(setDataMigrationSourceDevice(activeDevice.serialNumber)) await dispatch(deactivateDevice()) await dispatch(handleDeviceActivated(serialNumber)) history.push(pathToRedirect) From 81df470dabe5842e3d7a23599c02b01273da7b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kurczewski?= Date: Fri, 12 Jul 2024 09:38:01 +0200 Subject: [PATCH 3/7] Added error handling for not enough storage (#1967) --- .../renderer/locales/default/en-US.json | 1 + .../lib/modals/data-migration-error-modal.tsx | 2 + .../store/src/lib/data-transfer/reducer.ts | 8 +- .../transfer-data-to-device.action.ts | 18 +++-- .../lib/selectors/data-transfer-error-type.ts | 12 +++ .../store/src/lib/selectors/index.ts | 1 + .../components/transfer-fail-message.tsx | 80 ++++++++++++------- .../data-migration/data-migration.tsx | 2 + 8 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 libs/generic-view/store/src/lib/selectors/data-transfer-error-type.ts diff --git a/libs/core/__deprecated__/renderer/locales/default/en-US.json b/libs/core/__deprecated__/renderer/locales/default/en-US.json index f051ecb042..206249d219 100644 --- a/libs/core/__deprecated__/renderer/locales/default/en-US.json +++ b/libs/core/__deprecated__/renderer/locales/default/en-US.json @@ -954,6 +954,7 @@ "module.genericViews.dataMigration.pureError.connection.title": "Data transfer failed", "module.genericViews.dataMigration.transferError.title": "Data transfer failed", "module.genericViews.dataMigration.transferError.partialChangesDescription": "We transferred some data before the process failed, but we didn't transfer these items:", + "module.genericViews.dataMigration.transferError.notEnoughSpace": "Your phone’s storage is full.", "module.genericViews.dataMigration.progress.title": "Transferring data", "module.genericViews.dataMigration.progress.description": "Please don't unplug your devices from your computer.", "module.genericViews.dataMigration.progress.cancelButtonLabel": "Cancel", diff --git a/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx b/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx index f42b5347f3..f97332e9c1 100644 --- a/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx +++ b/libs/generic-view/feature/src/lib/modals/data-migration-error-modal.tsx @@ -7,6 +7,7 @@ import React, { FunctionComponent, useEffect } from "react" import { Modal, TransferErrorModal } from "generic-view/ui" import { useDispatch, useSelector } from "react-redux" import { + clearDataTransfer, DataMigrationPercentageProgress, selectActiveApiDeviceId, selectDataMigrationStatus, @@ -32,6 +33,7 @@ export const DataMigrationErrorModal: FunctionComponent = () => { dispatch(setDataMigrationSourceDevice(undefined)) dispatch(setDataMigrationPureDbIndexing(false)) dispatch(setDeviceErrorModalOpened(false)) + dispatch(clearDataTransfer()) } useEffect(() => { diff --git a/libs/generic-view/store/src/lib/data-transfer/reducer.ts b/libs/generic-view/store/src/lib/data-transfer/reducer.ts index 3346b3542f..027ce320de 100644 --- a/libs/generic-view/store/src/lib/data-transfer/reducer.ts +++ b/libs/generic-view/store/src/lib/data-transfer/reducer.ts @@ -11,6 +11,7 @@ import { setDataTransferStatus, } from "./actions" import { transferDataToDevice } from "./transfer-data-to-device.action" +import { ApiFileTransferError } from "device/models" type Domain = string @@ -32,6 +33,7 @@ export type DataTransfer = Record export interface DataTransferState { transfer: DataTransfer status: DataTransferStatus + errorType?: ApiFileTransferError abortController?: AbortController } @@ -49,15 +51,17 @@ export const genericDataTransferReducer = createReducer( ...action.payload, } }) - builder.addCase(clearDataTransfer, (state, action) => { + builder.addCase(clearDataTransfer, (state) => { state.transfer = {} state.status = "IDLE" + delete state.errorType }) - builder.addCase(transferDataToDevice.pending, (state, action) => { + builder.addCase(transferDataToDevice.pending, (state) => { state.status = "IN-PROGRESS" }) builder.addCase(transferDataToDevice.rejected, (state, action) => { state.status = "FAILED" + state.errorType = action.payload as ApiFileTransferError }) builder.addCase(setDataTransferStatus, (state, action) => { state.status = action.payload diff --git a/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts b/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts index 3c2fb82aa8..97d19cbef1 100644 --- a/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts +++ b/libs/generic-view/store/src/lib/data-transfer/transfer-data-to-device.action.ts @@ -6,7 +6,11 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { ReduxRootState } from "Core/__deprecated__/renderer/store" import { ActionName } from "../action-names" -import { DataTransferDomain, UnifiedContact } from "device/models" +import { + ApiFileTransferError, + DataTransferDomain, + UnifiedContact, +} from "device/models" import { cancelDataTransferRequest, checkDataTransferRequest, @@ -47,10 +51,10 @@ export const transferDataToDevice = createAsyncThunk< transferAbortController.abort = abort dispatch(setDataTransferAbort(transferAbortController)) - const handleError = () => { - void clearTransfers?.() + const handleError = (type?: ApiFileTransferError) => { dispatch(clearDataTransfer()) - return rejectWithValue(undefined) + void clearTransfers?.() + return rejectWithValue(type) } const abortListener = async () => { @@ -90,7 +94,9 @@ export const transferDataToDevice = createAsyncThunk< ) if (!preDataTransferResponse.ok) { - return handleError() + return handleError( + preDataTransferResponse.error?.type as ApiFileTransferError + ) } const { dataTransferId, domains: domainsPathMap } = @@ -153,7 +159,7 @@ export const transferDataToDevice = createAsyncThunk< ) if (!preSendResponse.ok) { - return handleError() + return handleError(preSendResponse.error?.type as ApiFileTransferError) } domainsPaths[index].transfer = preSendResponse.data diff --git a/libs/generic-view/store/src/lib/selectors/data-transfer-error-type.ts b/libs/generic-view/store/src/lib/selectors/data-transfer-error-type.ts new file mode 100644 index 0000000000..e4338011b0 --- /dev/null +++ b/libs/generic-view/store/src/lib/selectors/data-transfer-error-type.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ReduxRootState } from "Core/__deprecated__/renderer/store" +import { createSelector } from "@reduxjs/toolkit" + +export const selectDataTransferErrorType = createSelector( + (state: ReduxRootState) => state.genericDataTransfer.errorType, + (error) => error +) diff --git a/libs/generic-view/store/src/lib/selectors/index.ts b/libs/generic-view/store/src/lib/selectors/index.ts index e4ea501d41..2aff2bd537 100644 --- a/libs/generic-view/store/src/lib/selectors/index.ts +++ b/libs/generic-view/store/src/lib/selectors/index.ts @@ -30,3 +30,4 @@ export * from "./select-failed-devices" export * from "./select-generic-view-state" export * from "./data-migration-pure-db-indexing" export * from "./device-error-modal-opened" +export * from "./data-transfer-error-type" diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx index 31943c2763..f8ac0a5476 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/components/transfer-fail-message.tsx @@ -3,16 +3,19 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { FunctionComponent, useEffect, useMemo, useRef } from "react" +import React, { useEffect, useMemo, useRef } from "react" import { defineMessages } from "react-intl" import { useSelector } from "react-redux" import { selectDataMigrationFeatures, selectDataMigrationStatus, selectDataTransferDomains, + selectDataTransferErrorType, } from "generic-view/store" import { intl } from "Core/__deprecated__/renderer/utils/intl" import { isEmpty } from "lodash" +import { ApiFileTransferError } from "device/models" +import { FunctionComponent } from "Core/core/types/function-component.interface" const messages = defineMessages({ genericDescription: { @@ -27,6 +30,9 @@ const messages = defineMessages({ failedPartialChangesDescription: { id: "module.genericViews.dataMigration.transferError.partialChangesDescription", }, + notEnoughSpace: { + id: "module.genericViews.dataMigration.transferError.notEnoughSpace", + }, }) export const TransferFailMessage: FunctionComponent = () => { @@ -34,48 +40,62 @@ export const TransferFailMessage: FunctionComponent = () => { const transferDomains = useSelector(selectDataTransferDomains) const transferDomainsRef = useRef() const dataMigrationStatus = useSelector(selectDataMigrationStatus) + const dataTransferErrorType = useSelector(selectDataTransferErrorType) const description = useMemo(() => { const domains = isEmpty(transferDomainsRef.current) ? transferDomains : transferDomainsRef.current - - if (isEmpty(domains)) { - return

{intl.formatMessage(messages.genericDescription)}

- } - const transferredDomains = Object.entries(domains) .filter(([, { status }]) => status === "FINISHED") .map(([domain]) => domain) - - if (isEmpty(transferredDomains)) { - return

{intl.formatMessage(messages.noChangesDescription)}

- } - const notTransferredFeatures = selectedFeatures.filter((feature) => transferredDomains?.every((domain) => !domain.startsWith(feature)) ) - return ( -
-

- {intl.formatMessage( - dataMigrationStatus === "CANCELLED" - ? messages.cancelledPartialChangesDescription - : messages.failedPartialChangesDescription - )} + const notEnoughSpace = + dataTransferErrorType === ApiFileTransferError.NotEnoughSpace + + if (notEnoughSpace && isEmpty(domains)) { + return ( +

+ {intl.formatMessage(messages.notEnoughSpace)}{" "} + {intl.formatMessage(messages.noChangesDescription)}

-
    - {notTransferredFeatures.map((feature) => ( -
  • - {intl.formatMessage({ - id: `module.genericViews.dataMigration.features.${feature}`, - })} -
  • - ))} -
-
- ) + ) + } + + if (notEnoughSpace || !isEmpty(transferredDomains)) { + return ( +
+

+ {notEnoughSpace && ( + <>{intl.formatMessage(messages.notEnoughSpace)} + )} + {intl.formatMessage( + dataMigrationStatus === "CANCELLED" + ? messages.cancelledPartialChangesDescription + : messages.failedPartialChangesDescription + )} +

+
    + {notTransferredFeatures.map((feature) => ( +
  • + {intl.formatMessage({ + id: `module.genericViews.dataMigration.features.${feature}`, + })} +
  • + ))} +
+
+ ) + } + + if (!isEmpty(domains) && isEmpty(transferredDomains)) { + return

{intl.formatMessage(messages.noChangesDescription)}

+ } + + return

{intl.formatMessage(messages.genericDescription)}

// eslint-disable-next-line react-hooks/exhaustive-deps }, [dataMigrationStatus, selectedFeatures, transferDomains]) diff --git a/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx b/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx index e90fb5170e..4d90f67cc1 100644 --- a/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx +++ b/libs/generic-view/ui/src/lib/predefined/data-migration/data-migration.tsx @@ -18,6 +18,7 @@ import { McDataMigrationConfig } from "generic-view/models" import { getActiveDevice } from "device-manager/feature" import { abortDataMigration, + clearDataTransfer, DataMigrationPercentageProgress, performDataMigration, selectDataMigrationSourceDevice, @@ -98,6 +99,7 @@ const DataMigrationUI: FunctionComponent = ({ dispatch(setDataMigrationStatus("IDLE")) dispatch(setDataMigrationProgress(DataMigrationPercentageProgress.None)) dispatch(setDataMigrationFeatures([])) + dispatch(clearDataTransfer()) }, modalTransitionDuration) } From d05059166348d8cc10af18ea637dddf1bc57f5eb Mon Sep 17 00:00:00 2001 From: mkurczewski Date: Thu, 18 Jul 2024 09:27:34 +0200 Subject: [PATCH 4/7] Fixed data migration cancelling conditions --- .../store/src/lib/hooks/use-api-serial-port-listeners.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts b/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts index a3bf164cad..7921b473a2 100644 --- a/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts +++ b/libs/generic-view/store/src/lib/hooks/use-api-serial-port-listeners.ts @@ -75,7 +75,7 @@ const useHandleDevicesDetached = () => { async (deviceDetachedEvents: DeviceBaseProperties[]) => { for (const event of deviceDetachedEvents) { if ( - migrationStatus !== "IDLE" && + migrationStatus === "IN-PROGRESS" && (sourceDevice?.serialNumber === event.serialNumber || targetDevice?.serialNumber === event.serialNumber) ) { From 6aec9eb0da6361cd0f02fcbfeb3b5752d098a5c5 Mon Sep 17 00:00:00 2001 From: mkurczewski Date: Fri, 19 Jul 2024 11:14:36 +0200 Subject: [PATCH 5/7] Added handling of file select cancel --- .../feature/src/lib/file-dialog/file-dialog.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/system-utils/feature/src/lib/file-dialog/file-dialog.service.ts b/libs/system-utils/feature/src/lib/file-dialog/file-dialog.service.ts index 90c044982c..6de40dccea 100644 --- a/libs/system-utils/feature/src/lib/file-dialog/file-dialog.service.ts +++ b/libs/system-utils/feature/src/lib/file-dialog/file-dialog.service.ts @@ -55,6 +55,11 @@ export class FileDialog { BrowserWindow.getFocusedWindow() as BrowserWindow, openDialogOptions ) + if (result.canceled) { + return Result.failed( + new AppError(FileDialogError.OpenFile, "cancelled") + ) + } this.lastSelectedPath = result.filePaths[0] return Result.success(result.filePaths) From 5534483b4e3abf3438f41c8a415688beb8c50c58 Mon Sep 17 00:00:00 2001 From: mkurczewski Date: Fri, 19 Jul 2024 12:49:49 +0200 Subject: [PATCH 6/7] Fixed contacts import from file --- .../store/src/lib/imports/import-contacts-from.file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/generic-view/store/src/lib/imports/import-contacts-from.file.ts b/libs/generic-view/store/src/lib/imports/import-contacts-from.file.ts index 2de1e5dd97..0f12ed4a29 100644 --- a/libs/generic-view/store/src/lib/imports/import-contacts-from.file.ts +++ b/libs/generic-view/store/src/lib/imports/import-contacts-from.file.ts @@ -72,7 +72,7 @@ export const importContactsFromFile = createAsyncThunk< return handleError() } - const fileBuffer = Buffer.from(filePath) + const fileBuffer = Buffer.from(fileResponse.data) const { encoding } = detect(fileBuffer) const content = fileBuffer.toString(encoding as BufferEncoding) From 827422cd13014a71e8c8db2f66f1f059ef66e2e5 Mon Sep 17 00:00:00 2001 From: mkurczewski Date: Fri, 19 Jul 2024 20:35:30 +0200 Subject: [PATCH 7/7] Enhanced error handling for contacts import --- .../renderer/locales/default/en-US.json | 7 ++ libs/generic-view/store/src/index.ts | 4 +- .../store/src/lib/action-names.ts | 18 +++-- .../store/src/lib/imports/actions.ts | 8 +- .../store/src/lib/imports/reducer.ts | 25 ++++--- ...on.ts => start-import-to-device.action.ts} | 32 ++++---- .../import-contacts-progress.tsx | 73 ++++++++++++++++++- .../import-contacts-provider.tsx | 4 +- .../import-contacts/import-contacts.tsx | 28 ++++--- 9 files changed, 144 insertions(+), 55 deletions(-) rename libs/generic-view/store/src/lib/imports/{start-data-transfer-to-device.action.ts => start-import-to-device.action.ts} (90%) diff --git a/libs/core/__deprecated__/renderer/locales/default/en-US.json b/libs/core/__deprecated__/renderer/locales/default/en-US.json index 206249d219..191a1cee87 100644 --- a/libs/core/__deprecated__/renderer/locales/default/en-US.json +++ b/libs/core/__deprecated__/renderer/locales/default/en-US.json @@ -907,6 +907,12 @@ "module.genericViews.importContacts.progress.title": "Importing", "module.genericViews.importContacts.progress.description": "Please wait, this might take a few minutes.", "module.genericViews.importContacts.progress.progressDetails": "Preparing contacts", + "module.genericViews.importContacts.progress.progressDetailsForFeature": "Importing {featureLabel}", + "module.genericViews.importContacts.progress.cancelButtonLabel": "Cancel", + "module.genericViews.importContacts.cancelConfirm.title": "Cancel import?", + "module.genericViews.importContacts.cancelConfirm.description": "We'll stop the import process and no changes will be made to your phone.", + "module.genericViews.importContacts.cancelConfirm.backButtonLabel": "Back", + "module.genericViews.importContacts.cancelConfirm.cancelButtonLabel": "Cancel import", "module.genericViews.importContacts.success.title": "Import complete", "module.genericViews.importContacts.success.description": "{count, plural, =-1 {All contacts} one {# contact} other {# contacts}} imported successfully.", "module.genericViews.importContacts.success.closeButtonLabel": "Close", @@ -914,6 +920,7 @@ "module.genericViews.importContacts.failure.defaultErrorMessage": "The process was interrupted.", "module.genericViews.importContacts.failure.fileErrorMessage": "We didn’t recognise some of the data in the file. Check that it only contains contact data and try again.", "module.genericViews.importContacts.failure.closeButtonLabel": "Close", + "module.genericViews.importContacts.failure.notEnoughSpace": "Your phone’s storage is full.", "module.genericViews.importContacts.cancellation.title": "Import cancelled", "module.genericViews.importContacts.cancellation.message": "No changes were made to your phone’s contacts.", "module.genericViews.importContacts.fileUploadDialog.title": "Import contacts", diff --git a/libs/generic-view/store/src/index.ts b/libs/generic-view/store/src/index.ts index 34a44c5ff6..98b6381729 100644 --- a/libs/generic-view/store/src/index.ts +++ b/libs/generic-view/store/src/index.ts @@ -25,7 +25,7 @@ export * from "./lib/imports/get-google-contacts.action" export * from "./lib/imports/reducer" export * from "./lib/imports/actions" export * from "./lib/imports/import-contacts-from-external-source.action" -export * from "./lib/imports/start-data-transfer-to-device.action" +export * from "./lib/imports/start-import-to-device.action" export * from "./lib/imports/import-contacts-from.file" export { getDisplayName } from "./lib/imports/contacts-mappers/helpers" export * from "./lib/external-providers/reducer" @@ -40,7 +40,7 @@ export * from "./lib/external-providers/outlook/outlook.interface" export * from "./lib/external-providers/outlook/outlook.constants" export * from "./lib/external-providers/outlook/token-requester" export * from "./lib/external-providers/external-providers.interface" -export * from "./lib/imports/start-data-transfer-to-device.action" +export * from "./lib/imports/start-import-to-device.action" export * from "./lib/data-migration/reducer" export * from "./lib/data-migration/actions" export * from "./lib/data-migration/start-data.migration" diff --git a/libs/generic-view/store/src/lib/action-names.ts b/libs/generic-view/store/src/lib/action-names.ts index c3aec3c48b..24b2d6fed0 100644 --- a/libs/generic-view/store/src/lib/action-names.ts +++ b/libs/generic-view/store/src/lib/action-names.ts @@ -8,6 +8,7 @@ export enum ActionName { GetAny = "api-actions/get-any", GetMenuConfig = "api-actions/get-menu-config", GetOutboxData = "api-actions/get-outbox-data", + SetMenu = "generic-views/set-menu", SetViewLayout = "generic-views/set-view-layout", SetViewData = "generic-views/set-view-data", @@ -15,12 +16,14 @@ export enum ActionName { AddDevice = "generic-views/add-device", RemoveDevice = "generic-views/remove-device", SetDeviceState = "generic-views/set-device-state", + OpenModal = "generic-modals/open-modal", CloseModal = "generic-modals/close-modal", CloseAllModals = "generic-modals/close-all-modals", ReplaceModal = "generic-modals/replace-modal", CloseDomainModals = "generic-modals/close-domain-modals", SetDeviceErrorModalOpened = "generic-modals/set-device-error-modal-opened", + SetBackupProcess = "generic-backups/set-backup-process", CleanBackupProcess = "generic-backups/clean-backup-process", BackupProcessStatus = "generic-backups/backup-process-status", @@ -33,6 +36,7 @@ export enum ActionName { ChooseRestoreFile = "generic-backups/choose-restore-file", RefreshBackupList = "generic-backups/refresh-backup-list", LoadBackupMetadata = "generic-backups/load-backup-metadata", + FileTransferSend = "generic-file-transfer/send", PreFileTransferSend = "generic-file-transfer/pre-send", ChunkFileTransferSend = "generic-file-transfer/chunk-sent", @@ -42,25 +46,27 @@ export enum ActionName { ChunkFileTransferGet = "generic-file-transfer/chunk-get", ClearFileTransferGetError = "generic-file-transfer/clear-get-errors", TransferDataToDevice = "generic-file-transfer/transfer-data-to-device", + SetDataTransfer = "generic-data-transfer/set-data-transfer", SetDataTransferStatus = "generic-data-transfer/set-data-transfer-status", ClearDataTransfer = "generic-data-transfer/clear-data-transfer", + SetDataTransferAbort = "generic-data-transfer/set-data-transfer-abort", + AbortDataTransfer = "generic-data-transfer/abort-data-transfer", + StartGoogleAuthorization = "generic-imports/start-google-authorization", ImportContactsFromExternalSource = "generic-imports/import-contacts-from-external-source", - StartDataTransferToDevice = "generic-imports/start-data-transfer-to-device", - SetDataTransferProcessStatus = "generic-imports/set-data-transfer-process-status", - SetDataTransferProcessFileStatus = "generic-imports/set-data-transfer-process-file-status", - SetDataTransferAbort = "generic-imports/set-data-transfer-abort", - AbortDataTransfer = "generic-imports/abort-data-transfer", + StartImportToDevice = "generic-imports/start-data-transfer-to-device", + SetImportProcessStatus = "generic-imports/set-data-transfer-process-status", + SetImportProcessFileStatus = "generic-imports/set-data-transfer-process-file-status", CleanImportProcess = "generic-imports/clean-import-process", StartContactsFileImport = "generic-imports/start-contacts-file-import", - GetContactsFromCSV = "generic-imports/get-contacts-from-csv", GoogleAuthorizeProcess = "generic-imports/google-authorization-process", GoogleGetContactsProcess = "generic-imports/google-get-contacts-process", SetGoogleAuthDataProcess = "generic-imports/set-google-auth-data-process", OutlookAuthorizeProcess = "generic-imports/outlook-authorization-process", OutlookGetContactsProcess = "generic-imports/outlook-get-contacts-process", SetOutlookAuthDataProcess = "generic-imports/set-outlook-auth-data-process", + SetDataMigrationSourceDevice = "data-migration/set-source-device", SetDataMigrationFeatures = "data-migration/set-features", setDataMigrationPureDbIndexing = "data-migration/set-pure-db-indexing", diff --git a/libs/generic-view/store/src/lib/imports/actions.ts b/libs/generic-view/store/src/lib/imports/actions.ts index 5119bfa28a..0671b28f62 100644 --- a/libs/generic-view/store/src/lib/imports/actions.ts +++ b/libs/generic-view/store/src/lib/imports/actions.ts @@ -9,11 +9,11 @@ import { ImportStatus, ProcessFileStatus } from "./reducer" export const cleanImportProcess = createAction(ActionName.CleanImportProcess) -export const setDataTransferProcessStatus = createAction<{ +export const setImportProcessStatus = createAction<{ status: ImportStatus -}>(ActionName.SetDataTransferProcessStatus) +}>(ActionName.SetImportProcessStatus) -export const setDataTransferProcessFileStatus = createAction<{ +export const setImportProcessFileStatus = createAction<{ domain: string status: ProcessFileStatus -}>(ActionName.SetDataTransferProcessFileStatus) +}>(ActionName.SetImportProcessFileStatus) diff --git a/libs/generic-view/store/src/lib/imports/reducer.ts b/libs/generic-view/store/src/lib/imports/reducer.ts index 8a16a6d4d5..91c6a65677 100644 --- a/libs/generic-view/store/src/lib/imports/reducer.ts +++ b/libs/generic-view/store/src/lib/imports/reducer.ts @@ -7,12 +7,12 @@ import { createReducer } from "@reduxjs/toolkit" import { startGoogleAuthorization } from "./get-google-contacts.action" import { cleanImportProcess, - setDataTransferProcessFileStatus, - setDataTransferProcessStatus, + setImportProcessFileStatus, + setImportProcessStatus, } from "./actions" import { importContactsFromExternalSource } from "./import-contacts-from-external-source.action" -import { UnifiedContact } from "device/models" -import { startDataTransferToDevice } from "./start-data-transfer-to-device.action" +import { ApiFileTransferError, UnifiedContact } from "device/models" +import { startImportToDevice } from "./start-import-to-device.action" import { importContactsFromFile } from "./import-contacts-from.file" interface ImportsState { @@ -44,7 +44,7 @@ export interface ImportProviderState { string, { transferId?: number; status: ProcessFileStatus } > - error?: string + error?: string | ApiFileTransferError } const initialState: ImportsState = { @@ -138,7 +138,7 @@ export const importsReducer = createReducer(initialState, (builder) => { contacts: action.payload, } }) - builder.addCase(setDataTransferProcessStatus, (state, action) => { + builder.addCase(setImportProcessStatus, (state, action) => { const provider = state.currentImportProvider as ImportProvider if (state.providers[provider]) { state.providers[provider] = { @@ -149,7 +149,7 @@ export const importsReducer = createReducer(initialState, (builder) => { } } }) - builder.addCase(startDataTransferToDevice.pending, (state, action) => { + builder.addCase(startImportToDevice.pending, (state, action) => { const provider = state.currentImportProvider as ImportProvider if (state.providers[provider]) { state.providers[provider] = { @@ -160,17 +160,22 @@ export const importsReducer = createReducer(initialState, (builder) => { } } }) - builder.addCase(startDataTransferToDevice.rejected, (state, action) => { + builder.addCase(startImportToDevice.rejected, (state, action) => { const provider = state.currentImportProvider as ImportProvider if (state.providers[provider]) { state.providers[provider] = { ...state.providers[provider], domainFilesTransfer: {}, status: "FAILED", + ...(action.payload + ? { + error: action.payload as ImportProviderState["error"], + } + : {}), } } }) - builder.addCase(startDataTransferToDevice.fulfilled, (state, action) => { + builder.addCase(startImportToDevice.fulfilled, (state, action) => { const provider = state.currentImportProvider as ImportProvider if (state.providers[provider]) { state.providers[provider] = { @@ -180,7 +185,7 @@ export const importsReducer = createReducer(initialState, (builder) => { } } }) - builder.addCase(setDataTransferProcessFileStatus, (state, action) => { + builder.addCase(setImportProcessFileStatus, (state, action) => { const provider = state.currentImportProvider as ImportProvider if (state.providers[provider]) { state.providers[provider]!.domainFilesTransfer = { diff --git a/libs/generic-view/store/src/lib/imports/start-data-transfer-to-device.action.ts b/libs/generic-view/store/src/lib/imports/start-import-to-device.action.ts similarity index 90% rename from libs/generic-view/store/src/lib/imports/start-data-transfer-to-device.action.ts rename to libs/generic-view/store/src/lib/imports/start-import-to-device.action.ts index ff48cac595..f0f32be178 100644 --- a/libs/generic-view/store/src/lib/imports/start-data-transfer-to-device.action.ts +++ b/libs/generic-view/store/src/lib/imports/start-import-to-device.action.ts @@ -5,7 +5,11 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { ReduxRootState } from "Core/__deprecated__/renderer/store" -import { DataTransferDomain, UnifiedContact } from "device/models" +import { + ApiFileTransferError, + DataTransferDomain, + UnifiedContact, +} from "device/models" import { cancelDataTransferRequest, checkDataTransferRequest, @@ -17,17 +21,14 @@ import { import { ActionName } from "../action-names" import { sendFile } from "../file-transfer/send-file.action" import { selectActiveApiDeviceId } from "../selectors" -import { - setDataTransferProcessFileStatus, - setDataTransferProcessStatus, -} from "./actions" +import { setImportProcessFileStatus, setImportProcessStatus } from "./actions" -export const startDataTransferToDevice = createAsyncThunk< +export const startImportToDevice = createAsyncThunk< undefined, { domains: DataTransferDomain[]; contactsIds: string[] }, { state: ReduxRootState } >( - ActionName.StartDataTransferToDevice, + ActionName.StartImportToDevice, async ( { domains, contactsIds }, { getState, dispatch, rejectWithValue, signal } @@ -131,14 +132,15 @@ export const startDataTransferToDevice = createAsyncThunk< ) if (!preSendResponse.ok) { - console.log("cannot start pre send") clearTransfers() - return rejectWithValue(undefined) + return rejectWithValue( + preSendResponse.error?.type as ApiFileTransferError + ) } domainsPaths[i].transfer = preSendResponse.data dispatch( - setDataTransferProcessFileStatus({ + setImportProcessFileStatus({ domain: domain.domainKey, status: "PENDING", }) @@ -146,7 +148,7 @@ export const startDataTransferToDevice = createAsyncThunk< } dispatch( - setDataTransferProcessStatus({ + setImportProcessStatus({ status: "IMPORT-INTO-DEVICE-FILES-TRANSFER", }) ) @@ -157,7 +159,7 @@ export const startDataTransferToDevice = createAsyncThunk< } const domain = domainsPaths[i] dispatch( - setDataTransferProcessFileStatus({ + setImportProcessFileStatus({ domain: domain.domainKey, status: "IN_PROGRESS", }) @@ -179,7 +181,7 @@ export const startDataTransferToDevice = createAsyncThunk< } if (!aborted) { dispatch( - setDataTransferProcessFileStatus({ + setImportProcessFileStatus({ domain: domain.domainKey, status: "DONE", }) @@ -189,9 +191,7 @@ export const startDataTransferToDevice = createAsyncThunk< clearTransfers() - dispatch( - setDataTransferProcessStatus({ status: "IMPORT-DEVICE-DATA-TRANSFER" }) - ) + dispatch(setImportProcessStatus({ status: "IMPORT-DEVICE-DATA-TRANSFER" })) if (aborted) { return rejectWithValue(undefined) diff --git a/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-progress.tsx b/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-progress.tsx index cdd5083f31..9e27849b2a 100644 --- a/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-progress.tsx +++ b/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-progress.tsx @@ -3,15 +3,18 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React from "react" +import React, { useState } from "react" +import { FunctionComponent } from "Core/core/types/function-component.interface" import styled from "styled-components" -import { IconType } from "generic-view/utils" +import { ButtonAction, IconType } from "generic-view/utils" import { ProgressBar } from "../../interactive/progress-bar/progress-bar" import { Modal } from "../../interactive/modal" import { defineMessages } from "react-intl" import { intl } from "Core/__deprecated__/renderer/utils/intl" import { useSelector } from "react-redux" import { importContactsProgress } from "generic-view/store" +import { ButtonSecondary } from "../../buttons/button-secondary" +import { ButtonPrimary } from "../../buttons/button-primary" const messages = defineMessages({ title: { @@ -23,12 +26,65 @@ const messages = defineMessages({ progressDetails: { id: "module.genericViews.importContacts.progress.progressDetails", }, + cancelButtonLabel: { + id: "module.genericViews.importContacts.progress.cancelButtonLabel", + }, + cancelTitle: { + id: "module.genericViews.importContacts.cancelConfirm.title", + }, + cancelDescription: { + id: "module.genericViews.importContacts.cancelConfirm.description", + }, + cancelBackButtonLabel: { + id: "module.genericViews.importContacts.cancelConfirm.backButtonLabel", + }, + cancelAbortButtonLabel: { + id: "module.genericViews.importContacts.cancelConfirm.cancelButtonLabel", + }, }) -export const ImportContactsProgress = () => { +interface Props { + cancelAction: ButtonAction +} + +export const ImportContactsProgress: FunctionComponent = ({ + cancelAction, +}) => { + const [cancelRequested, setCancelRequested] = useState(false) const { progress } = useSelector(importContactsProgress) const detailMessage = intl.formatMessage(messages.progressDetails) + const requestCancel = () => { + setCancelRequested(true) + } + + if (cancelRequested) { + return ( + <> + + {intl.formatMessage(messages.title)} +

{intl.formatMessage(messages.cancelDescription)}

+ + setCancelRequested(false), + }, + text: intl.formatMessage(messages.cancelBackButtonLabel), + }} + /> + + + + ) + } + return ( <> @@ -43,6 +99,17 @@ export const ImportContactsProgress = () => { message: detailMessage, }} /> + + + ) } diff --git a/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-provider.tsx b/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-provider.tsx index 1270a47dc8..a8580701d0 100644 --- a/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-provider.tsx +++ b/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts-provider.tsx @@ -13,7 +13,7 @@ import { useDispatch } from "react-redux" import { Dispatch } from "Core/__deprecated__/renderer/store" import { importContactsFromFile, - setDataTransferProcessStatus, + setImportProcessStatus, startGoogleAuthorization, } from "generic-view/store" import { ButtonSecondary } from "../../buttons/button-secondary" @@ -87,7 +87,7 @@ export const ImportContactsProvider = () => { callback: () => { dispatch(importContactsFromFile()) dispatch( - setDataTransferProcessStatus({ status: "FILE-SELECT" }) + setImportProcessStatus({ status: "FILE-SELECT" }) ) }, }, diff --git a/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts.tsx b/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts.tsx index e6ba3288b0..9bb909e1fd 100644 --- a/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts.tsx +++ b/libs/generic-view/ui/src/lib/predefined/import-contacts/import-contacts.tsx @@ -16,7 +16,7 @@ import { importContactsFromExternalSource, ImportStatus, importStatusSelector, - startDataTransferToDevice, + startImportToDevice, } from "generic-view/store" import { ImportContactsProvider } from "./import-contacts-provider" import { ImportContactsLoader } from "./import-contats-loader" @@ -31,6 +31,7 @@ import { intl } from "Core/__deprecated__/renderer/utils/intl" import { defineMessages } from "react-intl" import { useFormContext } from "react-hook-form" import { ImportContactsConfig } from "generic-view/models" +import { ApiFileTransferError } from "device/models" const messages = defineMessages({ cancellationErrorTitle: { @@ -39,6 +40,9 @@ const messages = defineMessages({ cancellationErrorMessage: { id: "module.genericViews.importContacts.cancellation.message", }, + notEnoughSpace: { + id: "module.genericViews.importContacts.failure.notEnoughSpace", + }, }) const ImportContactsForm: FunctionComponent = ({ @@ -54,17 +58,18 @@ const ImportContactsForm: FunctionComponent = ({ const selectedContacts = watch(SELECTED_CONTACTS_FIELD) || [] const currentStatus = freezedStatus || importStatus - const abortButtonVisible = + const importInProgress = currentStatus === "IMPORT-INTO-DEVICE-IN-PROGRESS" || currentStatus === "IMPORT-INTO-DEVICE-FILES-TRANSFER" || currentStatus === "IMPORT-DEVICE-DATA-TRANSFER" const closeButtonVisible = - currentStatus !== "PENDING-AUTH" && !abortButtonVisible + currentStatus !== "PENDING-AUTH" && !importInProgress const closeModal = () => { setFreezedStatus(importStatus) dispatch(closeModalAction({ key: modalKey })) dispatch(cleanImportProcess()) + setError(undefined) } const importCloseButtonAction: ButtonAction = { @@ -76,7 +81,7 @@ const ImportContactsForm: FunctionComponent = ({ type: "custom", callback: () => { const promise = dispatch( - startDataTransferToDevice({ + startImportToDevice({ domains: ["contacts-v1"], contactsIds: selectedContacts, }) @@ -103,8 +108,12 @@ const ImportContactsForm: FunctionComponent = ({ useEffect(() => { if (importError) { + let message = importError + if (importError === ApiFileTransferError.NotEnoughSpace) { + message = intl.formatMessage(messages.notEnoughSpace) + } setError({ - message: importError, + message: message as string, }) } }, [importError]) @@ -128,9 +137,6 @@ const ImportContactsForm: FunctionComponent = ({ {closeButtonVisible && ( )} - {abortButtonVisible && ( - - )} {(currentStatus === undefined || currentStatus === "INIT") && ( )} @@ -148,10 +154,8 @@ const ImportContactsForm: FunctionComponent = ({ {currentStatus === "IMPORT-INTO-MC-DONE" && ( )} - {(currentStatus === "IMPORT-INTO-DEVICE-IN-PROGRESS" || - currentStatus === "IMPORT-INTO-DEVICE-FILES-TRANSFER" || - currentStatus === "IMPORT-DEVICE-DATA-TRANSFER") && ( - + {importInProgress && ( + )} {currentStatus === "FAILED" && (