mirror of
https://github.com/mudita/mudita-center.git
synced 2025-12-23 22:28:03 -05:00
[CP-3385] export files from app list (#2557)
This commit is contained in:
@@ -553,3 +553,7 @@ ipcMain.answerRenderer(
|
||||
ipcMain.answerRenderer(OutlookAuthActions.CloseWindow, () => {
|
||||
outlookAuthWindow?.close()
|
||||
})
|
||||
|
||||
ipcMain.answerRenderer("get-downloads-path", async () => {
|
||||
return app.getPath("downloads")
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface MtpTransferFileData {
|
||||
storageId: string
|
||||
destinationPath: string
|
||||
sourcePath: string
|
||||
action?: string
|
||||
}
|
||||
|
||||
export interface TransferFileResultData {
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"component.deviceSelection.selectDevice": "Show connected devices",
|
||||
"component.deviceSelection.changeDevice": "Show connected devices",
|
||||
"component.dialog.title": "Browse Files",
|
||||
"component.dialog.openDirectory.title": "Browse directories",
|
||||
"component.drawer.headerTitle": "Select a device",
|
||||
"component.deviceLockedModalHeadline": "Unlock your phone",
|
||||
"component.deviceLockedModalParagraph": "Enter your passcode or scan your fingerprint",
|
||||
@@ -1023,6 +1024,7 @@
|
||||
"module.genericViews.entities.delete.failure.some.modalDescription": "{succeededFiles, plural, =0 {No files} one {# file} other {# files}} successfully deleted, {failedFiles, plural, one {# file} other {# files}} not deleted:",
|
||||
"module.genericViews.entities.delete.success.toastMessage": "{count, plural, one {# item} other {# items}} deleted",
|
||||
"module.genericViews.filesManager.upload.progress.modalTitle": "{filesCount, plural, one {Transferring file to Kompakt...} other {Transferring # files to Kompakt...}}",
|
||||
"module.genericViews.filesManager.export.progress.modalTitle": "{filesCount, plural, one {Exporting file...} other {Exporting # files...}}",
|
||||
"module.genericViews.filesManager.upload.progress.cancelButton": "Cancel",
|
||||
"module.genericViews.filesManager.upload.failure.all.modalTitle": "Couldn't transfer {filesCount, plural, one {file} other {some files}}",
|
||||
"module.genericViews.filesManager.upload.failure.all.unknownError": "Please try again.",
|
||||
|
||||
@@ -60,13 +60,24 @@ export class MtpFileTransferService {
|
||||
storageId,
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
action,
|
||||
}: MtpTransferFileData): Promise<ResultObject<TransferFileResultData>> {
|
||||
const result = await this.mtp.uploadFile({
|
||||
deviceId,
|
||||
storageId,
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
})
|
||||
let result: ResultObject<TransferFileResultData>
|
||||
if (action === "export") {
|
||||
result = await this.mtp.exportFile({
|
||||
deviceId,
|
||||
storageId,
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
})
|
||||
} else {
|
||||
result = await this.mtp.uploadFile({
|
||||
deviceId,
|
||||
storageId,
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
})
|
||||
}
|
||||
return this.mapToApiFileTransferErrorResult(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export class MockFileDialog {
|
||||
return this.mockFilePaths
|
||||
}
|
||||
|
||||
getMockDirectoryPath(): string {
|
||||
return this.mockFilePaths[0]
|
||||
}
|
||||
|
||||
clearMockFilePaths() {
|
||||
this.mockFilePaths = []
|
||||
}
|
||||
|
||||
@@ -67,9 +67,9 @@ import { buttonPlain } from "./lib/button-plain"
|
||||
import { highlightText } from "./lib/highlight-text"
|
||||
import { mcContactsSearchResults } from "./lib/mc-contacts-search-results"
|
||||
import { TypographyMap } from "./lib/typography"
|
||||
import { mcFilesManagerUploadProgress } from "./lib/mc-files-manager-upload-progress"
|
||||
import { mcFilesManagerUploadFinished } from "./lib/mc-files-manager-upload-finished"
|
||||
import { mcFilesManagerUploadValidationError } from "./lib/mc-files-manager-upload-validation-error"
|
||||
import { mcFilesManagerTransferProgress } from "./lib/mc-files-manager-transfer-progress"
|
||||
import { mcFilesManagerTransferFinished } from "./lib/mc-files-manager-transfer-finished"
|
||||
import { mcFilesManagerTransferValidationError } from "./lib/mc-files-manager-transfer-validation-error"
|
||||
import { entitiesDeleteError } from "./lib/entities-delete-error"
|
||||
import { mcAppInstallationProgress } from "./lib/mc-app-installation-progress"
|
||||
import { mcAppInstallationError } from "./lib/mc-app-installation-error"
|
||||
@@ -144,9 +144,9 @@ export * from "./lib/app-portal"
|
||||
export * from "./lib/highlight-text"
|
||||
export * from "./lib/mc-contacts-search-results"
|
||||
export * from "./lib/typography"
|
||||
export * from "./lib/mc-files-manager-upload-progress"
|
||||
export * from "./lib/mc-files-manager-upload-finished"
|
||||
export * from "./lib/mc-files-manager-upload-validation-error"
|
||||
export * from "./lib/mc-files-manager-transfer-progress"
|
||||
export * from "./lib/mc-files-manager-transfer-finished"
|
||||
export * from "./lib/mc-files-manager-transfer-validation-error"
|
||||
export * from "./lib/entities-delete-error"
|
||||
export * from "./lib/mc-app-installation-progress"
|
||||
export * from "./lib/mc-app-installation-error"
|
||||
@@ -217,10 +217,10 @@ export default {
|
||||
[appPortal.key]: appPortal,
|
||||
[highlightText.key]: highlightText,
|
||||
[mcContactsSearchResults.key]: mcContactsSearchResults,
|
||||
[mcFilesManagerUploadProgress.key]: mcFilesManagerUploadProgress,
|
||||
[mcFilesManagerUploadFinished.key]: mcFilesManagerUploadFinished,
|
||||
[mcFilesManagerUploadValidationError.key]:
|
||||
mcFilesManagerUploadValidationError,
|
||||
[mcFilesManagerTransferProgress.key]: mcFilesManagerTransferProgress,
|
||||
[mcFilesManagerTransferFinished.key]: mcFilesManagerTransferFinished,
|
||||
[mcFilesManagerTransferValidationError.key]:
|
||||
mcFilesManagerTransferValidationError,
|
||||
[entitiesDeleteError.key]: entitiesDeleteError,
|
||||
[mcAppInstallationProgress.key]: mcAppInstallationProgress,
|
||||
[mcAppInstallationError.key]: mcAppInstallationError,
|
||||
|
||||
@@ -138,10 +138,19 @@ export type NativeActionSelectFiles = z.infer<
|
||||
>
|
||||
|
||||
const nativeActionSelectDirectoryValidator = z.object({
|
||||
// TODO: Implement "select-directory" action
|
||||
type: z.literal("select-directory"),
|
||||
title: z.string().optional(),
|
||||
defaultPath: z.string().optional(),
|
||||
formOptions: z.object({
|
||||
formKey: z.string().optional(),
|
||||
selectedDirectoryFieldName: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export type NativeActionSelectDirectory = z.infer<
|
||||
typeof nativeActionSelectDirectoryValidator
|
||||
>
|
||||
|
||||
export const nativeActionsValidator = z.union([
|
||||
nativeActionSelectFilesValidator,
|
||||
nativeActionSelectDirectoryValidator,
|
||||
@@ -175,14 +184,41 @@ export type FilesTransferUploadFilesAction = z.infer<
|
||||
typeof filesTransferUploadFilesActionValidator
|
||||
>
|
||||
|
||||
const filesTransferDownloadFilesActionValidator = z.object({
|
||||
// TODO: Implement "download-files" action
|
||||
type: z.literal("download-files"),
|
||||
const filesTransferExportFilesActionValidator = z.object({
|
||||
type: z.literal("export-files"),
|
||||
destinationPath: z.string(),
|
||||
entitiesType: z.string().optional(),
|
||||
actionId: z.string(),
|
||||
|
||||
formOptions: z.object({
|
||||
formKey: z.string(),
|
||||
selectedDirectoryFieldName: z.string(),
|
||||
}),
|
||||
|
||||
sourceFormKey: z.string(),
|
||||
selectedItemsFieldName: z.string(),
|
||||
|
||||
preActions: z
|
||||
.object({
|
||||
validationFailure: entityPostActionsValidator,
|
||||
})
|
||||
.optional(),
|
||||
|
||||
postActions: z
|
||||
.object({
|
||||
success: entityPostActionsValidator,
|
||||
failure: entityPostActionsValidator,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type FilesTransferExportFilesAction = z.infer<
|
||||
typeof filesTransferExportFilesActionValidator
|
||||
>
|
||||
|
||||
export const filesTransferActionValidator = z.union([
|
||||
filesTransferUploadFilesActionValidator,
|
||||
filesTransferDownloadFilesActionValidator,
|
||||
filesTransferExportFilesActionValidator,
|
||||
])
|
||||
|
||||
const startAppInstallationActionValidator = z.object({
|
||||
|
||||
@@ -9,17 +9,19 @@ const dataValidator = z.object({
|
||||
freeSpace: z.number(),
|
||||
})
|
||||
|
||||
export type McFilesManagerUploadFinishedData = z.infer<typeof dataValidator>
|
||||
export type McFilesManagerTransferFinishedData = z.infer<typeof dataValidator>
|
||||
|
||||
const configValidator = z.object({
|
||||
modalKey: z.string(),
|
||||
uploadActionId: z.string(),
|
||||
transferActionId: z.string(),
|
||||
})
|
||||
|
||||
export type McFilesManagerUploadFinishedConfig = z.infer<typeof configValidator>
|
||||
export type McFilesManagerTransferFinishedConfig = z.infer<
|
||||
typeof configValidator
|
||||
>
|
||||
|
||||
export const mcFilesManagerUploadFinished = {
|
||||
key: "mc-files-manager-upload-finished",
|
||||
export const mcFilesManagerTransferFinished = {
|
||||
key: "mc-files-manager-transfer-finished",
|
||||
dataValidator,
|
||||
configValidator,
|
||||
} as const
|
||||
@@ -8,16 +8,19 @@ import { z } from "zod"
|
||||
const dataValidator = z.undefined()
|
||||
|
||||
const configValidator = z.object({
|
||||
storagePath: z.string(),
|
||||
storagePath: z.string().optional(),
|
||||
directoryPath: z.string(),
|
||||
entitiesType: z.string(),
|
||||
uploadActionId: z.string(),
|
||||
transferActionId: z.string(),
|
||||
actionType: z.string(),
|
||||
})
|
||||
|
||||
export type McFilesManagerUploadProgressConfig = z.infer<typeof configValidator>
|
||||
export type McFilesManagerTransferProgressConfig = z.infer<
|
||||
typeof configValidator
|
||||
>
|
||||
|
||||
export const mcFilesManagerUploadProgress = {
|
||||
key: "mc-files-manager-upload-progress",
|
||||
export const mcFilesManagerTransferProgress = {
|
||||
key: "mc-files-manager-transfer-progress",
|
||||
dataValidator,
|
||||
configValidator,
|
||||
} as const
|
||||
@@ -9,7 +9,7 @@ const dataValidator = z.object({
|
||||
fileList: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type McFilesManagerUploadValidationErrorData = z.infer<
|
||||
export type McFilesManagerTransferValidationErrorData = z.infer<
|
||||
typeof dataValidator
|
||||
>
|
||||
|
||||
@@ -18,12 +18,12 @@ const configValidator = z.object({
|
||||
uploadActionId: z.string(),
|
||||
})
|
||||
|
||||
export type McFilesManagerUploadValidationErrorConfig = z.infer<
|
||||
export type McFilesManagerTransferValidationErrorConfig = z.infer<
|
||||
typeof configValidator
|
||||
>
|
||||
|
||||
export const mcFilesManagerUploadValidationError = {
|
||||
key: "mc-files-manager-upload-validation-error",
|
||||
export const mcFilesManagerTransferValidationError = {
|
||||
key: "mc-files-manager-transfer-validation-error",
|
||||
dataValidator,
|
||||
configValidator,
|
||||
} as const
|
||||
@@ -54,6 +54,7 @@ export enum ActionName {
|
||||
TransferDataToDevice = "generic-file-transfer/transfer-data-to-device",
|
||||
// New approach for transferring files
|
||||
SendFiles = "generic-file-transfer/send-files",
|
||||
ExportFiles = "generic-file-transfer/export-files",
|
||||
SendFileViaSerialPort = "generic-file-transfer/send-file-via-serial-port",
|
||||
SendFileViaMTP = "generic-file-transfer/send-file-via-mtp",
|
||||
SendFilesPreSend = "generic-file-transfer/send-files-pre-send",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { GetFileErrorPayload } from "./get-file.action"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { AppError } from "Core/core/errors"
|
||||
import { ApiFileTransferError } from "device/models"
|
||||
import { FilesTransferMode } from "./files-transfer-mode.type"
|
||||
import { FilesTransferMode } from "./files-transfer.type"
|
||||
|
||||
export const fileTransferSendPrepared = createAction<
|
||||
Pick<FileProgress, "chunksCount" | "transferId" | "filePath">
|
||||
|
||||
@@ -7,3 +7,8 @@ export enum FilesTransferMode {
|
||||
SerialPort = "serial-port",
|
||||
Mtp = "mtp",
|
||||
}
|
||||
|
||||
export enum SendFilesAction {
|
||||
ActionUpload = "upload",
|
||||
ActionExport = "export",
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { sliceSegments } from "shared/utils"
|
||||
import { ApiFileTransferError } from "device/models"
|
||||
import { getDeviceStoragesRequest, getMtpDeviceIdRequest } from "device/feature"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
@@ -17,6 +16,7 @@ import { selectDeviceById } from "../selectors/select-devices"
|
||||
|
||||
interface GetMtpSendFileMetadataPayload {
|
||||
destinationPath: string
|
||||
isMtpPathInternal: boolean
|
||||
customDeviceId?: DeviceId
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getMtpSendFileMetadata = createAsyncThunk<
|
||||
>(
|
||||
ActionName.GetMtpSendFileMetadata,
|
||||
async (
|
||||
{ destinationPath, customDeviceId },
|
||||
{ destinationPath, customDeviceId, isMtpPathInternal },
|
||||
{ signal, getState, rejectWithValue }
|
||||
) => {
|
||||
const deviceId = customDeviceId || selectActiveApiDeviceId(getState())
|
||||
@@ -54,8 +54,6 @@ export const getMtpSendFileMetadata = createAsyncThunk<
|
||||
return rejectWithValue(error)
|
||||
}
|
||||
|
||||
const isInternal = destinationPath.startsWith("/storage/emulated/0")
|
||||
|
||||
const mtpDeviceStorages = await getDeviceStoragesRequest(mtpDeviceId)
|
||||
if (signal.aborted) {
|
||||
return rejectWithValue(
|
||||
@@ -72,7 +70,7 @@ export const getMtpSendFileMetadata = createAsyncThunk<
|
||||
}
|
||||
|
||||
const storageId = mtpDeviceStorages.data.find(
|
||||
(s) => s.isInternal === isInternal
|
||||
(s) => s.isInternal === isMtpPathInternal
|
||||
)?.id
|
||||
|
||||
if (!storageId) {
|
||||
@@ -86,11 +84,7 @@ export const getMtpSendFileMetadata = createAsyncThunk<
|
||||
return {
|
||||
storageId,
|
||||
deviceId: mtpDeviceId,
|
||||
destinationPath: isInternal
|
||||
? destinationPath
|
||||
.replace(/^\/storage\/emulated\/0\//, "")
|
||||
.replace(/\/$/, "")
|
||||
: sliceSegments(destinationPath, 2),
|
||||
destinationPath: destinationPath,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ import { legacySendFile } from "./legacy-send-file.action"
|
||||
import { getFile } from "./get-file.action"
|
||||
import { sendFiles } from "./send-files.action"
|
||||
import { ApiFileTransferError } from "device/models"
|
||||
import { FilesTransferMode } from "./files-transfer-mode.type"
|
||||
import { FilesTransferMode } from "./files-transfer.type"
|
||||
import { sendFilesTransferAnalysis } from "./send-files-transfer-analysis.action"
|
||||
|
||||
interface FileTransferError {
|
||||
|
||||
@@ -21,13 +21,14 @@ import {
|
||||
trackInfo,
|
||||
} from "./actions"
|
||||
import { ActionName } from "../action-names"
|
||||
import { FilesTransferMode } from "./files-transfer-mode.type"
|
||||
import { FilesTransferMode } from "./files-transfer.type"
|
||||
|
||||
export interface SendFileViaMTPPayload {
|
||||
file: FileWithPath
|
||||
deviceId: string
|
||||
storageId: string
|
||||
destinationPath: string
|
||||
action?: string
|
||||
}
|
||||
|
||||
export const sendFileViaMTP = createAsyncThunk<
|
||||
@@ -39,7 +40,7 @@ export const sendFileViaMTP = createAsyncThunk<
|
||||
>(
|
||||
ActionName.SendFileViaMTP,
|
||||
async (
|
||||
{ file, deviceId, destinationPath, storageId },
|
||||
{ file, deviceId, destinationPath, storageId, action },
|
||||
{ dispatch, signal, rejectWithValue }
|
||||
) => {
|
||||
const handleAbort = async () => {
|
||||
@@ -65,6 +66,7 @@ export const sendFileViaMTP = createAsyncThunk<
|
||||
storageId,
|
||||
destinationPath,
|
||||
sourcePath: file.path,
|
||||
action,
|
||||
})
|
||||
|
||||
if (!startSendFileViaMtpResult.ok) {
|
||||
@@ -74,7 +76,7 @@ export const sendFileViaMTP = createAsyncThunk<
|
||||
const { transactionId } = startSendFileViaMtpResult.data
|
||||
|
||||
if (signal.aborted) {
|
||||
return await handleAbort()
|
||||
return await handleAbort()
|
||||
}
|
||||
|
||||
const abortListener = async () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { ApiFileTransferError } from "device/models"
|
||||
import { AppError } from "Core/core/errors"
|
||||
import { createEntityDataAction } from "../entities/create-entity-data.action"
|
||||
import { FilesTransferMode } from "./files-transfer-mode.type"
|
||||
import { FilesTransferMode } from "./files-transfer.type"
|
||||
|
||||
export interface SendFileViaSerialPortPayload {
|
||||
file: FileBase
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "./send-file-via-mtp.action"
|
||||
import { getMtpSendFileMetadata } from "./get-mtp-send-file-metadata.action"
|
||||
import { sendFileViaSerialPort } from "./send-file-via-serial-port.action"
|
||||
import { FilesTransferMode } from "./files-transfer-mode.type"
|
||||
import { FilesTransferMode, SendFilesAction } from "./files-transfer.type"
|
||||
import { isMtpInitializeAccessError } from "./is-mtp-initialize-access-error"
|
||||
import {
|
||||
selectFilesSendingGroup,
|
||||
@@ -37,8 +37,10 @@ export interface SendFilesPayload {
|
||||
actionId: string
|
||||
files: FileBase[]
|
||||
destinationPath: string
|
||||
isMtpPathInternal: boolean
|
||||
entitiesType?: string
|
||||
customDeviceId?: DeviceId
|
||||
actionType: SendFilesAction
|
||||
}
|
||||
|
||||
export const sendFiles = createAsyncThunk<
|
||||
@@ -48,7 +50,15 @@ export const sendFiles = createAsyncThunk<
|
||||
>(
|
||||
ActionName.SendFiles,
|
||||
async (
|
||||
{ actionId, files, destinationPath, entitiesType, customDeviceId },
|
||||
{
|
||||
actionId,
|
||||
files,
|
||||
destinationPath,
|
||||
entitiesType,
|
||||
customDeviceId,
|
||||
isMtpPathInternal,
|
||||
actionType,
|
||||
},
|
||||
{ dispatch, signal, abort, rejectWithValue, getState }
|
||||
) => {
|
||||
const deviceId = customDeviceId || selectActiveApiDeviceId(getState())
|
||||
@@ -66,7 +76,6 @@ export const sendFiles = createAsyncThunk<
|
||||
dispatch(
|
||||
sendFilesAbortRegister({ actionId, abortController: mainAbortController })
|
||||
)
|
||||
|
||||
const sendFileAbortController = new AbortController()
|
||||
const getMtpSendFileMetadataAbortController = new AbortController()
|
||||
|
||||
@@ -80,9 +89,12 @@ export const sendFiles = createAsyncThunk<
|
||||
let filesTransferMode = selectFilesTransferMode(getState())
|
||||
let mtpSendFileMetadata: Omit<SendFileViaMTPPayload, "file"> | undefined =
|
||||
undefined
|
||||
|
||||
const getMtpSendFileMetadataDispatch = dispatch(
|
||||
getMtpSendFileMetadata({ destinationPath, customDeviceId })
|
||||
getMtpSendFileMetadata({
|
||||
destinationPath,
|
||||
customDeviceId,
|
||||
isMtpPathInternal: isMtpPathInternal,
|
||||
})
|
||||
)
|
||||
|
||||
getMtpSendFileMetadataAbortController.abort = (
|
||||
@@ -92,7 +104,6 @@ export const sendFiles = createAsyncThunk<
|
||||
).abort
|
||||
|
||||
await preSendFilesCleanup()
|
||||
|
||||
const { meta, payload } = await getMtpSendFileMetadataDispatch
|
||||
|
||||
if (meta.requestStatus === "fulfilled" && payload !== undefined) {
|
||||
@@ -107,10 +118,13 @@ export const sendFiles = createAsyncThunk<
|
||||
|
||||
const checkMtpAvailability = async () => {
|
||||
const res = await dispatch(
|
||||
getMtpSendFileMetadata({ destinationPath, customDeviceId })
|
||||
getMtpSendFileMetadata({
|
||||
destinationPath: destinationPath,
|
||||
customDeviceId,
|
||||
isMtpPathInternal,
|
||||
})
|
||||
)
|
||||
const { meta, payload } = res
|
||||
|
||||
if (meta.requestStatus === "fulfilled" && payload !== undefined) {
|
||||
mtpSendFileMetadata = payload as Omit<SendFileViaMTPPayload, "file">
|
||||
dispatch(setFilesTransferMode(FilesTransferMode.Mtp))
|
||||
@@ -118,7 +132,6 @@ export const sendFiles = createAsyncThunk<
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const mtpMonitor = setInterval(async () => {
|
||||
const currentMode = selectFilesTransferMode(getState())
|
||||
if (
|
||||
@@ -136,7 +149,6 @@ export const sendFiles = createAsyncThunk<
|
||||
let currentFileIndex = 0
|
||||
|
||||
let wasAborted = false
|
||||
|
||||
const processFiles = async () => {
|
||||
while (currentFileIndex < files.length) {
|
||||
const file = files[currentFileIndex]
|
||||
@@ -148,6 +160,7 @@ export const sendFiles = createAsyncThunk<
|
||||
sendFileViaMTP({
|
||||
...mtpSendFileMetadata!,
|
||||
file,
|
||||
action: actionType,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +39,19 @@ export const selectEntities = createSelector(
|
||||
}
|
||||
)
|
||||
|
||||
export const selectEntitiesByIds = createSelector(
|
||||
selectEntities,
|
||||
(_: ReduxRootState, params: { ids: string[] }) => params.ids,
|
||||
(entities, ids) => {
|
||||
if (!entities || !entities.data) return []
|
||||
|
||||
const idSet = new Set(ids)
|
||||
return entities.data.filter((entity) =>
|
||||
idSet.has((entity as { id: string }).id)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const selectEntitiesMetadata = createSelector(
|
||||
selectEntities,
|
||||
(entities) => entities?.metadata
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { sliceSegments } from "shared/utils"
|
||||
|
||||
const internalPathPrefix = "/storage/emulated/0"
|
||||
|
||||
export const sliceMtpPaths = (path: string, isInternal: boolean) => {
|
||||
return isInternal
|
||||
? path.replace(internalPathPrefix, "").replace(/\/$/, "")
|
||||
: sliceSegments(path, 2)
|
||||
}
|
||||
|
||||
export const isMtpPathInternal = (path: string) => {
|
||||
return path.startsWith(internalPathPrefix)
|
||||
}
|
||||
@@ -19,10 +19,18 @@ jest.mock("./use-select-files-button-action", () => ({
|
||||
useSelectFilesButtonAction: jest.fn().mockReturnValue(jest.fn()),
|
||||
}))
|
||||
|
||||
jest.mock("./use-select-directory-button-action", () => ({
|
||||
useSelectDirectoryButtonAction: jest.fn().mockReturnValue(jest.fn()),
|
||||
}))
|
||||
|
||||
jest.mock("./use-upload-files-button-action", () => ({
|
||||
useUploadFilesButtonAction: jest.fn().mockReturnValue(jest.fn()),
|
||||
}))
|
||||
|
||||
jest.mock("./use-export-files-button-action", () => ({
|
||||
useExportFilesButtonAction: jest.fn().mockReturnValue(jest.fn()),
|
||||
}))
|
||||
|
||||
jest.mock("react-redux", () => ({
|
||||
useDispatch: jest.fn().mockReturnValue(jest.fn()),
|
||||
useSelector: jest.fn(),
|
||||
|
||||
@@ -24,7 +24,9 @@ import { ButtonActions } from "generic-view/models"
|
||||
import { useViewFormContext } from "generic-view/utils"
|
||||
import { useSelectFilesButtonAction } from "./use-select-files-button-action"
|
||||
import { useUploadFilesButtonAction } from "./use-upload-files-button-action"
|
||||
import { useExportFilesButtonAction } from "./use-export-files-button-action"
|
||||
import { modalTransitionDuration } from "generic-view/theme"
|
||||
import { useSelectDirectoryButtonAction } from "./use-select-directory-button-action"
|
||||
|
||||
export const useButtonAction = (viewKey: string) => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
@@ -33,7 +35,9 @@ export const useButtonAction = (viewKey: string) => {
|
||||
const getFormContext = useViewFormContext()
|
||||
const activeDeviceId = useSelector(selectActiveApiDeviceId)!
|
||||
const selectFiles = useSelectFilesButtonAction()
|
||||
const selectDirectory = useSelectDirectoryButtonAction()
|
||||
const uploadFiles = useUploadFilesButtonAction()
|
||||
const exportFiles = useExportFilesButtonAction()
|
||||
|
||||
return (actions: ButtonActions) =>
|
||||
runActions(actions)(
|
||||
@@ -46,7 +50,9 @@ export const useButtonAction = (viewKey: string) => {
|
||||
},
|
||||
{
|
||||
selectFiles,
|
||||
selectDirectory,
|
||||
uploadFiles,
|
||||
exportFiles,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -61,7 +67,9 @@ export interface RunActionsProviders {
|
||||
|
||||
interface CustomActions {
|
||||
selectFiles: ReturnType<typeof useSelectFilesButtonAction>
|
||||
selectDirectory: ReturnType<typeof useSelectDirectoryButtonAction>
|
||||
uploadFiles: ReturnType<typeof useUploadFilesButtonAction>
|
||||
exportFiles: ReturnType<typeof useExportFilesButtonAction>
|
||||
}
|
||||
|
||||
const waitForModalTransition = () => {
|
||||
@@ -147,6 +155,15 @@ const runActions = (actions?: ButtonActions) => {
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "select-directory":
|
||||
{
|
||||
const selected = await customActions.selectDirectory(action)
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
case "upload-files":
|
||||
await customActions.uploadFiles(action, {
|
||||
onValidationFailure: async () => {
|
||||
@@ -169,6 +186,28 @@ const runActions = (actions?: ButtonActions) => {
|
||||
},
|
||||
})
|
||||
break
|
||||
case "export-files":
|
||||
await customActions.exportFiles(action, {
|
||||
onValidationFailure: async () => {
|
||||
await runActions(action.preActions?.validationFailure)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await runActions(action.postActions?.success)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
onFailure: async () => {
|
||||
await runActions(action.postActions?.failure)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
})
|
||||
break
|
||||
case "start-app-installation":
|
||||
await dispatch(
|
||||
startAppInstallationAction({
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { useCallback } from "react"
|
||||
import { useDispatch, useStore } from "react-redux"
|
||||
import { useViewFormContext } from "generic-view/utils"
|
||||
import { ReduxRootState, Dispatch } from "Core/__deprecated__/renderer/store"
|
||||
import { FilesTransferExportFilesAction } from "generic-view/models"
|
||||
import {
|
||||
sendFilesTransferAnalysis,
|
||||
clearFileTransferErrors,
|
||||
selectFilesSendingFailed,
|
||||
selectEntitiesByIds,
|
||||
FileWithPath,
|
||||
sendFiles,
|
||||
sendFilesClear,
|
||||
} from "generic-view/store"
|
||||
import { activeDeviceIdSelector } from "active-device-registry/feature"
|
||||
import { isMtpPathInternal, sliceMtpPaths } from "./file-transfer-paths-helper"
|
||||
import { SendFilesAction } from "../../../../../store/src/lib/file-transfer/files-transfer.type"
|
||||
|
||||
export const useExportFilesButtonAction = () => {
|
||||
const store = useStore<ReduxRootState>()
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const getFormContext = useViewFormContext()
|
||||
const deviceId = activeDeviceIdSelector(store.getState())
|
||||
|
||||
return useCallback(
|
||||
async (
|
||||
action: FilesTransferExportFilesAction,
|
||||
callbacks: {
|
||||
onSuccess: () => Promise<void>
|
||||
onFailure: () => Promise<void>
|
||||
onValidationFailure: () => Promise<void>
|
||||
}
|
||||
) => {
|
||||
const form = getFormContext(action.formOptions.formKey)
|
||||
|
||||
if (action.entitiesType === undefined) return
|
||||
if (deviceId === undefined) return
|
||||
const selectedItems: string[] = getFormContext(
|
||||
action.sourceFormKey
|
||||
).getValues(action.selectedItemsFieldName)
|
||||
|
||||
const destinationPath: string = form.getValues(
|
||||
action.formOptions.selectedDirectoryFieldName
|
||||
)
|
||||
|
||||
const entities = selectEntitiesByIds(store.getState(), {
|
||||
deviceId,
|
||||
entitiesType: action.entitiesType,
|
||||
ids: selectedItems,
|
||||
})
|
||||
const exportFilesData: FileWithPath[] = entities.map((e) => ({
|
||||
id: String(e.id),
|
||||
path: sliceMtpPaths(e.filePath as string, e.isInternal as boolean),
|
||||
name: String(e.fileName),
|
||||
size: Number(e.fileSize),
|
||||
groupId: action.actionId,
|
||||
}))
|
||||
|
||||
//TODO - available space validation
|
||||
|
||||
const sourcePath = entities[0].filePath as string
|
||||
|
||||
const response = (await dispatch(
|
||||
sendFiles({
|
||||
files: exportFilesData,
|
||||
destinationPath,
|
||||
actionId: action.actionId,
|
||||
entitiesType: action.entitiesType,
|
||||
isMtpPathInternal: isMtpPathInternal(sourcePath),
|
||||
actionType: SendFilesAction.ActionExport,
|
||||
})
|
||||
)) as Awaited<ReturnType<ReturnType<typeof sendFiles>>>
|
||||
|
||||
const failedFiles = selectFilesSendingFailed(store.getState(), {
|
||||
groupId: action.actionId,
|
||||
})
|
||||
|
||||
dispatch(sendFilesTransferAnalysis({ groupId: action.actionId }))
|
||||
|
||||
if (
|
||||
response.meta.requestStatus === "rejected" ||
|
||||
failedFiles.length > 0
|
||||
) {
|
||||
await callbacks.onFailure()
|
||||
} else {
|
||||
await callbacks.onSuccess()
|
||||
dispatch(sendFilesClear({ groupId: action.actionId }))
|
||||
dispatch(clearFileTransferErrors({ actionId: action.actionId }))
|
||||
}
|
||||
},
|
||||
[dispatch, getFormContext, store, deviceId]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { useCallback } from "react"
|
||||
import { chooseDirectoryRequest } from "system-utils/feature"
|
||||
import { NativeActionSelectDirectory } from "generic-view/models"
|
||||
import { useViewFormContext } from "generic-view/utils"
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
|
||||
export const useSelectDirectoryButtonAction = () => {
|
||||
const getFormContext = useViewFormContext()
|
||||
return useCallback(
|
||||
async (action: NativeActionSelectDirectory) => {
|
||||
const downloadsPath = (await ipcRenderer.callMain(
|
||||
"get-downloads-path"
|
||||
)) as string
|
||||
|
||||
const selectorResponse = await chooseDirectoryRequest({
|
||||
title: action.title || "Choose a directory to export",
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
defaultPath: downloadsPath,
|
||||
})
|
||||
|
||||
if (!selectorResponse.ok || !selectorResponse.data.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
getFormContext(action.formOptions.formKey).setValue(
|
||||
action.formOptions.selectedDirectoryFieldName,
|
||||
selectorResponse.data
|
||||
)
|
||||
|
||||
return true
|
||||
},
|
||||
[getFormContext]
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import { Dispatch, ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { useViewFormContext } from "generic-view/utils"
|
||||
import { activeDeviceIdSelector } from "active-device-registry/feature"
|
||||
import { validateSelectedFiles } from "../../shared/validate-selected-files"
|
||||
import { isMtpPathInternal, sliceMtpPaths } from "./file-transfer-paths-helper"
|
||||
import { SendFilesAction } from "../../../../../store/src/lib/file-transfer/files-transfer.type"
|
||||
|
||||
export const useUploadFilesButtonAction = () => {
|
||||
const store = useStore<ReduxRootState>()
|
||||
@@ -85,12 +87,18 @@ export const useUploadFilesButtonAction = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const isDestinationInternal = isMtpPathInternal(action.destinationPath)
|
||||
const response = (await dispatch(
|
||||
sendFiles({
|
||||
files,
|
||||
actionId: action.actionId,
|
||||
destinationPath: action.destinationPath,
|
||||
destinationPath: sliceMtpPaths(
|
||||
action.destinationPath,
|
||||
isDestinationInternal
|
||||
),
|
||||
entitiesType: action.entitiesType,
|
||||
isMtpPathInternal: isDestinationInternal,
|
||||
actionType: SendFilesAction.ActionUpload,
|
||||
})
|
||||
)) as Awaited<ReturnType<ReturnType<typeof sendFiles>>>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { ComponentGenerator, IconType } from "generic-view/utils"
|
||||
|
||||
export const generateAppInstallaion: ComponentGenerator<{
|
||||
export const generateAppInstallation: ComponentGenerator<{
|
||||
id: string
|
||||
entityType: string
|
||||
}> = (key, { id, entityType }) => {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ComponentGenerator, IconType } from "generic-view/utils"
|
||||
import { McFileManagerConfig } from "generic-view/models"
|
||||
import { SendFilesAction } from "../../../../../store/src/lib/file-transfer/files-transfer.type"
|
||||
|
||||
export const generateFilesExportProcessButtonKey = (key: string) =>
|
||||
`${key}filesExportButton`
|
||||
|
||||
export const generateFilesExportButtonModalKey = (
|
||||
key: string,
|
||||
modalName: string
|
||||
) => {
|
||||
return generateFilesExportProcessButtonKey(key) + "Modal" + modalName
|
||||
}
|
||||
|
||||
export const generateFileExportProcessButton: ComponentGenerator<
|
||||
Pick<
|
||||
McFileManagerConfig["categories"][number],
|
||||
"entityType" | "supportedFileTypes" | "directoryPath" | "label"
|
||||
> & {
|
||||
storagePath: string
|
||||
}
|
||||
> = (key, { directoryPath, entityType }) => {
|
||||
const exportActionId = entityType + "Export"
|
||||
|
||||
return {
|
||||
[generateFilesExportProcessButtonKey(key)]: {
|
||||
component: "button-text",
|
||||
config: {
|
||||
text: entityType === "applicationFiles" ? "Export APK" : "Export",
|
||||
icon: IconType.Export,
|
||||
actions: [
|
||||
{
|
||||
type: "select-directory",
|
||||
title: "Choose a directory to export",
|
||||
formOptions: {
|
||||
formKey: `${key}fileExportForm`,
|
||||
selectedDirectoryFieldName: "exportPath",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "open-modal",
|
||||
modalKey: generateFilesExportButtonModalKey(key, "Progress"),
|
||||
},
|
||||
{
|
||||
type: "export-files",
|
||||
formOptions: {
|
||||
formKey: `${key}fileExportForm`,
|
||||
selectedDirectoryFieldName: "exportPath",
|
||||
},
|
||||
destinationPath: "exportPath",
|
||||
sourceFormKey: `${key}fileListForm`,
|
||||
selectedItemsFieldName: "selectedItems",
|
||||
entitiesType: entityType,
|
||||
actionId: exportActionId,
|
||||
preActions: {
|
||||
validationFailure: [
|
||||
{
|
||||
type: "replace-modal",
|
||||
modalKey: generateFilesExportButtonModalKey(
|
||||
key,
|
||||
"ValidationFailure"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
postActions: {
|
||||
success: [
|
||||
{
|
||||
type: "close-modal",
|
||||
modalKey: generateFilesExportButtonModalKey(key, "Progress"),
|
||||
},
|
||||
{
|
||||
type: "open-toast",
|
||||
toastKey: `${key}FilesExportedToast`,
|
||||
},
|
||||
{
|
||||
type: "form-set-field",
|
||||
formKey: `${key}fileListForm`,
|
||||
key: "selectedItems",
|
||||
value: [],
|
||||
},
|
||||
],
|
||||
failure: [
|
||||
{
|
||||
type: "form-set-field",
|
||||
formKey: `${key}fileListForm`,
|
||||
key: "selectedItems",
|
||||
value: [],
|
||||
},
|
||||
{
|
||||
type: "replace-modal",
|
||||
modalKey: generateFilesExportButtonModalKey(key, "Finished"),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
modifiers: ["uppercase"],
|
||||
},
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
componentField: "data.fields.selectedItems",
|
||||
modifier: "length",
|
||||
},
|
||||
],
|
||||
},
|
||||
childrenKeys: [`${key}fileExportForm`],
|
||||
},
|
||||
[generateFilesExportButtonModalKey(key, "Progress")]: {
|
||||
component: "modal",
|
||||
config: {
|
||||
size: "small",
|
||||
},
|
||||
childrenKeys: [generateFilesExportButtonModalKey(key, "ProgressContent")],
|
||||
},
|
||||
|
||||
[generateFilesExportButtonModalKey(key, "ProgressContent")]: {
|
||||
component: "mc-files-manager-transfer-progress",
|
||||
config: {
|
||||
directoryPath,
|
||||
entitiesType: entityType,
|
||||
transferActionId: exportActionId,
|
||||
actionType: SendFilesAction.ActionExport,
|
||||
},
|
||||
childrenKeys: [generateFilesExportButtonModalKey(key, "Finished")],
|
||||
},
|
||||
[generateFilesExportButtonModalKey(key, "Finished")]: {
|
||||
component: "modal",
|
||||
config: {
|
||||
size: "small",
|
||||
maxHeight: "538px",
|
||||
},
|
||||
childrenKeys: [generateFilesExportButtonModalKey(key, "FinishedContent")],
|
||||
},
|
||||
[generateFilesExportButtonModalKey(key, "FinishedContent")]: {
|
||||
component: "mc-files-manager-transfer-finished",
|
||||
config: {
|
||||
modalKey: generateFilesExportButtonModalKey(key, "Finished"),
|
||||
transferActionId: exportActionId,
|
||||
},
|
||||
},
|
||||
|
||||
[`${key}FilesExportedToast`]: {
|
||||
component: "toast",
|
||||
childrenKeys: [
|
||||
`${key}FilesExportedToastIcon`,
|
||||
`${key}FilesExportedToastText`,
|
||||
],
|
||||
},
|
||||
|
||||
[`${key}FilesExportedToastIcon`]: {
|
||||
component: "icon",
|
||||
config: {
|
||||
type: IconType.Success,
|
||||
},
|
||||
},
|
||||
|
||||
[`${key}FilesExportedToastText`]: {
|
||||
component: "typography.p1",
|
||||
config: {
|
||||
messageTemplate:
|
||||
"{exportedFiles} {exportedFiles, plural, one {file} other {files}} exported",
|
||||
},
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
componentField: "data.fields.exportedFiles",
|
||||
modifier: "length",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}fileExportForm`]: {
|
||||
component: "form",
|
||||
config: {
|
||||
formOptions: {
|
||||
defaultValues: {
|
||||
exportPath: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
generateFileUploadProcessButtonKey,
|
||||
} from "./file-upload-button"
|
||||
import { fileCounterDataProvider } from "./file-counter-data-provider"
|
||||
import { generateAppInstallaion } from "./app-installation"
|
||||
import { generateAppInstallation as generateAppInstallation } from "./app-installation"
|
||||
import {
|
||||
generateFileExportProcessButton,
|
||||
generateFilesExportProcessButtonKey as generateFileExportProcessButtonKey,
|
||||
} from "./file-export-button"
|
||||
|
||||
const generateFileList: ComponentGenerator<
|
||||
McFileManagerConfig["categories"][number] & {
|
||||
@@ -210,6 +214,7 @@ const generateFileList: ComponentGenerator<
|
||||
childrenKeys: [
|
||||
`${key}${id}selectAllCheckbox`,
|
||||
`${key}${id}selectedItemsCounter`,
|
||||
generateFileExportProcessButtonKey(`${key}${id}`),
|
||||
`${key}${id}deleteButton`,
|
||||
],
|
||||
layout: {
|
||||
@@ -217,7 +222,7 @@ const generateFileList: ComponentGenerator<
|
||||
padding: "8px 24px 8px 12px",
|
||||
gridLayout: {
|
||||
rows: ["auto"],
|
||||
columns: ["auto", "1fr", "auto"],
|
||||
columns: ["auto", "1fr", "auto", "auto"],
|
||||
alignItems: "center",
|
||||
columnGap: "14px",
|
||||
},
|
||||
@@ -256,6 +261,13 @@ const generateFileList: ComponentGenerator<
|
||||
],
|
||||
},
|
||||
},
|
||||
...generateFileExportProcessButton(`${key}${id}`, {
|
||||
directoryPath,
|
||||
entityType,
|
||||
storagePath,
|
||||
supportedFileTypes,
|
||||
label,
|
||||
}),
|
||||
[`${key}${id}deleteButton`]: {
|
||||
component: "button-text",
|
||||
config: {
|
||||
@@ -541,7 +553,7 @@ const generateFileList: ComponentGenerator<
|
||||
],
|
||||
},
|
||||
},
|
||||
...generateAppInstallaion(key, {
|
||||
...generateAppInstallation(key, {
|
||||
id,
|
||||
entityType,
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { ComponentGenerator, IconType } from "generic-view/utils"
|
||||
import { McFileManagerConfig } from "generic-view/models"
|
||||
import { SendFilesAction } from "../../../../../store/src/lib/file-transfer/files-transfer.type"
|
||||
|
||||
export const generateFileUploadProcessButtonKey = (key: string) => {
|
||||
return `${key}filesUploadButton`
|
||||
@@ -156,12 +157,13 @@ export const generateFileUploadProcessButton: ComponentGenerator<
|
||||
childrenKeys: [generateFileUploadButtonModalKey(key, "ProgressContent")],
|
||||
},
|
||||
[generateFileUploadButtonModalKey(key, "ProgressContent")]: {
|
||||
component: "mc-files-manager-upload-progress",
|
||||
component: "mc-files-manager-transfer-progress",
|
||||
config: {
|
||||
storagePath,
|
||||
directoryPath,
|
||||
entitiesType: entityType,
|
||||
uploadActionId,
|
||||
transferActionId: uploadActionId,
|
||||
actionType: SendFilesAction.ActionUpload,
|
||||
},
|
||||
},
|
||||
[generateFileUploadButtonModalKey(key, "Finished")]: {
|
||||
@@ -173,10 +175,10 @@ export const generateFileUploadProcessButton: ComponentGenerator<
|
||||
childrenKeys: [generateFileUploadButtonModalKey(key, "FinishedContent")],
|
||||
},
|
||||
[generateFileUploadButtonModalKey(key, "FinishedContent")]: {
|
||||
component: "mc-files-manager-upload-finished",
|
||||
component: "mc-files-manager-transfer-finished",
|
||||
config: {
|
||||
modalKey: generateFileUploadButtonModalKey(key, "Finished"),
|
||||
uploadActionId,
|
||||
transferActionId: uploadActionId,
|
||||
},
|
||||
},
|
||||
[generateFileUploadButtonModalKey(key, "ValidationFailure")]: {
|
||||
@@ -189,7 +191,7 @@ export const generateFileUploadProcessButton: ComponentGenerator<
|
||||
],
|
||||
},
|
||||
[generateFileUploadButtonModalKey(key, "ValidationFailureContent")]: {
|
||||
component: "mc-files-manager-upload-validation-error",
|
||||
component: "mc-files-manager-transfer-validation-error",
|
||||
config: {
|
||||
modalKey: generateFileUploadButtonModalKey(key, "ValidationFailure"),
|
||||
uploadActionId,
|
||||
|
||||
@@ -62,6 +62,7 @@ import Exclamation from "./svg/exclamation.svg"
|
||||
import Namaste from "./svg/namaste.svg"
|
||||
import Delete from "./svg/delete.svg"
|
||||
import DropdownArrow from "./svg/dropdown-arrow.svg"
|
||||
import Export from "./svg/export.svg"
|
||||
|
||||
import { IconType } from "generic-view/utils"
|
||||
|
||||
@@ -122,6 +123,7 @@ const typeToIcon: Record<IconType, typeof BatteryHigh> = {
|
||||
[IconType.Namaste]: Namaste,
|
||||
[IconType.Delete]: Delete,
|
||||
[IconType.DropdownArrow]: DropdownArrow,
|
||||
[IconType.Export]: Export,
|
||||
}
|
||||
|
||||
export const getIcon = (
|
||||
|
||||
9
libs/generic-view/ui/src/lib/icon/svg/export.svg
Normal file
9
libs/generic-view/ui/src/lib/icon/svg/export.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_43910_2214" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="20" height="20">
|
||||
<path d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z" fill="white" stroke="black" stroke-width="2"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_43910_2214)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 14C18.2761 14 18.5 14.2239 18.5 14.5V17.5C18.5 18.3284 17.8284 19 17 19H7C6.17157 19 5.5 18.3284 5.5 17.5V14.75C5.5 14.4739 5.72386 14.25 6 14.25C6.27614 14.25 6.5 14.4739 6.5 14.75V17.5C6.5 17.7761 6.72386 18 7 18H17C17.2761 18 17.5 17.7761 17.5 17.5V14.5C17.5 14.2239 17.7239 14 18 14Z" fill="#3B3F42"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1434 14.5C11.8365 14.5 11.5878 14.2589 11.5878 13.9615L11.5878 7.80979L9.44211 9.7329C9.22173 9.93982 8.87002 9.93441 8.65653 9.72081C8.44305 9.50722 8.44863 9.16633 8.669 8.95941L11.7568 6.15172C11.8627 6.05232 12.005 5.99777 12.1523 6.00007C12.2997 6.00237 12.4401 6.06135 12.5426 6.164L15.3437 8.9717C15.557 9.1854 15.5513 9.52629 15.3308 9.73309C15.1103 9.9399 14.7586 9.93431 14.5452 9.72061L12.6989 7.86917L12.6989 13.9615C12.6989 14.2589 12.4502 14.5 12.1434 14.5Z" fill="#3B3F42"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -7,8 +7,8 @@ import React, { useCallback, useLayoutEffect, useMemo } from "react"
|
||||
import { APIFC, compareValues, IconType } from "generic-view/utils"
|
||||
import {
|
||||
ButtonAction,
|
||||
McFilesManagerUploadFinishedConfig,
|
||||
McFilesManagerUploadFinishedData,
|
||||
McFilesManagerTransferFinishedConfig,
|
||||
McFilesManagerTransferFinishedData,
|
||||
} from "generic-view/models"
|
||||
import { Modal } from "../../interactive/modal/modal"
|
||||
import { intl } from "Core/__deprecated__/renderer/utils/intl"
|
||||
@@ -83,21 +83,22 @@ const messages = defineMessages({
|
||||
})
|
||||
|
||||
export const FilesManagerUploadFinished: APIFC<
|
||||
McFilesManagerUploadFinishedData,
|
||||
McFilesManagerUploadFinishedConfig
|
||||
McFilesManagerTransferFinishedData,
|
||||
McFilesManagerTransferFinishedConfig
|
||||
> = ({ config, data }) => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const selectorsConfig = { groupId: config.uploadActionId }
|
||||
|
||||
const selectorsConfig = { groupId: config.transferActionId }
|
||||
const filesCount = useSelector((state: ReduxRootState) => {
|
||||
return selectFilesSendingCount(state, selectorsConfig)
|
||||
})
|
||||
|
||||
const succeededFiles = useSelector((state: ReduxRootState) => {
|
||||
return selectFilesSendingSucceeded(state, selectorsConfig)
|
||||
})
|
||||
const failedFiles = useSelector((state: ReduxRootState) => {
|
||||
return selectFilesSendingFailed(state, selectorsConfig)
|
||||
})
|
||||
|
||||
const allFilesFailed = failedFiles.length === filesCount
|
||||
const errorTypes = uniq(failedFiles.map((file) => file.error.message))
|
||||
|
||||
@@ -127,8 +128,10 @@ export const FilesManagerUploadFinished: APIFC<
|
||||
type: "custom",
|
||||
callback: () => {
|
||||
setTimeout(() => {
|
||||
dispatch(sendFilesClear({ groupId: config.uploadActionId }))
|
||||
dispatch(clearFileTransferErrors({ actionId: config.uploadActionId }))
|
||||
dispatch(sendFilesClear({ groupId: config.transferActionId }))
|
||||
dispatch(
|
||||
clearFileTransferErrors({ actionId: config.transferActionId })
|
||||
)
|
||||
}, modalTransitionDuration)
|
||||
},
|
||||
},
|
||||
@@ -305,15 +308,22 @@ export const FilesManagerUploadFinished: APIFC<
|
||||
}, [errorTypes])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (succeededFiles.length !== 0 && failedFiles.length === 0) {
|
||||
const processedCount = succeededFiles.length + failedFiles.length
|
||||
const allProcessed = processedCount === filesCount
|
||||
if (
|
||||
allProcessed &&
|
||||
succeededFiles.length !== 0 &&
|
||||
failedFiles.length === 0
|
||||
) {
|
||||
dispatch(closeModal({ key: config.modalKey }))
|
||||
}
|
||||
}, [
|
||||
config.modalKey,
|
||||
config.uploadActionId,
|
||||
config.transferActionId,
|
||||
dispatch,
|
||||
failedFiles.length,
|
||||
succeededFiles.length,
|
||||
filesCount,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDispatch, useSelector } from "react-redux"
|
||||
import { APIFC, IconType } from "generic-view/utils"
|
||||
import {
|
||||
ButtonAction,
|
||||
McFilesManagerUploadProgressConfig,
|
||||
McFilesManagerTransferProgressConfig,
|
||||
} from "generic-view/models"
|
||||
import {
|
||||
selectFilesSendingCount,
|
||||
@@ -24,12 +24,18 @@ import { ProgressBar } from "../../interactive/progress-bar/progress-bar"
|
||||
import { Modal } from "../../interactive/modal/modal"
|
||||
import { ButtonSecondary } from "../../buttons/button-secondary"
|
||||
import { FilesManagerUploadProgressWarning } from "./files-manager-upload-progress-warning"
|
||||
import { FilesTransferMode } from "../../../../../store/src/lib/file-transfer/files-transfer-mode.type"
|
||||
import {
|
||||
FilesTransferMode,
|
||||
SendFilesAction,
|
||||
} from "../../../../../store/src/lib/file-transfer/files-transfer.type"
|
||||
|
||||
const messages = defineMessages({
|
||||
progressModalTitle: {
|
||||
progressUploadModalTitle: {
|
||||
id: "module.genericViews.filesManager.upload.progress.modalTitle",
|
||||
},
|
||||
progressExportModalTitle: {
|
||||
id: "module.genericViews.filesManager.export.progress.modalTitle",
|
||||
},
|
||||
cancelButton: {
|
||||
id: "module.genericViews.filesManager.upload.progress.cancelButton",
|
||||
},
|
||||
@@ -37,10 +43,10 @@ const messages = defineMessages({
|
||||
|
||||
export const FilesManagerUploadProgress: APIFC<
|
||||
undefined,
|
||||
McFilesManagerUploadProgressConfig
|
||||
McFilesManagerTransferProgressConfig
|
||||
> = ({ config }) => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const selectorsConfig = { groupId: config.uploadActionId }
|
||||
const selectorsConfig = { groupId: config.transferActionId }
|
||||
|
||||
const filesCount = useSelector((state: ReduxRootState) => {
|
||||
return selectFilesSendingCount(state, selectorsConfig)
|
||||
@@ -56,7 +62,7 @@ export const FilesManagerUploadProgress: APIFC<
|
||||
const abortAction: ButtonAction = {
|
||||
type: "custom",
|
||||
callback: () => {
|
||||
dispatch(sendFilesAbort(config.uploadActionId))
|
||||
dispatch(sendFilesAbort(config.transferActionId))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,9 +70,14 @@ export const FilesManagerUploadProgress: APIFC<
|
||||
<>
|
||||
<Modal.TitleIcon config={{ type: IconType.SpinnerDark }} />
|
||||
<Modal.Title>
|
||||
{intl.formatMessage(messages.progressModalTitle, {
|
||||
filesCount,
|
||||
})}
|
||||
{intl.formatMessage(
|
||||
config.actionType === SendFilesAction.ActionExport
|
||||
? messages.progressExportModalTitle
|
||||
: messages.progressUploadModalTitle,
|
||||
{
|
||||
filesCount,
|
||||
}
|
||||
)}
|
||||
</Modal.Title>
|
||||
{filesTransferMode === FilesTransferMode.SerialPort && (
|
||||
<FilesManagerUploadProgressWarning />
|
||||
|
||||
@@ -9,8 +9,8 @@ import { defineMessages } from "react-intl"
|
||||
import { APIFC, IconType } from "generic-view/utils"
|
||||
import {
|
||||
ButtonAction,
|
||||
McFilesManagerUploadValidationErrorConfig,
|
||||
McFilesManagerUploadValidationErrorData,
|
||||
McFilesManagerTransferValidationErrorConfig,
|
||||
McFilesManagerTransferValidationErrorData,
|
||||
} from "generic-view/models"
|
||||
import {
|
||||
clearFileTransferErrors,
|
||||
@@ -41,8 +41,8 @@ const messages = defineMessages({
|
||||
})
|
||||
|
||||
export const FilesManagerUploadValidationError: APIFC<
|
||||
McFilesManagerUploadValidationErrorData,
|
||||
McFilesManagerUploadValidationErrorConfig
|
||||
McFilesManagerTransferValidationErrorData,
|
||||
McFilesManagerTransferValidationErrorConfig
|
||||
> = ({ config, data }) => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const validationFailureType = useSelector((state: ReduxRootState) =>
|
||||
|
||||
@@ -14,8 +14,8 @@ import { DataMigration } from "./data-migration/data-migration"
|
||||
import { IncomingFeatureInfo } from "./incoming-feature-info"
|
||||
import { SelectionManager } from "./selection-manager"
|
||||
import { McContactsSearchResult } from "./contacts/mc-contacts-search-result"
|
||||
import { FilesManagerUploadProgress } from "./files-manager-upload/files-manager-upload-progress"
|
||||
import { FilesManagerUploadFinished } from "./files-manager-upload/files-manager-upload-finished"
|
||||
import { FilesManagerUploadProgress as FilesManagerTransferProgress } from "./files-manager-upload/files-manager-upload-progress"
|
||||
import { FilesManagerUploadFinished as FilesManagerTransferFinished } from "./files-manager-upload/files-manager-upload-finished"
|
||||
import { FilesManagerUploadValidationError } from "./files-manager-upload/files-manager-upload-validation-error"
|
||||
import { EntitiesDeleteError } from "./entities/entities-delete-error"
|
||||
import { AppInstallationProgress } from "./app-installation/app-installation-progress"
|
||||
@@ -32,9 +32,9 @@ import {
|
||||
lastBackupDate,
|
||||
mcContactsSearchResults,
|
||||
mcDataMigration,
|
||||
mcFilesManagerUploadFinished,
|
||||
mcFilesManagerUploadProgress,
|
||||
mcFilesManagerUploadValidationError,
|
||||
mcFilesManagerTransferFinished,
|
||||
mcFilesManagerTransferProgress,
|
||||
mcFilesManagerTransferValidationError,
|
||||
overviewOsVersion,
|
||||
selectionManager,
|
||||
mcAppInstallationProgress,
|
||||
@@ -55,9 +55,10 @@ export const predefinedComponents = {
|
||||
[incomingFeatureInfo.key]: IncomingFeatureInfo,
|
||||
[selectionManager.key]: SelectionManager,
|
||||
[mcContactsSearchResults.key]: McContactsSearchResult,
|
||||
[mcFilesManagerUploadProgress.key]: FilesManagerUploadProgress,
|
||||
[mcFilesManagerUploadFinished.key]: FilesManagerUploadFinished,
|
||||
[mcFilesManagerUploadValidationError.key]: FilesManagerUploadValidationError,
|
||||
[mcFilesManagerTransferProgress.key]: FilesManagerTransferProgress,
|
||||
[mcFilesManagerTransferFinished.key]: FilesManagerTransferFinished,
|
||||
[mcFilesManagerTransferValidationError.key]:
|
||||
FilesManagerUploadValidationError,
|
||||
[mcAppInstallationProgress.key]: AppInstallationProgress,
|
||||
[mcAppInstallationError.key]: AppInstallationError,
|
||||
[mcAppInstallationSuccess.key]: AppInstallationSuccess,
|
||||
|
||||
@@ -62,11 +62,14 @@ export async function isStorageSpaceSufficientForUpload(
|
||||
}
|
||||
> {
|
||||
const totalFileSize = await getTotalFileSizeAsync(filePaths)
|
||||
return CalculateAndFormatAvailableSpace(availableSpace, totalFileSize)
|
||||
}
|
||||
|
||||
const { isSufficient, difference } = compareValues(
|
||||
availableSpace,
|
||||
totalFileSize
|
||||
)
|
||||
export const CalculateAndFormatAvailableSpace = (
|
||||
availableSpace: number,
|
||||
totalSpace: number
|
||||
) => {
|
||||
const { isSufficient, difference } = compareValues(availableSpace, totalSpace)
|
||||
|
||||
const formattedDifference = formatBytes(Math.abs(difference), {
|
||||
minUnit: "B",
|
||||
|
||||
@@ -60,4 +60,5 @@ export enum IconType {
|
||||
Namaste = "namaste",
|
||||
Delete = "delete",
|
||||
DropdownArrow = "dropdown-arrow",
|
||||
Export = "export",
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
export * from "./lib/system-utils.module"
|
||||
export * from "./lib/directory/directory.requests"
|
||||
export * from "./lib/file-dialog/open-file.request"
|
||||
export * from "./lib/file-dialog/choose-directory.request"
|
||||
export * from "./lib/file-stats/get-file-stats.request"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron-better-ipc"
|
||||
import { OpenDialogOptions } from "electron"
|
||||
import { FileDialogToMainEvents } from "system-utils/models"
|
||||
import { ResultObject } from "Core/core/builder"
|
||||
|
||||
export const chooseDirectoryRequest = (
|
||||
options: OpenDialogOptions
|
||||
): Promise<ResultObject<string>> => {
|
||||
return ipcRenderer.callMain(FileDialogToMainEvents.ChooseDirectory, {
|
||||
options,
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,11 @@ const defaultOptions: OpenDialogOptions = {
|
||||
properties: [],
|
||||
}
|
||||
|
||||
const defaultDirectoryOptions: OpenDialogOptions = {
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
title: intl.formatMessage({ id: "component.dialog.openDirectory.title" }),
|
||||
}
|
||||
|
||||
export class FileDialog {
|
||||
private lastSelectedPath: string | undefined
|
||||
|
||||
@@ -51,6 +56,26 @@ export class FileDialog {
|
||||
}
|
||||
}
|
||||
|
||||
@IpcEvent(FileDialogToMainEvents.ChooseDirectory)
|
||||
public async chooseDirectory({
|
||||
options,
|
||||
}: {
|
||||
options?: OpenDialogOptions
|
||||
}): Promise<ResultObject<string>> {
|
||||
if (this.mockServiceEnabled) {
|
||||
const mockDirectoryPaths = this.mockFileDialog.getMockDirectoryPath()
|
||||
return Result.success(mockDirectoryPaths)
|
||||
} else {
|
||||
callRenderer(FileDialogToRendererEvents.FileDialogOpened)
|
||||
|
||||
const result = await this.performChooseDirectory(options)
|
||||
|
||||
callRenderer(FileDialogToRendererEvents.FileDialogClosed)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public async performOpenFile(
|
||||
options: OpenDialogOptions = defaultOptions
|
||||
): Promise<ResultObject<string[]>> {
|
||||
@@ -82,4 +107,38 @@ export class FileDialog {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public async performChooseDirectory(
|
||||
options: OpenDialogOptions = defaultDirectoryOptions
|
||||
): Promise<ResultObject<string>> {
|
||||
try {
|
||||
const openDialogOptions: OpenDialogOptions = {
|
||||
...defaultDirectoryOptions,
|
||||
...options,
|
||||
defaultPath: options.defaultPath || this.lastSelectedPath,
|
||||
}
|
||||
|
||||
const result = await dialog.showOpenDialog(
|
||||
BrowserWindow.getFocusedWindow() as BrowserWindow,
|
||||
openDialogOptions
|
||||
)
|
||||
|
||||
if (result.canceled) {
|
||||
return Result.failed(
|
||||
new AppError(FileDialogError.ChooseDirectory, "cancelled")
|
||||
)
|
||||
}
|
||||
|
||||
this.lastSelectedPath = result.filePaths[0]
|
||||
|
||||
return Result.success(result.filePaths[0])
|
||||
} catch (error) {
|
||||
return Result.failed(
|
||||
new AppError(
|
||||
FileDialogError.ChooseDirectory,
|
||||
error ? (error as Error).message : undefined
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
|
||||
export enum FileDialogError {
|
||||
OpenFile = "file-dialog/open-file-error",
|
||||
ChooseDirectory = "file-dialog/choose-directory-error",
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
export enum FileDialogToMainEvents {
|
||||
OpenFile = "file-dialog/open-file",
|
||||
ChooseDirectory = "file-dialog/choose-directory",
|
||||
}
|
||||
|
||||
export enum FileDialogToRendererEvents {
|
||||
|
||||
Reference in New Issue
Block a user