[CP-3385] export files from app list (#2557)

This commit is contained in:
slawomir-werner
2025-07-07 14:54:36 +02:00
committed by GitHub
parent 633ab83a95
commit cabd6e0142
42 changed files with 733 additions and 106 deletions

View File

@@ -553,3 +553,7 @@ ipcMain.answerRenderer(
ipcMain.answerRenderer(OutlookAuthActions.CloseWindow, () => {
outlookAuthWindow?.close()
})
ipcMain.answerRenderer("get-downloads-path", async () => {
return app.getPath("downloads")
})

View File

@@ -43,6 +43,7 @@ export interface MtpTransferFileData {
storageId: string
destinationPath: string
sourcePath: string
action?: string
}
export interface TransferFileResultData {

View File

@@ -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.",

View File

@@ -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)
}

View File

@@ -14,6 +14,10 @@ export class MockFileDialog {
return this.mockFilePaths
}
getMockDirectoryPath(): string {
return this.mockFilePaths[0]
}
clearMockFilePaths() {
this.mockFilePaths = []
}

View File

@@ -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,

View File

@@ -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({

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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">

View File

@@ -7,3 +7,8 @@ export enum FilesTransferMode {
SerialPort = "serial-port",
Mtp = "mtp",
}
export enum SendFilesAction {
ActionUpload = "upload",
ActionExport = "export",
}

View File

@@ -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,
}
}
)

View File

@@ -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 {

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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,
})
)

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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(),

View File

@@ -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({

View File

@@ -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]
)
}

View File

@@ -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]
)
}

View File

@@ -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>>>

View File

@@ -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 }) => {

View File

@@ -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: "",
},
},
},
},
}
}

View File

@@ -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,
}),

View File

@@ -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,

View File

@@ -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 = (

View 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

View File

@@ -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 (

View File

@@ -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 />

View File

@@ -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) =>

View File

@@ -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,

View File

@@ -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",

View File

@@ -60,4 +60,5 @@ export enum IconType {
Namaste = "namaste",
Delete = "delete",
DropdownArrow = "dropdown-arrow",
Export = "export",
}

View File

@@ -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"

View File

@@ -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,
})
}

View File

@@ -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
)
)
}
}
}

View File

@@ -5,4 +5,5 @@
export enum FileDialogError {
OpenFile = "file-dialog/open-file-error",
ChooseDirectory = "file-dialog/choose-directory-error",
}

View File

@@ -5,6 +5,7 @@
export enum FileDialogToMainEvents {
OpenFile = "file-dialog/open-file",
ChooseDirectory = "file-dialog/choose-directory",
}
export enum FileDialogToRendererEvents {