From 76e6e118d1619b40f0a1a90e88676af5e1e9f71f Mon Sep 17 00:00:00 2001 From: mkurczewski Date: Tue, 7 Jan 2025 17:25:19 +0100 Subject: [PATCH] [WiP] basic mechanism of adding files --- .../models/src/lib/common-validators.ts | 77 +++++++++++++--- .../models/src/lib/mc-file-manager-view.ts | 13 +++ libs/generic-view/store/src/index.ts | 1 + .../store/src/lib/action-names.ts | 1 + .../lib/entities/create-entity-data.action.ts | 11 ++- .../select-and-send-files.action.ts | 87 +++++++++++++++++++ .../buttons/button-base/use-button-action.ts | 26 ++++++ .../generated/mc-file-manager/file-list.ts | 65 +++++++++++++- 8 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 libs/generic-view/store/src/lib/file-transfer/select-and-send-files.action.ts diff --git a/libs/generic-view/models/src/lib/common-validators.ts b/libs/generic-view/models/src/lib/common-validators.ts index 2e30a02d22..62509e41d4 100644 --- a/libs/generic-view/models/src/lib/common-validators.ts +++ b/libs/generic-view/models/src/lib/common-validators.ts @@ -4,6 +4,7 @@ */ import { z } from "zod" +import { entityDataSchema } from "../../../../device/models/src/lib/entities/entity-data.validator" export const modalSizeValidator = z.enum(["small", "medium", "large"]) @@ -93,20 +94,73 @@ const entityPostActionsValidator = z ) .optional() -export const entityActionValidator = z.object({ - type: z.literal("entities-delete"), - entitiesType: z.string(), - ids: z.array(z.string()), - postActions: z - .object({ - success: entityPostActionsValidator, - failure: entityPostActionsValidator, - }) - .optional(), -}) +export const entityActionValidator = z.union([ + z.object({ + type: z.literal("entities-delete"), + entitiesType: z.string(), + ids: z.array(z.string()), + postActions: z + .object({ + success: entityPostActionsValidator, + failure: entityPostActionsValidator, + }) + .optional(), + }), + z.object({ + type: z.literal("entity-create"), + entitiesType: z.string(), + data: entityDataSchema, + postActions: z + .object({ + success: entityPostActionsValidator, + failure: entityPostActionsValidator, + }) + .optional(), + }), +]) export type EntityAction = z.infer +const filePostActionsValidator = z + .array( + z.union([ + modalActionValidator, + navigateActionValidator, + customActionValidator, + formActionValidator, + toastActionValidator, + entityActionValidator, + ]) + ) + .optional() + +export const fileActionValidator = z.union([ + z.object({ + type: z.literal("file-upload"), + storagePath: z.string(), + typesName: z.string(), + fileTypes: z.array(z.string()), + postActions: z + .object({ + success: filePostActionsValidator, + failure: filePostActionsValidator, + }) + .optional(), + }), + z.object({ + type: z.literal("file-download"), + storagePath: z.string(), + postActions: z + .object({ + success: filePostActionsValidator, + failure: filePostActionsValidator, + }) + .optional(), + }), +]) + +export type FileAction = z.infer + export const buttonActionsValidator = z.array( z.union([ modalActionValidator, @@ -115,6 +169,7 @@ export const buttonActionsValidator = z.array( formActionValidator, entityActionValidator, toastActionValidator, + fileActionValidator, ]) ) diff --git a/libs/generic-view/models/src/lib/mc-file-manager-view.ts b/libs/generic-view/models/src/lib/mc-file-manager-view.ts index ada01955ad..9df1865dc0 100644 --- a/libs/generic-view/models/src/lib/mc-file-manager-view.ts +++ b/libs/generic-view/models/src/lib/mc-file-manager-view.ts @@ -34,6 +34,19 @@ const configValidator = z.object({ entityTypes: z.array(z.string()).min(1), }) +// +// const configValidator = z.object({ +// entityTypes: z +// .array( +// z.object({ +// entityType: z.string(), +// storagePath: z.string(), +// supportedFileTypes: z.array(z.string()), +// }) +// ) +// .min(1), +// }) + export type McFileManagerView = z.infer export const mcFileManagerView = { diff --git a/libs/generic-view/store/src/index.ts b/libs/generic-view/store/src/index.ts index 91df371b0c..624d44c66b 100644 --- a/libs/generic-view/store/src/index.ts +++ b/libs/generic-view/store/src/index.ts @@ -19,6 +19,7 @@ export * from "./lib/file-transfer/reducer" export * from "./lib/file-transfer/actions" export * from "./lib/file-transfer/send-file.action" export * from "./lib/file-transfer/get-file.action" +export * from "./lib/file-transfer/select-and-send-files.action" export * from "./lib/backup/load-backup-metadata.action" export * from "./lib/backup/restore-backup.action" export * from "./lib/backup/backup.types" diff --git a/libs/generic-view/store/src/lib/action-names.ts b/libs/generic-view/store/src/lib/action-names.ts index 3e59c504d2..48c14e4f33 100644 --- a/libs/generic-view/store/src/lib/action-names.ts +++ b/libs/generic-view/store/src/lib/action-names.ts @@ -51,6 +51,7 @@ export enum ActionName { ChunkFileTransferGet = "generic-file-transfer/chunk-get", ClearFileTransferGetError = "generic-file-transfer/clear-get-errors", TransferDataToDevice = "generic-file-transfer/transfer-data-to-device", + SendSelectedFiles = "generic-file-transfer/send-selected-files", SetDataTransfer = "generic-data-transfer/set-data-transfer", SetDataTransferStatus = "generic-data-transfer/set-data-transfer-status", diff --git a/libs/generic-view/store/src/lib/entities/create-entity-data.action.ts b/libs/generic-view/store/src/lib/entities/create-entity-data.action.ts index fe760934e9..6ee4292a51 100644 --- a/libs/generic-view/store/src/lib/entities/create-entity-data.action.ts +++ b/libs/generic-view/store/src/lib/entities/create-entity-data.action.ts @@ -18,17 +18,23 @@ export const createEntityDataAction = createAsyncThunk< entitiesType: string data: EntityData deviceId: DeviceId + onSuccess?: () => Promise | void + onError?: () => Promise | void }, { state: ReduxRootState } >( ActionName.CreateEntityData, - async ({ entitiesType, data, deviceId }, { rejectWithValue, getState }) => { + async ( + { entitiesType, data, deviceId, onError, onSuccess }, + { rejectWithValue, getState } + ) => { const response = await createEntityDataRequest({ entitiesType, data, deviceId, }) if (!response.ok) { + await onError?.() return rejectWithValue(response.error) } @@ -39,6 +45,7 @@ export const createEntityDataAction = createAsyncThunk< logger.error( `Entities of type ${entitiesType} for device ${deviceId} not found` ) + await onError?.() return rejectWithValue(undefined) } if ( @@ -51,11 +58,13 @@ export const createEntityDataAction = createAsyncThunk< response.data.data[idFieldKey] as string } already exists` ) + await onError?.() return rejectWithValue(undefined) } const computedFields = genericEntities[deviceId]?.[entitiesType]?.config.computedFields || {} + await onSuccess?.() return enhanceEntity(response.data.data, { computedFields }) } ) diff --git a/libs/generic-view/store/src/lib/file-transfer/select-and-send-files.action.ts b/libs/generic-view/store/src/lib/file-transfer/select-and-send-files.action.ts new file mode 100644 index 0000000000..1cb752fc3b --- /dev/null +++ b/libs/generic-view/store/src/lib/file-transfer/select-and-send-files.action.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createAsyncThunk } from "@reduxjs/toolkit" +import { ActionName } from "../action-names" +import { ReduxRootState } from "Core/__deprecated__/renderer/store" +import { openFileRequest } from "system-utils/feature" +import { sendFile } from "./send-file.action" +import { selectActiveApiDeviceId } from "../selectors" +import * as path from "node:path" +import { isEmpty } from "lodash" + +interface SendSelectedFilesActionPayload { + typesName: string + fileTypes: string[] + storagePath: string + onFileSuccess?: (file: string) => Promise + onFileError?: (file: string) => Promise + onSuccess?: () => Promise + onError?: () => Promise +} + +export const selectAndSendFilesAction = createAsyncThunk< + undefined, + SendSelectedFilesActionPayload, + { state: ReduxRootState } +>( + ActionName.SendSelectedFiles, + async ( + { + typesName, + fileTypes, + storagePath, + onFileSuccess, + onFileError, + onSuccess, + onError, + }, + { rejectWithValue, dispatch, getState } + ) => { + const failedFiles = [] + + const deviceId = selectActiveApiDeviceId(getState()) + const openFileResult = await openFileRequest({ + filters: [ + { + name: typesName, + extensions: fileTypes, + }, + ], + properties: ["openFile", "multiSelections"], + }) + + if (!openFileResult.ok) { + return rejectWithValue("cancelled") + } + + if (!deviceId) { + return rejectWithValue(undefined) + } + + for (const file of openFileResult.data) { + const fileName = path.basename(file) + const sentFile = await dispatch( + sendFile({ + filePath: file, + targetPath: storagePath + fileName, + deviceId, + }) + ) + if (sentFile.meta.requestStatus === "fulfilled") { + await onFileSuccess?.(file) + } else { + failedFiles.push(file) + await onFileError?.(file) + } + } + if (isEmpty(failedFiles)) { + await onSuccess?.() + } else { + await onError?.() + } + return + } +) diff --git a/libs/generic-view/ui/src/lib/buttons/button-base/use-button-action.ts b/libs/generic-view/ui/src/lib/buttons/button-base/use-button-action.ts index b84c06d989..bbe476a9d9 100644 --- a/libs/generic-view/ui/src/lib/buttons/button-base/use-button-action.ts +++ b/libs/generic-view/ui/src/lib/buttons/button-base/use-button-action.ts @@ -7,11 +7,13 @@ import { closeAllModals, closeDomainModals, closeModal, + createEntityDataAction, deleteEntitiesDataAction, openModal, openToastAction, replaceModal, selectActiveApiDeviceId, + selectAndSendFilesAction, useScreenTitle, } from "generic-view/store" import { useDispatch, useSelector } from "react-redux" @@ -127,6 +129,30 @@ const runActions = (actions?: ButtonActions) => { }) ) break + case "entity-create": + await dispatch( + createEntityDataAction({ + data: action.data, + entitiesType: action.entitiesType, + deviceId: activeDeviceId, + onSuccess: () => { + return runActions(action.postActions?.success)(providers) + }, + onError: () => { + return runActions(action.postActions?.failure)(providers) + }, + }) + ) + break + case "file-upload": + await dispatch( + selectAndSendFilesAction({ + storagePath: action.storagePath, + typesName: action.typesName, + fileTypes: action.fileTypes, + }) + ) + break case "open-toast": await dispatch(openToastAction(action.toastKey)) break diff --git a/libs/generic-view/ui/src/lib/generated/mc-file-manager/file-list.ts b/libs/generic-view/ui/src/lib/generated/mc-file-manager/file-list.ts index 79499272ef..0d035365e3 100644 --- a/libs/generic-view/ui/src/lib/generated/mc-file-manager/file-list.ts +++ b/libs/generic-view/ui/src/lib/generated/mc-file-manager/file-list.ts @@ -10,20 +10,57 @@ interface FileListConfig { id: string name: string entitiesType: string + supportedFileTypes?: string[] + storagePath?: string } const CONFIG_MAP: Record> = { audioFiles: { name: "Music", entitiesType: "audioFiles", + supportedFileTypes: [ + "3gp", + "mp4", + "m4a", + "aac", + "ts", + "amr", + "flac", + "mid", + "xmf", + "mxmf", + "rtttl", + "rtx", + "ota", + "imy", + "mp3", + "mkv", + "wav", + "ogg", + ], + storagePath: "/storage/emulated/0/Music", }, imageFiles: { name: "Photos", entitiesType: "imageFiles", + supportedFileTypes: [ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "webp", + "heic", + "heif", + "avif", + ], + storagePath: "/storage/emulated/0/Pictures", }, ebookFiles: { name: "E-books", entitiesType: "ebookFiles", + supportedFileTypes: ["epub", "pdf"], + storagePath: "/storage/emulated/0/Documents", }, applicationFiles: { name: "Apps", @@ -349,7 +386,33 @@ const generateFileList = ({ }, config: { text: "Add file", - actions: [], + actions: [ + { + type: "file-upload", + storagePath: "/storage/emulated/0/Music/", + typesName: "Audio", + fileTypes: [ + "3gp", + "mp4", + "m4a", + "aac", + "ts", + "amr", + "flac", + "mid", + "xmf", + "mxmf", + "rtttl", + "rtx", + "ota", + "imy", + "mp3", + "mkv", + "wav", + "ogg", + ], + }, + ], }, }, [`${id}fileListEmptyTable`]: {