mirror of
https://github.com/mudita/mudita-center.git
synced 2025-12-23 22:28:03 -05:00
[CP-3386] Added photo preview (#2608)
Co-authored-by: slawomir-werner <slawomir.werner@mudita.com>
This commit is contained in:
committed by
GitHub
parent
127f7086ea
commit
3acf9e49ef
4
.gitignore
vendored
4
.gitignore
vendored
@@ -69,4 +69,6 @@ scripts/manage-test-files/file-manager-test-files/*
|
||||
|
||||
libs/app-mtp/**/*.js
|
||||
|
||||
*.orig
|
||||
*.orig
|
||||
|
||||
.github/copilot-instructions.md
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
shell,
|
||||
protocol,
|
||||
net,
|
||||
} from "electron"
|
||||
import { ipcMain } from "electron-better-ipc"
|
||||
import * as path from "path"
|
||||
@@ -77,6 +79,8 @@ import {
|
||||
registerShortcuts,
|
||||
} from "shared/utils"
|
||||
import { mockServiceEnabled, startServer, stopServer } from "e2e-mock-server"
|
||||
import getAppPath from "Core/__deprecated__/main/utils/get-app-path"
|
||||
import fs from "fs-extra"
|
||||
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
@@ -248,6 +252,33 @@ const createWindow = async () => {
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
} else {
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: "safe-file",
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
bypassCSP: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
app.whenReady().then(() => {
|
||||
protocol.handle("safe-file", async (request) => {
|
||||
const fileUrl = request.url.replace("safe-file://", "file:///")
|
||||
if (
|
||||
!fileUrl
|
||||
.toLowerCase()
|
||||
.startsWith(`file://${encodeURI(getAppPath())}`.toLowerCase())
|
||||
) {
|
||||
throw new Error(
|
||||
"Access to files outside of the userData directory is not allowed."
|
||||
)
|
||||
}
|
||||
return net.fetch(fileUrl)
|
||||
})
|
||||
})
|
||||
// AUTO DISABLED - fix me if you like :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
app.on("ready", createWindow)
|
||||
@@ -262,7 +293,8 @@ if (!gotTheLock) {
|
||||
stopServer()
|
||||
})
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.on("window-all-closed", async () => {
|
||||
await fs.remove(getAppPath("file-preview"))
|
||||
app.quit()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.965955 0.400337C1.24283 0.139987 1.69175 0.139987 1.96862 0.400337L7.98464 6.05719L14.0007 0.400337C14.2775 0.139987 14.7265 0.139987 15.0033 0.400337C15.2802 0.660686 15.2802 1.0828 15.0033 1.34315L8.98731 7L15.0033 12.6569C15.2802 12.9172 15.2802 13.3393 15.0033 13.5997C14.7265 13.86 14.2775 13.86 14.0007 13.5997L7.98464 7.94281L1.96862 13.5997C1.69175 13.86 1.24283 13.86 0.965955 13.5997C0.689075 13.3393 0.689075 12.9172 0.965955 12.6569L6.98197 7L0.965955 1.34315C0.689075 1.0828 0.689075 0.660686 0.965955 0.400337Z" fill="black"/>
|
||||
<svg width="16" height="16" viewBox="0 -1 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0.965955 0.400337C1.24283 0.139987 1.69175 0.139987 1.96862 0.400337L7.98464 6.05719L14.0007 0.400337C14.2775 0.139987 14.7265 0.139987 15.0033 0.400337C15.2802 0.660686 15.2802 1.0828 15.0033 1.34315L8.98731 7L15.0033 12.6569C15.2802 12.9172 15.2802 13.3393 15.0033 13.5997C14.7265 13.86 14.2775 13.86 14.0007 13.5997L7.98464 7.94281L1.96862 13.5997C1.69175 13.86 1.24283 13.86 0.965955 13.5997C0.689075 13.3393 0.689075 12.9172 0.965955 12.6569L6.98197 7L0.965955 1.34315C0.689075 1.0828 0.689075 0.660686 0.965955 0.400337Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 728 B |
@@ -12,6 +12,7 @@ import { isEmpty } from "lodash"
|
||||
import { RecursiveLayout } from "./recursive-layout"
|
||||
import styled from "styled-components"
|
||||
import { Icon, Toast, Typography } from "generic-view/ui"
|
||||
import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum"
|
||||
|
||||
const selectToastsToRender = createSelector(selectViewConfig, (config) => {
|
||||
return Object.entries(config || {})
|
||||
@@ -68,5 +69,5 @@ const ToastsWrapper = styled.div`
|
||||
position: fixed;
|
||||
right: 3.2rem;
|
||||
bottom: 3.2rem;
|
||||
z-index: 3;
|
||||
z-index: ${ModalLayers.Default};
|
||||
`
|
||||
|
||||
@@ -183,7 +183,7 @@ const SelfDataProvider: FunctionComponent<
|
||||
|
||||
for (const fieldConfig of dataProvider.fields) {
|
||||
const { componentField, providerField, ...config } = fieldConfig
|
||||
const fieldValue = get({ data: componentData }, providerField)
|
||||
const fieldValue = get(childrenProps, providerField)
|
||||
const value = processField(config, fieldValue)
|
||||
|
||||
set(childrenProps, componentField, value)
|
||||
|
||||
@@ -131,6 +131,12 @@ const nativeActionSelectFilesValidator = z.object({
|
||||
formKey: z.string().optional(),
|
||||
selectedFilesFieldName: z.string(),
|
||||
}),
|
||||
postActions: z
|
||||
.object({
|
||||
success: entityPostActionsValidator,
|
||||
failure: entityPostActionsValidator,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type NativeActionSelectFiles = z.infer<
|
||||
@@ -145,6 +151,12 @@ const nativeActionSelectDirectoryValidator = z.object({
|
||||
formKey: z.string().optional(),
|
||||
selectedDirectoryFieldName: z.string(),
|
||||
}),
|
||||
postActions: z
|
||||
.object({
|
||||
success: entityPostActionsValidator,
|
||||
failure: entityPostActionsValidator,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type NativeActionSelectDirectory = z.infer<
|
||||
@@ -188,6 +200,7 @@ const filesTransferExportFilesActionValidator = z.object({
|
||||
type: z.literal("export-files"),
|
||||
destinationPath: z.string(),
|
||||
entitiesType: z.string().optional(),
|
||||
singleEntityId: z.string().optional(),
|
||||
actionId: z.string(),
|
||||
|
||||
formOptions: z.object({
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const dataValidator = z.object({
|
||||
render: z.boolean(),
|
||||
render: z.boolean().or(z.array(z.boolean())),
|
||||
})
|
||||
export type ConditionalRendererData = z.infer<typeof dataValidator>
|
||||
|
||||
const configValidator = z.undefined()
|
||||
const configValidator = z
|
||||
.object({
|
||||
multipleConditionsMethod: z.enum(["and", "or"]).optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
export type ConditionalRendererConfig = z.infer<typeof configValidator>
|
||||
|
||||
export const conditionalRenderer = {
|
||||
key: "conditional-renderer",
|
||||
|
||||
@@ -21,6 +21,7 @@ const configValidator = z.object({
|
||||
validation: inputValidation.optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
size: z.enum(["small", "large"]).optional(),
|
||||
inactive: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type FormCheckboxInputConfig = z.infer<typeof configValidator>
|
||||
|
||||
@@ -16,6 +16,18 @@ const configValidator = z.object({
|
||||
selectedIdsFieldName: z.string().optional(),
|
||||
allIdsFieldName: z.string().optional(),
|
||||
}),
|
||||
previewOptions: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
entitiesType: z.string(),
|
||||
entityIdFieldName: z.string(),
|
||||
entityPathFieldName: z.string(),
|
||||
entityTitleFieldName: z.string(),
|
||||
entityMimeTypeFieldName: z.string(),
|
||||
entitySizeFieldName: z.string(),
|
||||
componentKey: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type TableConfig = z.infer<typeof configValidator>
|
||||
|
||||
@@ -21,6 +21,7 @@ export * from "./lib/file-transfer/legacy-send-file.action"
|
||||
export * from "./lib/file-transfer/get-file.action"
|
||||
export * from "./lib/file-transfer/send-files.action"
|
||||
export * from "./lib/file-transfer/send-files-transfer-analysis.action"
|
||||
export * from "./lib/file-transfer/files-transfer.type"
|
||||
export * from "./lib/backup/load-backup-metadata.action"
|
||||
export * from "./lib/backup/restore-backup.action"
|
||||
export * from "./lib/backup/backup.types"
|
||||
|
||||
@@ -140,7 +140,7 @@ export const sendFileViaMTP = createAsyncThunk<
|
||||
return error
|
||||
}
|
||||
|
||||
await delay(500, signal)
|
||||
await delay(250, signal)
|
||||
return await checkSendFileProgress()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const color = {
|
||||
blue1: "#40749A",
|
||||
blue2: "#6D9BBC",
|
||||
blue5: "#F2F7FA",
|
||||
grey0: "#2a2a2a",
|
||||
grey1: "#3B3F42",
|
||||
grey2: "#6A6A6A",
|
||||
grey3: "#A5A5A5",
|
||||
|
||||
@@ -149,7 +149,20 @@ const runActions = (actions?: ButtonActions) => {
|
||||
break
|
||||
case "select-files":
|
||||
{
|
||||
const selected = await customActions.selectFiles(action)
|
||||
const selected = await customActions.selectFiles(action, {
|
||||
onSuccess: async () => {
|
||||
await runActions(action.postActions?.success)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
onFailure: async () => {
|
||||
await runActions(action.postActions?.failure)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
})
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
@@ -158,7 +171,20 @@ const runActions = (actions?: ButtonActions) => {
|
||||
|
||||
case "select-directory":
|
||||
{
|
||||
const selected = await customActions.selectDirectory(action)
|
||||
const selected = await customActions.selectDirectory(action, {
|
||||
onSuccess: async () => {
|
||||
await runActions(action.postActions?.success)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
onFailure: async () => {
|
||||
await runActions(action.postActions?.failure)(
|
||||
providers,
|
||||
customActions
|
||||
)
|
||||
},
|
||||
})
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,9 +46,11 @@ export const useExportFilesButtonAction = () => {
|
||||
|
||||
if (action.entitiesType === undefined) return
|
||||
if (deviceId === undefined) return
|
||||
const selectedItems: string[] = getFormContext(
|
||||
action.sourceFormKey
|
||||
).getValues(action.selectedItemsFieldName)
|
||||
const selectedItems: string[] = action.singleEntityId
|
||||
? [action.singleEntityId]
|
||||
: getFormContext(action.sourceFormKey).getValues(
|
||||
action.selectedItemsFieldName
|
||||
)
|
||||
|
||||
const destinationPath: string = form.getValues(
|
||||
action.formOptions.selectedDirectoryFieldName
|
||||
|
||||
@@ -12,7 +12,13 @@ import { ipcRenderer } from "electron-better-ipc"
|
||||
export const useSelectDirectoryButtonAction = () => {
|
||||
const getFormContext = useViewFormContext()
|
||||
return useCallback(
|
||||
async (action: NativeActionSelectDirectory) => {
|
||||
async (
|
||||
action: NativeActionSelectDirectory,
|
||||
callbacks: {
|
||||
onSuccess?: () => Promise<void>
|
||||
onFailure?: () => Promise<void>
|
||||
}
|
||||
) => {
|
||||
const downloadsPath = (await ipcRenderer.callMain(
|
||||
"get-downloads-path"
|
||||
)) as string
|
||||
@@ -24,6 +30,7 @@ export const useSelectDirectoryButtonAction = () => {
|
||||
})
|
||||
|
||||
if (!selectorResponse.ok || !selectorResponse.data.length) {
|
||||
await callbacks.onFailure?.()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -32,6 +39,7 @@ export const useSelectDirectoryButtonAction = () => {
|
||||
selectorResponse.data
|
||||
)
|
||||
|
||||
await callbacks.onSuccess?.()
|
||||
return true
|
||||
},
|
||||
[getFormContext]
|
||||
|
||||
@@ -12,7 +12,13 @@ export const useSelectFilesButtonAction = () => {
|
||||
const getFormContext = useViewFormContext()
|
||||
|
||||
return useCallback(
|
||||
async (action: NativeActionSelectFiles) => {
|
||||
async (
|
||||
action: NativeActionSelectFiles,
|
||||
callbacks: {
|
||||
onSuccess?: () => Promise<void>
|
||||
onFailure?: () => Promise<void>
|
||||
}
|
||||
) => {
|
||||
const selectorResponse = await openFileRequest({
|
||||
...(action.multiple
|
||||
? { properties: ["openFile", "multiSelections"] }
|
||||
@@ -27,6 +33,7 @@ export const useSelectFilesButtonAction = () => {
|
||||
}),
|
||||
})
|
||||
if (!selectorResponse.ok) {
|
||||
await callbacks.onFailure?.()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -34,6 +41,7 @@ export const useSelectFilesButtonAction = () => {
|
||||
action.formOptions.selectedFilesFieldName,
|
||||
selectorResponse.data
|
||||
)
|
||||
await callbacks.onSuccess?.()
|
||||
return true
|
||||
},
|
||||
[getFormContext]
|
||||
|
||||
@@ -4,35 +4,58 @@
|
||||
*/
|
||||
|
||||
import { ComponentGenerator, IconType } from "generic-view/utils"
|
||||
import { ButtonTextConfig } from "generic-view/models"
|
||||
|
||||
export const generateDeleteFilesButtonActions = (
|
||||
key: string,
|
||||
{ singleEntityId }: { singleEntityId?: string } = {}
|
||||
): ButtonTextConfig["actions"] => {
|
||||
return [
|
||||
...(singleEntityId !== undefined
|
||||
? ([
|
||||
{
|
||||
type: "form-set-field",
|
||||
formKey: `${key}fileListForm`,
|
||||
key: "selectedItems",
|
||||
value: [singleEntityId],
|
||||
},
|
||||
] as ButtonTextConfig["actions"])
|
||||
: []),
|
||||
{
|
||||
type: "open-modal",
|
||||
modalKey: `${key}deleteModal`,
|
||||
domain: "files-delete",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const generateDeleteFiles: ComponentGenerator<{
|
||||
id: string
|
||||
entityType: string
|
||||
}> = (key, { id, entityType }) => {
|
||||
}> = (key, { entityType }) => {
|
||||
return {
|
||||
[`${key}${id}deleteModal`]: {
|
||||
[`${key}deleteModal`]: {
|
||||
component: "modal",
|
||||
config: {
|
||||
size: "small",
|
||||
},
|
||||
childrenKeys: [
|
||||
`${key}${id}deleteModalIcon`,
|
||||
`${key}${id}deleteModalTitle`,
|
||||
`${key}${id}deleteModalContent`,
|
||||
`${key}${id}deleteModalButtons`,
|
||||
`${key}deleteModalIcon`,
|
||||
`${key}deleteModalTitle`,
|
||||
`${key}deleteModalContent`,
|
||||
`${key}deleteModalButtons`,
|
||||
],
|
||||
},
|
||||
[`${key}${id}deleteModalIcon`]: {
|
||||
[`${key}deleteModalIcon`]: {
|
||||
component: "modal.titleIcon",
|
||||
config: {
|
||||
type: IconType.Exclamation,
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteModalTitle`]: {
|
||||
[`${key}deleteModalTitle`]: {
|
||||
component: "modal.title",
|
||||
childrenKeys: [`${key}${id}deleteModalTitleText`],
|
||||
childrenKeys: [`${key}deleteModalTitleText`],
|
||||
},
|
||||
[`${key}${id}deleteModalTitleText`]: {
|
||||
[`${key}deleteModalTitleText`]: {
|
||||
component: "format-message",
|
||||
config: {
|
||||
messageTemplate:
|
||||
@@ -40,7 +63,7 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
},
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}${id}fileListForm`,
|
||||
formKey: `${key}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
@@ -50,7 +73,7 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteModalContent`]: {
|
||||
[`${key}deleteModalContent`]: {
|
||||
component: "typography.p1",
|
||||
config: {
|
||||
messageTemplate:
|
||||
@@ -58,7 +81,7 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
},
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}${id}fileListForm`,
|
||||
formKey: `${key}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
@@ -68,32 +91,32 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteModalButtons`]: {
|
||||
[`${key}deleteModalButtons`]: {
|
||||
component: "modal.buttons",
|
||||
childrenKeys: [
|
||||
`${key}${id}deleteModalCancelButton`,
|
||||
`${key}${id}deleteModalConfirmButton`,
|
||||
`${key}deleteModalCancelButton`,
|
||||
`${key}deleteModalConfirmButton`,
|
||||
],
|
||||
},
|
||||
[`${key}${id}deleteModalCancelButton`]: {
|
||||
[`${key}deleteModalCancelButton`]: {
|
||||
component: "button-secondary",
|
||||
config: {
|
||||
text: "Cancel",
|
||||
actions: [
|
||||
{
|
||||
type: "close-modal",
|
||||
modalKey: `${key}${id}deleteModal`,
|
||||
modalKey: `${key}deleteModal`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteModalConfirmButton`]: {
|
||||
[`${key}deleteModalConfirmButton`]: {
|
||||
component: "button-primary",
|
||||
config: {
|
||||
actions: [
|
||||
{
|
||||
type: "open-modal",
|
||||
modalKey: `${key}${id}deleteProgressModal`,
|
||||
modalKey: `${key}deleteProgressModal`,
|
||||
domain: "files-delete",
|
||||
},
|
||||
{
|
||||
@@ -116,14 +139,14 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
},
|
||||
{
|
||||
type: "open-modal",
|
||||
modalKey: `${key}${id}deleteErrorModal`,
|
||||
modalKey: `${key}deleteErrorModal`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
childrenKeys: [`${key}${id}deleteModalConfirmButtonText`],
|
||||
childrenKeys: [`${key}deleteModalConfirmButtonText`],
|
||||
layout: {
|
||||
flexLayout: {
|
||||
direction: "row",
|
||||
@@ -132,7 +155,7 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
},
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}${id}fileListForm`,
|
||||
formKey: `${key}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
@@ -141,7 +164,7 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteModalConfirmButtonText`]: {
|
||||
[`${key}deleteModalConfirmButtonText`]: {
|
||||
component: "format-message",
|
||||
config: {
|
||||
messageTemplate:
|
||||
@@ -149,7 +172,7 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
},
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}${id}fileListForm`,
|
||||
formKey: `${key}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
@@ -159,39 +182,39 @@ export const generateDeleteFiles: ComponentGenerator<{
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteProgressModal`]: {
|
||||
[`${key}deleteProgressModal`]: {
|
||||
component: "modal",
|
||||
config: {
|
||||
size: "small",
|
||||
},
|
||||
childrenKeys: [
|
||||
`${key}${id}deleteProgressModalIcon`,
|
||||
`${key}${id}deleteProgressModalTitle`,
|
||||
`${key}deleteProgressModalIcon`,
|
||||
`${key}deleteProgressModalTitle`,
|
||||
],
|
||||
},
|
||||
[`${key}${id}deleteProgressModalIcon`]: {
|
||||
[`${key}deleteProgressModalIcon`]: {
|
||||
component: "modal.titleIcon",
|
||||
config: {
|
||||
type: IconType.SpinnerDark,
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteProgressModalTitle`]: {
|
||||
[`${key}deleteProgressModalTitle`]: {
|
||||
component: "modal.title",
|
||||
config: {
|
||||
text: "Deleting, please wait...",
|
||||
},
|
||||
},
|
||||
[`${key}${id}deleteErrorModal`]: {
|
||||
[`${key}deleteErrorModal`]: {
|
||||
component: "modal",
|
||||
config: {
|
||||
size: "small",
|
||||
},
|
||||
childrenKeys: [`${key}${id}deleteErrorModalContent`],
|
||||
childrenKeys: [`${key}deleteErrorModalContent`],
|
||||
},
|
||||
[`${key}${id}deleteErrorModalContent`]: {
|
||||
[`${key}deleteErrorModalContent`]: {
|
||||
component: "entities-delete-error",
|
||||
config: {
|
||||
modalKey: `${key}${id}deleteErrorModal`,
|
||||
modalKey: `${key}deleteErrorModal`,
|
||||
entitiesType: entityType,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentGenerator, IconType } from "generic-view/utils"
|
||||
import { McFileManagerConfig } from "generic-view/models"
|
||||
import { ButtonTextConfig, McFileManagerConfig } from "generic-view/models"
|
||||
import { SendFilesAction } from "../../../../../store/src/lib/file-transfer/files-transfer.type"
|
||||
|
||||
export const generateFilesExportProcessButtonKey = (key: string) =>
|
||||
@@ -17,74 +17,38 @@ export const generateFilesExportButtonModalKey = (
|
||||
return generateFilesExportProcessButtonKey(key) + "Modal" + modalName
|
||||
}
|
||||
|
||||
export const generateFileExportProcessButton: ComponentGenerator<
|
||||
Pick<
|
||||
McFileManagerConfig["categories"][number],
|
||||
"entityType" | "supportedFileTypes" | "directoryPath" | "label"
|
||||
> & {
|
||||
storagePath: string
|
||||
export const generateFilesExportButtonActions = (
|
||||
key: string,
|
||||
{
|
||||
singleEntityId,
|
||||
entityType,
|
||||
exportActionId,
|
||||
}: Pick<McFileManagerConfig["categories"][number], "entityType"> & {
|
||||
singleEntityId?: string
|
||||
exportActionId: 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: [
|
||||
): ButtonTextConfig["actions"] => {
|
||||
return [
|
||||
...(singleEntityId !== undefined
|
||||
? ([
|
||||
{
|
||||
type: "select-directory",
|
||||
title: "Choose a directory to export",
|
||||
formOptions: {
|
||||
formKey: `${key}fileExportForm`,
|
||||
selectedDirectoryFieldName: "exportPath",
|
||||
},
|
||||
type: "form-set-field",
|
||||
formKey: `${key}fileListForm`,
|
||||
key: "selectedItems",
|
||||
value: [singleEntityId],
|
||||
},
|
||||
{
|
||||
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"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
] as ButtonTextConfig["actions"])
|
||||
: []),
|
||||
{
|
||||
type: "select-directory",
|
||||
title: "Choose a directory to export",
|
||||
formOptions: {
|
||||
formKey: `${key}fileListForm`,
|
||||
selectedDirectoryFieldName: "exportPath",
|
||||
},
|
||||
...(singleEntityId !== undefined
|
||||
? {
|
||||
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",
|
||||
@@ -92,14 +56,92 @@ export const generateFileExportProcessButton: ComponentGenerator<
|
||||
key: "selectedItems",
|
||||
value: [],
|
||||
},
|
||||
{
|
||||
type: "replace-modal",
|
||||
modalKey: generateFilesExportButtonModalKey(key, "Finished"),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
type: "open-modal",
|
||||
modalKey: generateFilesExportButtonModalKey(key, "Progress"),
|
||||
},
|
||||
{
|
||||
type: "export-files",
|
||||
formOptions: {
|
||||
formKey: `${key}fileListForm`,
|
||||
selectedDirectoryFieldName: "exportPath",
|
||||
},
|
||||
destinationPath: "exportPath",
|
||||
sourceFormKey: `${key}fileListForm`,
|
||||
selectedItemsFieldName: "selectedItems",
|
||||
entitiesType: entityType,
|
||||
singleEntityId,
|
||||
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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const generateFileExportProcessButton: ComponentGenerator<
|
||||
Pick<
|
||||
McFileManagerConfig["categories"][number],
|
||||
"entityType" | "directoryPath"
|
||||
> & {
|
||||
singleEntityId?: string
|
||||
exportActionId: string
|
||||
}
|
||||
> = (key, { directoryPath, entityType, singleEntityId, exportActionId }) => {
|
||||
return {
|
||||
[generateFilesExportProcessButtonKey(key)]: {
|
||||
component: "button-text",
|
||||
config: {
|
||||
text: entityType === "applicationFiles" ? "Export APK" : "Export",
|
||||
icon: IconType.Export,
|
||||
actions: generateFilesExportButtonActions(key, {
|
||||
singleEntityId,
|
||||
entityType,
|
||||
exportActionId,
|
||||
}),
|
||||
modifiers: ["uppercase"],
|
||||
},
|
||||
dataProvider: {
|
||||
@@ -113,7 +155,6 @@ export const generateFileExportProcessButton: ComponentGenerator<
|
||||
},
|
||||
],
|
||||
},
|
||||
childrenKeys: [`${key}fileExportForm`],
|
||||
},
|
||||
[generateFilesExportButtonModalKey(key, "Progress")]: {
|
||||
component: "modal",
|
||||
@@ -209,15 +250,5 @@ export const generateFileExportProcessButton: ComponentGenerator<
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}fileExportForm`]: {
|
||||
component: "form",
|
||||
config: {
|
||||
formOptions: {
|
||||
defaultValues: {
|
||||
exportPath: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
*/
|
||||
|
||||
import { ComponentGenerator, IconType, Subview } from "generic-view/utils"
|
||||
import { generateDeleteFiles } from "./delete-files"
|
||||
import {
|
||||
generateDeleteFiles,
|
||||
generateDeleteFilesButtonActions,
|
||||
} from "./delete-files"
|
||||
import { McFileManagerConfig } from "generic-view/models"
|
||||
import {
|
||||
generateFileUploadProcessButton,
|
||||
@@ -64,6 +67,8 @@ const generateFileList: ComponentGenerator<
|
||||
filesToUpload: [],
|
||||
activeFileName: null,
|
||||
activeFilePath: null,
|
||||
exportPath: "",
|
||||
previewMode: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -138,17 +143,26 @@ const generateFileList: ComponentGenerator<
|
||||
},
|
||||
[`${key}${id}fileListPanelDefaultMode`]: {
|
||||
component: "conditional-renderer",
|
||||
config: {
|
||||
multipleConditionsMethod: "or",
|
||||
},
|
||||
childrenKeys: [`${key}${id}fileListPanel`],
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
componentField: "data.render",
|
||||
componentField: "data.render[0]",
|
||||
modifier: "length",
|
||||
condition: "eq",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
providerField: "previewMode",
|
||||
componentField: "data.render[1]",
|
||||
condition: "eq",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -198,17 +212,26 @@ const generateFileList: ComponentGenerator<
|
||||
}),
|
||||
[`${key}${id}fileListPanelSelectMode`]: {
|
||||
component: "conditional-renderer",
|
||||
config: {
|
||||
multipleConditionsMethod: "and",
|
||||
},
|
||||
childrenKeys: [`${key}${id}fileListPanelSelector`],
|
||||
dataProvider: {
|
||||
source: "form-fields",
|
||||
fields: [
|
||||
{
|
||||
providerField: "selectedItems",
|
||||
componentField: "data.render",
|
||||
componentField: "data.render[0]",
|
||||
modifier: "length",
|
||||
condition: "gt",
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
providerField: "previewMode",
|
||||
componentField: "data.render[1]",
|
||||
condition: "eq",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -274,27 +297,18 @@ const generateFileList: ComponentGenerator<
|
||||
...generateFileExportProcessButton(`${key}${id}`, {
|
||||
directoryPath,
|
||||
entityType,
|
||||
storagePath,
|
||||
supportedFileTypes,
|
||||
label,
|
||||
exportActionId: entityType + "Export",
|
||||
}),
|
||||
[`${key}${id}deleteButton`]: {
|
||||
component: "button-text",
|
||||
config: {
|
||||
text: entityType === "applicationFiles" ? "Delete APK" : "Delete",
|
||||
icon: IconType.Delete,
|
||||
actions: [
|
||||
{
|
||||
type: "open-modal",
|
||||
modalKey: `${key}${id}deleteModal`,
|
||||
domain: "files-delete",
|
||||
},
|
||||
],
|
||||
actions: generateDeleteFilesButtonActions(`${key}${id}`),
|
||||
modifiers: ["uppercase"],
|
||||
},
|
||||
},
|
||||
...generateDeleteFiles(key, {
|
||||
id,
|
||||
...generateDeleteFiles(`${key}${id}`, {
|
||||
entityType,
|
||||
}),
|
||||
[`${key}${id}fileListEmptyState`]: {
|
||||
@@ -340,6 +354,16 @@ const generateFileList: ComponentGenerator<
|
||||
selectedIdsFieldName: "selectedItems",
|
||||
allIdsFieldName: "allItems",
|
||||
},
|
||||
previewOptions: {
|
||||
enabled: entityType === "imageFiles",
|
||||
entitiesType: entityType,
|
||||
entityIdFieldName: "id",
|
||||
entityPathFieldName: "filePath",
|
||||
entityTitleFieldName: "fileName",
|
||||
entityMimeTypeFieldName: "mimeType",
|
||||
entitySizeFieldName: "fileSize",
|
||||
componentKey: `${key}${id}`,
|
||||
},
|
||||
},
|
||||
dataProvider: {
|
||||
entitiesType: entityType,
|
||||
@@ -487,6 +511,18 @@ const generateFileList: ComponentGenerator<
|
||||
},
|
||||
],
|
||||
},
|
||||
dataProviderSecondary: {
|
||||
source: "form-fields",
|
||||
formKey: `${key}${id}fileListForm`,
|
||||
fields: [
|
||||
{
|
||||
providerField: "previewMode",
|
||||
componentField: "config.inactive",
|
||||
condition: "eq",
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[`${key}${id}columnName`]: {
|
||||
component: "table.cell",
|
||||
|
||||
@@ -33,6 +33,8 @@ import Spinner from "Core/__deprecated__/renderer/svg/spinner.svg"
|
||||
import Contact from "Core/__deprecated__/renderer/svg/contact.svg"
|
||||
import ContactsBook from "Core/__deprecated__/renderer/svg/menu-contacts.svg"
|
||||
|
||||
import ArrowLeft from "./svg/arrow-left.svg"
|
||||
import ArrowRight from "./svg/arrow-right.svg"
|
||||
import Backup from "./svg/backup.svg"
|
||||
import Book from "./svg/book.svg"
|
||||
import MusicNote from "./svg/music-note.svg"
|
||||
@@ -67,6 +69,8 @@ import Export from "./svg/export.svg"
|
||||
import { IconType } from "generic-view/utils"
|
||||
|
||||
const typeToIcon: Record<IconType, typeof BatteryHigh> = {
|
||||
[IconType.ArrowLeft]: ArrowLeft,
|
||||
[IconType.ArrowRight]: ArrowRight,
|
||||
[IconType.Battery0]: BatteryEmpty,
|
||||
[IconType.Battery1]: BatteryVeryLow,
|
||||
[IconType.Battery2]: BatteryLow,
|
||||
|
||||
5
libs/generic-view/ui/src/lib/icon/svg/arrow-left.svg
Normal file
5
libs/generic-view/ui/src/lib/icon/svg/arrow-left.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M25.3334 15.8089C25.3334 16.218 25.0119 16.5496 24.6154 16.5496L9.07976 16.5496L13.6439 21.4105C13.9198 21.7044 13.9126 22.1733 13.6278 22.458C13.343 22.7426 12.8885 22.7352 12.6126 22.4413L6.86899 16.3243C6.73646 16.1831 6.66373 15.9934 6.6668 15.7969C6.66987 15.6004 6.7485 15.4132 6.88538 15.2766L12.629 9.5418C12.9139 9.2573 13.3684 9.26499 13.6442 9.55897C13.9199 9.85295 13.9125 10.3219 13.6275 10.6064L9.15893 15.0681L24.6154 15.0681C25.0119 15.0681 25.3334 15.3998 25.3334 15.8089Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
5
libs/generic-view/ui/src/lib/icon/svg/arrow-right.svg
Normal file
5
libs/generic-view/ui/src/lib/icon/svg/arrow-right.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M6.66675 16.1911C6.66675 15.782 6.98818 15.4504 7.3847 15.4504L22.9204 15.4504L18.3562 10.5895C18.0803 10.2956 18.0875 9.82667 18.3723 9.54203C18.6571 9.25738 19.1116 9.26482 19.3875 9.55865L25.1311 15.6757C25.2637 15.8169 25.3364 16.0066 25.3333 16.2031C25.3303 16.3996 25.2516 16.5868 25.1147 16.7234L19.3712 22.4582C19.0862 22.7427 18.6317 22.735 18.356 22.441C18.0802 22.1471 18.0877 21.6781 18.3726 21.3936L22.8412 16.9319L7.3847 16.9319C6.98818 16.9319 6.66675 16.6002 6.66675 16.1911Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
@@ -1,9 +1,15 @@
|
||||
<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>
|
||||
<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="currentColor"/>
|
||||
<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="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -4,14 +4,25 @@
|
||||
*/
|
||||
|
||||
import { APIFC } from "generic-view/utils"
|
||||
import { ConditionalRendererData } from "generic-view/models"
|
||||
import {
|
||||
ConditionalRendererConfig,
|
||||
ConditionalRendererData,
|
||||
} from "generic-view/models"
|
||||
|
||||
export const ConditionalRenderer: APIFC<ConditionalRendererData> = ({
|
||||
children,
|
||||
data,
|
||||
}) => {
|
||||
if (data?.render) {
|
||||
return children
|
||||
export const ConditionalRenderer: APIFC<
|
||||
ConditionalRendererData,
|
||||
ConditionalRendererConfig
|
||||
> = ({ children, data, config }) => {
|
||||
const render = data?.render || false
|
||||
|
||||
if (typeof render === "boolean") {
|
||||
return render ? children : null
|
||||
}
|
||||
if (config?.multipleConditionsMethod === "and") {
|
||||
return render.every(Boolean) ? children : null
|
||||
}
|
||||
if (config?.multipleConditionsMethod === "or") {
|
||||
return render.some(Boolean) ? children : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ export const CheckboxInput: APIFC<undefined, Config> = ({
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (config.inactive) {
|
||||
return
|
||||
}
|
||||
const checked = e.target.checked
|
||||
config.onToggle?.(checked)
|
||||
|
||||
@@ -87,6 +90,9 @@ export const CheckboxInput: APIFC<undefined, Config> = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (config.inactive) {
|
||||
return
|
||||
}
|
||||
if (multiSelect) {
|
||||
if (multiSelect.selectedValues.length === multiSelect.allValues.length) {
|
||||
setValue(inputName, true)
|
||||
@@ -99,7 +105,7 @@ export const CheckboxInput: APIFC<undefined, Config> = ({
|
||||
multiSelect.selectedValues.length < multiSelect.allValues.length
|
||||
}
|
||||
}
|
||||
}, [fieldRegistrar.ref, inputName, multiSelect, setValue])
|
||||
}, [config.inactive, fieldRegistrar.ref, inputName, multiSelect, setValue])
|
||||
|
||||
return (
|
||||
<CheckboxInputWrapper
|
||||
@@ -115,8 +121,8 @@ export const CheckboxInput: APIFC<undefined, Config> = ({
|
||||
data-testid={CheckboxTestIds.Checkbox}
|
||||
type={"checkbox"}
|
||||
value={config.value}
|
||||
checked={config.checked}
|
||||
disabled={config.disabled}
|
||||
checked={!config.inactive && config.checked}
|
||||
disabled={config.disabled || config.inactive}
|
||||
{...fieldRegistrar}
|
||||
onChange={handleChange}
|
||||
ref={handleRef}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react"
|
||||
import { useSelector } from "react-redux"
|
||||
import styled, { css } from "styled-components"
|
||||
import { selectEntityData, selectFilesTransferMode } from "generic-view/store"
|
||||
import { IconType } from "generic-view/utils"
|
||||
import { Modal } from "../../interactive/modal/modal"
|
||||
import { IconButton } from "../../shared/button"
|
||||
import { Icon } from "../../icon/icon"
|
||||
import { Typography } from "../../typography"
|
||||
import { SpinnerLoader } from "../../shared/spinner-loader"
|
||||
import { ImagePreview } from "./image-preview"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { useFilesPreview, UseFilesPreviewParams } from "./use-files-preview"
|
||||
import { activeDeviceIdSelector } from "active-device-registry/feature"
|
||||
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { ButtonIcon } from "../../buttons/button-icon"
|
||||
import { generateFilesExportButtonActions } from "../../generated/mc-file-manager/file-export-button"
|
||||
import { generateDeleteFilesButtonActions } from "../../generated/mc-file-manager/delete-files"
|
||||
import { ModalLayers } from "Core/modals-manager/constants/modal-layers.enum"
|
||||
|
||||
interface Props {
|
||||
componentKey: string
|
||||
entitiesConfig: UseFilesPreviewParams["entitiesConfig"]
|
||||
items: string[]
|
||||
activeItem: string | undefined
|
||||
onActiveItemChange: (item: string | undefined) => void
|
||||
actions?: React.ReactNode
|
||||
}
|
||||
|
||||
export const FilePreview: FunctionComponent<Props> = memo(
|
||||
({ items, activeItem, onActiveItemChange, entitiesConfig, componentKey }) => {
|
||||
const deviceId = useSelector(activeDeviceIdSelector)
|
||||
const entity = useSelector((state: ReduxRootState) => {
|
||||
if (!activeItem || !deviceId) return undefined
|
||||
return selectEntityData(state, {
|
||||
deviceId,
|
||||
entitiesType: entitiesConfig.type,
|
||||
entityId: activeItem,
|
||||
})
|
||||
})
|
||||
const nextIdReference = useRef<string>()
|
||||
|
||||
// TODO: Handle file transfer mode
|
||||
const fileTransferMode = useSelector(selectFilesTransferMode)
|
||||
|
||||
const {
|
||||
data: fileInfo,
|
||||
nextId,
|
||||
previousId,
|
||||
} = useFilesPreview({
|
||||
items,
|
||||
activeItem,
|
||||
entitiesConfig,
|
||||
})
|
||||
|
||||
const isLoading = !fileInfo
|
||||
|
||||
const entityType = useMemo(() => {
|
||||
if (!entity) return ""
|
||||
return entity[entitiesConfig.mimeTypeField] as string
|
||||
}, [entitiesConfig.mimeTypeField, entity])
|
||||
|
||||
const entityName = useMemo(() => {
|
||||
if (!entity) return ""
|
||||
return entity[entitiesConfig.titleField] as string
|
||||
}, [entitiesConfig.titleField, entity])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onActiveItemChange(undefined)
|
||||
}, [onActiveItemChange])
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
// TODO: Handle errors
|
||||
}, [])
|
||||
|
||||
const handlePreviousFile = useCallback(() => {
|
||||
onActiveItemChange(previousId)
|
||||
}, [onActiveItemChange, previousId])
|
||||
|
||||
const handleNextFile = useCallback(() => {
|
||||
onActiveItemChange(nextIdReference.current || nextId)
|
||||
}, [nextId, onActiveItemChange])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
handlePreviousFile()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
handleNextFile()
|
||||
}
|
||||
},
|
||||
[handleNextFile, handlePreviousFile]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
if (!activeItem) {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [activeItem, handleKeyDown])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
componentKey={"file-preview-modal"}
|
||||
config={{
|
||||
defaultOpened: Boolean(activeItem),
|
||||
width: 960,
|
||||
maxHeight: 640,
|
||||
padding: 0,
|
||||
modalLayer: ModalLayers.Default - 1,
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<Header>
|
||||
<Typography.P1 config={{ color: "white" }}>
|
||||
{entityName}
|
||||
</Typography.P1>
|
||||
<IconButton onClick={handleClose}>
|
||||
<Icon
|
||||
config={{
|
||||
type: IconType.Close,
|
||||
size: "tiny",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Header>
|
||||
<Main>
|
||||
<Loader>
|
||||
<SpinnerLoader />
|
||||
</Loader>
|
||||
<AnimatePresence initial={true} mode={"wait"}>
|
||||
{!isLoading && (
|
||||
<PreviewWrapper
|
||||
key={fileInfo?.path}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{entityType.startsWith("image") && (
|
||||
<ImagePreview src={fileInfo.path} onError={handleError} />
|
||||
)}
|
||||
</PreviewWrapper>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Main>
|
||||
{items.length > 1 && (
|
||||
<Navigation>
|
||||
<NavigationButton onClick={handlePreviousFile}>
|
||||
<Icon
|
||||
config={{
|
||||
type: IconType.ArrowLeft,
|
||||
size: "large",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</NavigationButton>
|
||||
<NavigationButton onClick={handleNextFile}>
|
||||
<Icon
|
||||
config={{
|
||||
type: IconType.ArrowRight,
|
||||
size: "large",
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</NavigationButton>
|
||||
</Navigation>
|
||||
)}
|
||||
<Footer>
|
||||
<ButtonIcon
|
||||
config={{
|
||||
icon: IconType.Export,
|
||||
iconSize: "large",
|
||||
actions: [
|
||||
...generateFilesExportButtonActions(componentKey, {
|
||||
exportActionId: "previewExport",
|
||||
singleEntityId: activeItem,
|
||||
entityType: entitiesConfig.type,
|
||||
}),
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ButtonIcon
|
||||
config={{
|
||||
icon: IconType.Delete,
|
||||
iconSize: "large",
|
||||
actions: generateDeleteFilesButtonActions(componentKey, {
|
||||
singleEntityId: activeItem,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Footer>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const frameCommonStyles = css`
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 8rem;
|
||||
left: 0;
|
||||
padding: 0 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 2.4rem;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition-property: opacity, visibility;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const Header = styled.header`
|
||||
${frameCommonStyles};
|
||||
top: 0;
|
||||
|
||||
&:before {
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.34) 100%
|
||||
);
|
||||
filter: drop-shadow(0 1rem 5rem rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
color: ${({ theme }) => theme.color.white} !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
`
|
||||
|
||||
const Footer = styled.footer`
|
||||
${frameCommonStyles};
|
||||
bottom: 0;
|
||||
|
||||
&:before {
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0) -2.5%,
|
||||
rgba(0, 0, 0, 0.34) 100%
|
||||
);
|
||||
filter: drop-shadow(0 1rem 5rem rgba(0, 0, 0, 0.08));
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: ${({ theme }) => theme.color.white} !important;
|
||||
}
|
||||
`
|
||||
|
||||
const Navigation = styled.nav`
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
padding: 0 2.4rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition-property: opacity, visibility;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: ease-in-out;
|
||||
`
|
||||
|
||||
const NavigationButton = styled.button`
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
border: none;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ theme }) => theme.color.grey1};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
`
|
||||
|
||||
const ModalContent = styled.section`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: ${({ theme }) => theme.color.grey0};
|
||||
|
||||
&:hover {
|
||||
${Header}, ${Footer}, ${Navigation} {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Loader = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const PreviewWrapper = styled(motion.div)`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const Main = styled.main`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 64rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${({ theme }) => theme.color.grey0};
|
||||
`
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import styled from "styled-components"
|
||||
|
||||
interface Props {
|
||||
src?: string
|
||||
onError?: () => void
|
||||
}
|
||||
|
||||
export const ImagePreview: FunctionComponent<Props> = ({ src, onError }) => {
|
||||
const loadedTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
loadedTimeoutRef.current = setTimeout(() => {
|
||||
// Ensure bigger images are fully rendered
|
||||
setLoaded(true)
|
||||
}, 100)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
clearTimeout(loadedTimeoutRef.current)
|
||||
setLoaded(false)
|
||||
}, [src])
|
||||
|
||||
return (
|
||||
<Wrapper $loaded={loaded}>
|
||||
<BackgroundImage $url={src} />
|
||||
<MainImage key={src} src={src} onLoad={onLoad} onError={onError} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper = styled.div<{ $loaded?: boolean }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: ${({ $loaded }) => ($loaded ? 1 : 0)};
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
`
|
||||
|
||||
const MainImage = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const BackgroundImage = styled.div<{ $url?: string }>`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
background-image: url("${({ $url }) => $url}");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
filter: blur(5rem) brightness(0.4);
|
||||
`
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { useDispatch, useSelector, useStore } from "react-redux"
|
||||
import {
|
||||
FileWithPath,
|
||||
selectEntityData,
|
||||
sendFiles,
|
||||
SendFilesAction,
|
||||
} from "generic-view/store"
|
||||
import { AppDispatch, ReduxRootState } from "Core/__deprecated__/renderer/store"
|
||||
import { activeDeviceIdSelector } from "active-device-registry/feature"
|
||||
import { useCallback, useRef } from "react"
|
||||
import { checkPath, removeDirectory } from "system-utils/feature"
|
||||
import {
|
||||
isMtpPathInternal,
|
||||
sliceMtpPaths,
|
||||
} from "../../buttons/button-base/file-transfer-paths-helper"
|
||||
import path from "node:path"
|
||||
|
||||
export interface FilePreviewResponse {
|
||||
id: string
|
||||
path: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface UseFilePreviewDownloadParams {
|
||||
tempDirectoryPath?: string
|
||||
actionId: string
|
||||
entitiesType: string
|
||||
fields: {
|
||||
idField: string
|
||||
pathField: string
|
||||
titleField: string
|
||||
mimeTypeField: string
|
||||
sizeField: string
|
||||
}
|
||||
}
|
||||
|
||||
export const useFilePreviewDownload = ({
|
||||
tempDirectoryPath,
|
||||
actionId,
|
||||
entitiesType,
|
||||
fields,
|
||||
}: UseFilePreviewDownloadParams) => {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const store = useStore<ReduxRootState>()
|
||||
const abortReferences = useRef<Record<string, VoidFunction>>({})
|
||||
const deviceId = useSelector(activeDeviceIdSelector)
|
||||
|
||||
const getFilePath = useCallback(
|
||||
(entityFilePath: string) => {
|
||||
if (!tempDirectoryPath) {
|
||||
return undefined
|
||||
}
|
||||
const deviceFilePath = (entityFilePath as string).split("/")
|
||||
const fileName = deviceFilePath.pop() as string
|
||||
const nativeDir = path.join(tempDirectoryPath, ...deviceFilePath)
|
||||
const nativePath = path.join(nativeDir, fileName)
|
||||
return {
|
||||
nativeDir,
|
||||
nativePath: nativePath,
|
||||
safePath:
|
||||
process.platform === "win32"
|
||||
? nativePath
|
||||
: `safe-file://${nativePath}`,
|
||||
}
|
||||
},
|
||||
[tempDirectoryPath]
|
||||
)
|
||||
|
||||
const downloadFile = useCallback(
|
||||
async (
|
||||
entityId: string,
|
||||
onFileNameFound?: (name: string) => void
|
||||
): Promise<FilePreviewResponse | undefined> => {
|
||||
if (!deviceId || !tempDirectoryPath) {
|
||||
return
|
||||
}
|
||||
const entity = selectEntityData(store.getState(), {
|
||||
deviceId,
|
||||
entitiesType,
|
||||
entityId,
|
||||
})
|
||||
|
||||
if (!entity || !("filePath" in entity) || !("fileName" in entity)) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileName = entity[fields.titleField] as string
|
||||
onFileNameFound?.(fileName)
|
||||
const fileType = (entity[fields.mimeTypeField] as string)
|
||||
.split("/")[0]
|
||||
.toLowerCase()
|
||||
const filePath = getFilePath(entity[fields.pathField] as string)
|
||||
if (!filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
if ((await checkPath(filePath.nativePath)).data) {
|
||||
return {
|
||||
id: entityId,
|
||||
path: filePath.safePath,
|
||||
name: fileName,
|
||||
type: fileType,
|
||||
}
|
||||
}
|
||||
await checkPath(filePath.nativeDir, true)
|
||||
|
||||
const fileData: FileWithPath = {
|
||||
id: entityId,
|
||||
path: sliceMtpPaths(
|
||||
entity[fields.pathField] as string,
|
||||
entity.isInternal as boolean
|
||||
),
|
||||
name: String(entity[fields.titleField]),
|
||||
size: Number(entity[fields.sizeField]),
|
||||
groupId: actionId,
|
||||
}
|
||||
|
||||
const promise = dispatch(
|
||||
sendFiles({
|
||||
files: [fileData],
|
||||
destinationPath: filePath.nativeDir,
|
||||
actionId,
|
||||
entitiesType,
|
||||
isMtpPathInternal: isMtpPathInternal(
|
||||
entity[fields.pathField] as string
|
||||
),
|
||||
actionType: SendFilesAction.ActionExport,
|
||||
})
|
||||
) as unknown as ReturnType<ReturnType<typeof sendFiles>>
|
||||
|
||||
abortReferences.current[entityId] = (
|
||||
promise as unknown as {
|
||||
abort: VoidFunction
|
||||
}
|
||||
).abort
|
||||
|
||||
const response = await promise
|
||||
|
||||
if (response.meta.requestStatus === "rejected") {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
path: filePath.safePath,
|
||||
name: fileName,
|
||||
type: fileType,
|
||||
}
|
||||
},
|
||||
[
|
||||
actionId,
|
||||
deviceId,
|
||||
dispatch,
|
||||
entitiesType,
|
||||
fields.mimeTypeField,
|
||||
fields.pathField,
|
||||
fields.sizeField,
|
||||
fields.titleField,
|
||||
getFilePath,
|
||||
store,
|
||||
tempDirectoryPath,
|
||||
]
|
||||
)
|
||||
|
||||
const removePreviewFile = useCallback(
|
||||
async (entityId: string) => {
|
||||
if (!deviceId || !tempDirectoryPath) {
|
||||
return
|
||||
}
|
||||
const entity = selectEntityData(store.getState(), {
|
||||
deviceId,
|
||||
entitiesType,
|
||||
entityId,
|
||||
})
|
||||
if (!entity) {
|
||||
return
|
||||
}
|
||||
const path = getFilePath(entity[fields.pathField] as string)?.nativePath
|
||||
if (path) {
|
||||
await removeDirectory(path)
|
||||
}
|
||||
},
|
||||
[
|
||||
deviceId,
|
||||
entitiesType,
|
||||
fields.pathField,
|
||||
getFilePath,
|
||||
store,
|
||||
tempDirectoryPath,
|
||||
]
|
||||
)
|
||||
|
||||
const cancelDownload = useCallback(
|
||||
async (entityId: string) => {
|
||||
if (abortReferences.current[entityId]) {
|
||||
abortReferences.current[entityId]()
|
||||
delete abortReferences.current[entityId]
|
||||
}
|
||||
await removePreviewFile(entityId)
|
||||
},
|
||||
[removePreviewFile]
|
||||
)
|
||||
|
||||
return {
|
||||
downloadFile,
|
||||
cancelDownload,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { useDispatch } from "react-redux"
|
||||
import { sendFilesClear } from "generic-view/store"
|
||||
import { AppDispatch } from "Core/__deprecated__/renderer/store"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { checkPath, getAppPath, removeDirectory } from "system-utils/feature"
|
||||
import { uniqBy } from "lodash"
|
||||
import {
|
||||
FilePreviewResponse,
|
||||
useFilePreviewDownload,
|
||||
UseFilePreviewDownloadParams,
|
||||
} from "./use-file-preview-download"
|
||||
import { Queue } from "shared/utils"
|
||||
|
||||
export enum QueuePriority {
|
||||
Current = 2,
|
||||
Next = 1,
|
||||
Prev = 0,
|
||||
}
|
||||
|
||||
export interface UseFilesPreviewParams {
|
||||
items: string[]
|
||||
activeItem?: string
|
||||
entitiesConfig: UseFilePreviewDownloadParams["fields"] & {
|
||||
type: string
|
||||
}
|
||||
}
|
||||
|
||||
export const useFilesPreview = ({
|
||||
items,
|
||||
activeItem: activeItem,
|
||||
entitiesConfig,
|
||||
}: UseFilesPreviewParams) => {
|
||||
const actionId = entitiesConfig.type + "Preview"
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const [tempDirectoryPath, setTempDirectoryPath] = useState<string>()
|
||||
const [downloadedItems, setDownloadedItems] = useState<FilePreviewResponse[]>(
|
||||
[]
|
||||
)
|
||||
const downloadedItemsDependency = JSON.stringify(
|
||||
downloadedItems.map((item) => item.id)
|
||||
)
|
||||
|
||||
const { downloadFile, cancelDownload } = useFilePreviewDownload({
|
||||
actionId,
|
||||
tempDirectoryPath,
|
||||
entitiesType: entitiesConfig.type,
|
||||
fields: entitiesConfig,
|
||||
})
|
||||
|
||||
const queue = useMemo(() => {
|
||||
return new Queue({ interval: 100, capacity: 3 })
|
||||
}, [])
|
||||
|
||||
const getNearestFiles = useCallback(
|
||||
(item?: string) => {
|
||||
if (!item || !items.length) {
|
||||
return { previous: undefined, next: undefined }
|
||||
}
|
||||
const currentIndex = items.indexOf(item)
|
||||
return {
|
||||
previous: items[(currentIndex - 1 + items.length) % items.length],
|
||||
next: items[(currentIndex + 1) % items.length],
|
||||
}
|
||||
},
|
||||
[items]
|
||||
)
|
||||
|
||||
const nearestFiles = useMemo(() => {
|
||||
return getNearestFiles(activeItem)
|
||||
}, [activeItem, getNearestFiles])
|
||||
|
||||
const currentFile = useMemo(() => {
|
||||
if (!activeItem) return undefined
|
||||
return downloadedItems.find((item) => item.id === activeItem)
|
||||
// eslint-disable-next-line
|
||||
}, [activeItem, downloadedItemsDependency])
|
||||
|
||||
const getFilePriority = useCallback(
|
||||
(fileId: string) => {
|
||||
if (fileId === activeItem) {
|
||||
return QueuePriority.Current
|
||||
}
|
||||
const nearest = getNearestFiles(activeItem)
|
||||
if (fileId === nearest.next) {
|
||||
return QueuePriority.Next
|
||||
}
|
||||
return QueuePriority.Prev
|
||||
},
|
||||
[activeItem, getNearestFiles]
|
||||
)
|
||||
|
||||
const downloadFiles = useCallback(
|
||||
(filesIds: string[]) => {
|
||||
for (const fileId of filesIds) {
|
||||
void queue.add({
|
||||
id: fileId,
|
||||
task: async () => {
|
||||
const fileInfo = await downloadFile(fileId)
|
||||
if (fileInfo) {
|
||||
setDownloadedItems((prev) => uniqBy([...prev, fileInfo], "id"))
|
||||
}
|
||||
},
|
||||
priority: getFilePriority(fileId),
|
||||
})
|
||||
}
|
||||
},
|
||||
[downloadFile, getFilePriority, queue]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentFile?.id) return
|
||||
|
||||
const filesToRemove = downloadedItems.filter((item) => {
|
||||
return (
|
||||
item.id !== currentFile.id &&
|
||||
item.id !== nearestFiles.next &&
|
||||
item.id !== nearestFiles.previous
|
||||
)
|
||||
})
|
||||
|
||||
if (filesToRemove.length > 0) {
|
||||
for (const file of filesToRemove) {
|
||||
void cancelDownload(file.id)
|
||||
setDownloadedItems((prev) => prev.filter((item) => item.id !== file.id))
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [
|
||||
currentFile?.id,
|
||||
cancelDownload,
|
||||
downloadedItemsDependency,
|
||||
nearestFiles.next,
|
||||
nearestFiles.previous,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeItem) return
|
||||
|
||||
const filesToDownload = [
|
||||
activeItem,
|
||||
nearestFiles.next,
|
||||
nearestFiles.previous,
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
downloadFiles(filesToDownload)
|
||||
}, [activeItem, nearestFiles.next, nearestFiles.previous, downloadFiles])
|
||||
|
||||
const ensureTempDirectory = useCallback(async () => {
|
||||
const destinationPath = await getAppPath("filePreview")
|
||||
if (destinationPath.ok) {
|
||||
await checkPath(destinationPath.data, true)
|
||||
setTempDirectoryPath(destinationPath.data)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearTempDirectory = useCallback(async () => {
|
||||
dispatch(sendFilesClear({ groupId: actionId }))
|
||||
if (tempDirectoryPath) {
|
||||
await removeDirectory(tempDirectoryPath)
|
||||
}
|
||||
}, [actionId, dispatch, tempDirectoryPath])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeItem) {
|
||||
void clearTempDirectory()
|
||||
setDownloadedItems([])
|
||||
}
|
||||
}, [activeItem, clearTempDirectory])
|
||||
|
||||
useEffect(() => {
|
||||
void ensureTempDirectory()
|
||||
return () => {
|
||||
queue.clear()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return {
|
||||
data: currentFile,
|
||||
nextId: nearestFiles.next,
|
||||
previousId: nearestFiles.previous,
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,13 @@ export const SegmentBar: APIFC<SegmentBarData, SegmentBarConfig> = ({
|
||||
}, [segments, containerWidth])
|
||||
|
||||
return (
|
||||
<Wrapper data-tooltip-boundary ref={ref} width={"100%"} height={"14px"} {...props}>
|
||||
<Wrapper
|
||||
data-tooltip-boundary
|
||||
ref={ref}
|
||||
width={"100%"}
|
||||
height={"14px"}
|
||||
{...props}
|
||||
>
|
||||
{computedSegments.map((segment, index) => (
|
||||
<SegmentBarItem key={index} {...segment} isFirst={index === 0} />
|
||||
))}
|
||||
@@ -57,6 +63,7 @@ const Wrapper = styled.div<{
|
||||
height: string
|
||||
}>`
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: ${(props) => props.width};
|
||||
height: ${(props) => props.height};
|
||||
`
|
||||
|
||||
@@ -13,7 +13,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import styled from "styled-components"
|
||||
import styled, { css } from "styled-components"
|
||||
import { difference, intersection } from "lodash"
|
||||
import { TableTestIds } from "e2e-test-ids"
|
||||
import { APIFC, useViewFormContext } from "generic-view/utils"
|
||||
@@ -28,32 +28,47 @@ import {
|
||||
listRawItemStyles,
|
||||
} from "../list/list-item"
|
||||
import { toastAnimationDuration } from "../interactive/toast/toast"
|
||||
import { FilePreview } from "../predefined/file-preview/file-preview"
|
||||
|
||||
const rowHeight = 64
|
||||
|
||||
export const Table: APIFC<TableData, TableConfig> & {
|
||||
Cell: typeof TableCell
|
||||
HeaderCell: typeof TableCell
|
||||
} = ({ data = [], config, children, ...props }) => {
|
||||
} = ({
|
||||
data = [],
|
||||
config: { formOptions, previewOptions },
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const getFormContext = useViewFormContext()
|
||||
const formContext = getFormContext(config.formOptions.formKey)
|
||||
const formContext = getFormContext(formOptions.formKey)
|
||||
const scrollWrapperRef = useRef<HTMLDivElement>(null)
|
||||
const [visibleRowsBounds, setVisibleRowsBounds] = useState<[number, number]>([
|
||||
-1, -1,
|
||||
])
|
||||
|
||||
const { formOptions } = config
|
||||
const { activeIdFieldName } = formOptions
|
||||
const [activePreviewId, setActivePreviewId] = useState<string>()
|
||||
const nextActivePreviewId = useRef<string | undefined>(undefined)
|
||||
const { activeIdFieldName } = formOptions || {}
|
||||
const isClickable = Boolean(activeIdFieldName)
|
||||
|
||||
const activeRowId = activeIdFieldName
|
||||
? formContext.watch(activeIdFieldName)
|
||||
: undefined
|
||||
|
||||
const previewMode = previewOptions?.enabled
|
||||
? formContext.watch("previewMode")
|
||||
: undefined
|
||||
|
||||
const onRowClick = useCallback(
|
||||
(id: string) => {
|
||||
if (!activeIdFieldName) return
|
||||
formContext.setValue(activeIdFieldName!, id)
|
||||
if (previewOptions?.enabled) {
|
||||
handleActivePreviewIdChange(id)
|
||||
return
|
||||
}
|
||||
if (activeIdFieldName) {
|
||||
formContext.setValue(activeIdFieldName!, id)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[activeIdFieldName]
|
||||
@@ -82,6 +97,52 @@ export const Table: APIFC<TableData, TableConfig> & {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const scrollToPreviewActiveItem = useCallback(() => {
|
||||
const activeElement = scrollWrapperRef.current?.querySelector(
|
||||
`tr[data-item-id="${activePreviewId}"]`
|
||||
)
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({
|
||||
block: "nearest",
|
||||
})
|
||||
}
|
||||
}, [activePreviewId])
|
||||
|
||||
const handleActivePreviewIdChange = useCallback(
|
||||
(id?: string) => {
|
||||
setActivePreviewId(id)
|
||||
if (id) {
|
||||
nextActivePreviewId.current = data[(data.indexOf(id) + 1) % data.length]
|
||||
}
|
||||
},
|
||||
[data]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (activePreviewId !== undefined) {
|
||||
scrollToPreviewActiveItem()
|
||||
formContext.setValue("previewMode", true)
|
||||
} else if (previewMode) {
|
||||
formContext.setValue("previewMode", false)
|
||||
nextActivePreviewId.current = undefined
|
||||
if (formOptions.selectedIdsFieldName) {
|
||||
formContext.setValue(formOptions.selectedIdsFieldName, [])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
activePreviewId,
|
||||
formOptions.selectedIdsFieldName,
|
||||
previewMode,
|
||||
scrollToPreviewActiveItem,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (activePreviewId && !data.includes(activePreviewId)) {
|
||||
handleActivePreviewIdChange(nextActivePreviewId.current)
|
||||
}
|
||||
}, [activePreviewId, data, handleActivePreviewIdChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeRowId) {
|
||||
scrollToActiveItem()
|
||||
@@ -144,6 +205,7 @@ export const Table: APIFC<TableData, TableConfig> & {
|
||||
return (
|
||||
<RowPlaceholder
|
||||
key={id}
|
||||
data-item-id={id}
|
||||
data-testid={TableTestIds.TablePlaceholderRow}
|
||||
className={isActive ? "active" : ""}
|
||||
>
|
||||
@@ -200,6 +262,7 @@ export const Table: APIFC<TableData, TableConfig> & {
|
||||
return (
|
||||
<tr
|
||||
key={id}
|
||||
data-item-id={id}
|
||||
data-testid={TableTestIds.TableRow}
|
||||
onClick={onClick}
|
||||
className={isActive ? "active" : ""}
|
||||
@@ -217,22 +280,56 @@ export const Table: APIFC<TableData, TableConfig> & {
|
||||
]
|
||||
)
|
||||
|
||||
const preview = useMemo(() => {
|
||||
if (!previewOptions || !previewOptions.enabled) return null
|
||||
|
||||
return (
|
||||
<FilePreview
|
||||
items={data}
|
||||
activeItem={activePreviewId}
|
||||
onActiveItemChange={handleActivePreviewIdChange}
|
||||
componentKey={previewOptions.componentKey}
|
||||
entitiesConfig={{
|
||||
type: previewOptions.entitiesType,
|
||||
idField: previewOptions.entityIdFieldName,
|
||||
pathField: previewOptions.entityPathFieldName,
|
||||
titleField: previewOptions.entityTitleFieldName,
|
||||
mimeTypeField: previewOptions.entityMimeTypeFieldName,
|
||||
sizeField: previewOptions.entitySizeFieldName,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, [activePreviewId, data, handleActivePreviewIdChange, previewOptions])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<ScrollableWrapper ref={scrollWrapperRef} {...props}>
|
||||
<TableWrapper data-testid={TableTestIds.Table}>
|
||||
<TableHeader>
|
||||
<tr data-testid={TableTestIds.TableHeaderRow}>
|
||||
{renderHeaderChildren()}
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody $clickable={isClickable}>
|
||||
{data?.map((id, index) => renderRow(id, index))}
|
||||
</TableBody>
|
||||
</TableWrapper>
|
||||
</ScrollableWrapper>
|
||||
<>
|
||||
<ScrollableWrapper ref={scrollWrapperRef} {...props}>
|
||||
<TableWrapper data-testid={TableTestIds.Table}>
|
||||
<TableHeader
|
||||
$hasClickableRows={isClickable || previewOptions?.enabled}
|
||||
>
|
||||
<tr data-testid={TableTestIds.TableHeaderRow}>
|
||||
{renderHeaderChildren()}
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody $clickable={isClickable || previewOptions?.enabled}>
|
||||
{data?.map((id, index) => renderRow(id, index))}
|
||||
</TableBody>
|
||||
</TableWrapper>
|
||||
</ScrollableWrapper>
|
||||
{preview}
|
||||
</>
|
||||
),
|
||||
[data, isClickable, props, renderRow, renderHeaderChildren]
|
||||
[
|
||||
props,
|
||||
isClickable,
|
||||
previewOptions?.enabled,
|
||||
renderHeaderChildren,
|
||||
data,
|
||||
preview,
|
||||
renderRow,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -256,12 +353,22 @@ const TableWrapper = styled.table`
|
||||
border-spacing: 0;
|
||||
`
|
||||
|
||||
const TableHeader = styled.thead`
|
||||
const TableHeader = styled.thead<{ $hasClickableRows?: boolean }>`
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
|
||||
tr {
|
||||
${({ $hasClickableRows }) =>
|
||||
$hasClickableRows &&
|
||||
css`
|
||||
&:before {
|
||||
content: "";
|
||||
}
|
||||
`};
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
|
||||
export enum IconType {
|
||||
ArrowLeft = "arrow-left",
|
||||
ArrowRight = "arrow-right",
|
||||
Battery0 = "battery-0",
|
||||
Battery1 = "battery-1",
|
||||
Battery2 = "battery-2",
|
||||
|
||||
@@ -23,3 +23,4 @@ export * from "./lib/exec-command-with-sudo"
|
||||
export * from "./lib/use-filtered-routes-history"
|
||||
export * from "./lib/slice-segments"
|
||||
export * from "./lib/use-dev-console-mock"
|
||||
export * from "./lib/queue/queue"
|
||||
|
||||
346
libs/shared/utils/src/lib/queue/queue.test.ts
Normal file
346
libs/shared/utils/src/lib/queue/queue.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { Queue } from "./queue"
|
||||
|
||||
const delayTask = () => new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
describe("Queue E2E Tests", () => {
|
||||
let queue: Queue
|
||||
|
||||
beforeEach(() => {
|
||||
queue = new Queue({ interval: 100 })
|
||||
})
|
||||
|
||||
it("should process items in the order they were added", async () => {
|
||||
const results: string[] = []
|
||||
|
||||
const p1 = queue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
},
|
||||
})
|
||||
|
||||
const p2 = queue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
})
|
||||
|
||||
const p3 = queue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task3")
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.all([p1, p2, p3])
|
||||
expect(results).toEqual(["task1", "task2", "task3"])
|
||||
|
||||
const p4 = queue.add({
|
||||
id: "task4",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task4")
|
||||
},
|
||||
})
|
||||
|
||||
await p4
|
||||
expect(results).toEqual(["task1", "task2", "task3", "task4"])
|
||||
})
|
||||
|
||||
it("should process items in the order of priorities", async () => {
|
||||
const results: string[] = []
|
||||
|
||||
const p1 = queue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
},
|
||||
})
|
||||
|
||||
const p2 = queue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
})
|
||||
|
||||
const p3 = queue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task3")
|
||||
},
|
||||
priority: 2,
|
||||
})
|
||||
|
||||
await Promise.all([p1, p2, p3])
|
||||
|
||||
const p4 = queue.add({
|
||||
id: "task4",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task4")
|
||||
},
|
||||
priority: 3,
|
||||
})
|
||||
|
||||
await p4
|
||||
|
||||
expect(results).toEqual(["task3", "task1", "task2", "task4"])
|
||||
})
|
||||
|
||||
it("should override only item's priority when adding an item with same id", async () => {
|
||||
const results: string[] = []
|
||||
|
||||
const p1 = queue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
},
|
||||
})
|
||||
|
||||
const p2 = queue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
priority: 2,
|
||||
})
|
||||
|
||||
const p3 = queue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task3")
|
||||
},
|
||||
priority: 1,
|
||||
})
|
||||
|
||||
const p4 = queue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task3 (updated)")
|
||||
},
|
||||
priority: 3,
|
||||
})
|
||||
|
||||
const [_r1, _r2, r3, r4] = await Promise.all([p1, p2, p3, p4])
|
||||
|
||||
expect(results).toEqual(["task3", "task2", "task1"])
|
||||
expect(r3.priority).toEqual(3)
|
||||
expect(r4.priority).toEqual(3)
|
||||
})
|
||||
|
||||
it("should continue processing after certain task failure", async () => {
|
||||
const results: string[] = []
|
||||
|
||||
const p1 = queue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
},
|
||||
})
|
||||
|
||||
const p2 = queue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
})
|
||||
|
||||
const p3 = queue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
await Promise.reject("Failure in task3")
|
||||
results.push("task3")
|
||||
},
|
||||
})
|
||||
|
||||
const p4 = queue.add({
|
||||
id: "task4",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task4")
|
||||
},
|
||||
})
|
||||
|
||||
const [_r1, _r2, r3] = await Promise.all([p1, p2, p3, p4])
|
||||
|
||||
expect(results).toEqual(["task1", "task2", "task4"])
|
||||
expect(r3.reason).toEqual(new Error("Failure in task3"))
|
||||
})
|
||||
|
||||
it("should handle removed items correctly", async () => {
|
||||
const results: string[] = []
|
||||
|
||||
const p1 = queue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
},
|
||||
})
|
||||
|
||||
const p2 = queue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
})
|
||||
|
||||
const p3 = queue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task3")
|
||||
},
|
||||
})
|
||||
|
||||
const p4 = queue.add({
|
||||
id: "task4",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task4")
|
||||
},
|
||||
})
|
||||
|
||||
queue.remove("task1")
|
||||
queue.remove("task3")
|
||||
|
||||
const p5 = queue.add({
|
||||
id: "task5",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task5")
|
||||
},
|
||||
})
|
||||
|
||||
const [r1, _r2, r3] = await Promise.all([p1, p2, p3, p4, p5])
|
||||
|
||||
expect(results).toEqual(["task2", "task4", "task5"])
|
||||
expect(r1.reason).toEqual("removed")
|
||||
expect(r3.reason).toEqual("removed")
|
||||
})
|
||||
|
||||
it("should clear the queue after calling the `clear` method and finish processing tasks", async () => {
|
||||
const results: string[] = []
|
||||
|
||||
const p1 = queue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
},
|
||||
})
|
||||
|
||||
const p2 = queue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
})
|
||||
|
||||
queue.clear()
|
||||
|
||||
const [r1, r2] = await Promise.all([p1, p2])
|
||||
expect(results).toEqual([])
|
||||
expect(r1.reason).toEqual("removed")
|
||||
expect(r2.reason).toEqual("removed")
|
||||
})
|
||||
|
||||
it("should handle custom interval properly", async () => {
|
||||
const results: string[] = []
|
||||
const customQueue = new Queue({ interval: 1000 })
|
||||
|
||||
const timestamps: number[] = []
|
||||
|
||||
const p1 = customQueue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
await delayTask()
|
||||
results.push("task1")
|
||||
timestamps.push(performance.now())
|
||||
},
|
||||
})
|
||||
const p2 = customQueue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
timestamps.push(performance.now())
|
||||
await delayTask()
|
||||
results.push("task2")
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.all([p1, p2])
|
||||
expect(results).toEqual(["task1", "task2"])
|
||||
expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(999)
|
||||
})
|
||||
|
||||
it("should enforce capacity correctly", async () => {
|
||||
const results: string[] = []
|
||||
const capQueue = new Queue({ interval: 0, capacity: 3 })
|
||||
|
||||
const p1 = capQueue.add({
|
||||
id: "task1",
|
||||
task: async () => {
|
||||
results.push("task1")
|
||||
},
|
||||
priority: 1,
|
||||
})
|
||||
const p2 = capQueue.add({
|
||||
id: "task2",
|
||||
task: async () => {
|
||||
results.push("task2")
|
||||
},
|
||||
priority: 2,
|
||||
})
|
||||
const p3 = capQueue.add({
|
||||
id: "task3",
|
||||
task: async () => {
|
||||
results.push("task3")
|
||||
},
|
||||
priority: 3,
|
||||
})
|
||||
const p4 = capQueue.add({
|
||||
id: "task4",
|
||||
task: async () => {
|
||||
results.push("task4")
|
||||
},
|
||||
priority: 4,
|
||||
})
|
||||
const p5 = capQueue.add({
|
||||
id: "task5",
|
||||
task: async () => {
|
||||
results.push("task5")
|
||||
},
|
||||
priority: 5,
|
||||
})
|
||||
|
||||
const [r1, r2, r3, r4, r5] = await Promise.all([p1, p2, p3, p4, p5])
|
||||
expect(results).toEqual(["task5", "task4", "task3"])
|
||||
expect(r1.reason).toEqual("removed")
|
||||
expect(r2.reason).toEqual("removed")
|
||||
expect(r3.reason).toBeUndefined()
|
||||
expect(r3.priority).toEqual(3)
|
||||
expect(r4.priority).toEqual(4)
|
||||
expect(r5.priority).toEqual(5)
|
||||
})
|
||||
})
|
||||
179
libs/shared/utils/src/lib/queue/queue.ts
Normal file
179
libs/shared/utils/src/lib/queue/queue.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Copyright (c) Mudita sp. z o.o. All rights reserved.
|
||||
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events"
|
||||
|
||||
enum ItemEvent {
|
||||
Finished = "finished",
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: string
|
||||
task: (task: Omit<Item, "task" | "abortController">) => Promise<void>
|
||||
priority: number
|
||||
abortController: AbortController
|
||||
}
|
||||
|
||||
type ResponseItem = Omit<Item, "abortController"> & {
|
||||
reason?: "removed" | "aborted" | Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue class that manages a list of tasks to be processed in order of priority.
|
||||
* It allows adding, removing, and processing tasks with a specified interval.
|
||||
* Each task can be aborted, and the queue can handle a maximum capacity.
|
||||
*/
|
||||
export class Queue {
|
||||
private queue: Item[] = []
|
||||
private eventEmitter = new EventEmitter()
|
||||
private currentItem?: Item
|
||||
private processing = false
|
||||
private scheduledStart = false
|
||||
private readonly interval: number
|
||||
private readonly capacity?: number
|
||||
|
||||
/**
|
||||
* Creates a new Queue instance.
|
||||
* @param options - Configuration options for the queue.
|
||||
* @param options.interval - Time in milliseconds to wait between processing next tasks. Defaults to 0.
|
||||
* @param options.capacity - Maximum number of items in the queue. If exceeded, the lowest-priority items will be aborted and removed. Defaults to no limit.
|
||||
*/
|
||||
constructor(
|
||||
options: {
|
||||
interval?: number
|
||||
capacity?: number
|
||||
} = {}
|
||||
) {
|
||||
this.interval = options.interval || 0
|
||||
this.capacity = options.capacity
|
||||
}
|
||||
|
||||
private async processQueue() {
|
||||
this.processing = true
|
||||
while (this.queue.length > 0) {
|
||||
const item = this.queue.shift()!
|
||||
this.currentItem = item
|
||||
const { signal } = item.abortController
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
if (signal.aborted) continue
|
||||
try {
|
||||
await item.task({ id: item.id, priority: item.priority })
|
||||
this.eventEmitter.emit(ItemEvent.Finished, item.id)
|
||||
} catch (error) {
|
||||
item.abortController.abort(
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
)
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
return setTimeout(resolve, this.interval)
|
||||
})
|
||||
}
|
||||
this.currentItem = undefined
|
||||
this.processing = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new item to the queue or updates an existing one.
|
||||
* If the item already exists, it updates its priority and reorders the queue.
|
||||
* @param newItem - The item to add or update in the queue.
|
||||
* @param newItem.id - Unique identifier for the item.
|
||||
* @param newItem.task - The task function to be executed, which should return a promise.
|
||||
* @param newItem.priority - The priority of the item, where higher values indicate higher priority. Defaults to 0 if not provided.
|
||||
* @returns A promise that resolves to an object containing the item's id, task, priority, and any reason for abortion if applicable.
|
||||
*/
|
||||
async add(
|
||||
newItem: Pick<Item, "id" | "task"> & Partial<Pick<Item, "priority">>
|
||||
) {
|
||||
const existingItem = this.queue.find((i) => i.id === newItem.id)
|
||||
const abortController =
|
||||
existingItem?.abortController || new AbortController()
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.priority = newItem.priority ?? existingItem.priority
|
||||
this.queue.sort((a, b) => b.priority - a.priority)
|
||||
return {
|
||||
id: existingItem.id,
|
||||
task: existingItem.task,
|
||||
priority: existingItem.priority,
|
||||
}
|
||||
}
|
||||
const item: Item = {
|
||||
id: newItem.id,
|
||||
task: async (props) => {
|
||||
await newItem.task(props)
|
||||
},
|
||||
priority: newItem.priority ?? 0,
|
||||
abortController,
|
||||
}
|
||||
this.queue.push(item)
|
||||
|
||||
this.queue.sort((a, b) => b.priority - a.priority)
|
||||
if (this.capacity !== undefined) {
|
||||
const overflow = this.queue.length - this.capacity
|
||||
for (let i = 0; i < overflow; i++) {
|
||||
const removed = this.queue.pop()!
|
||||
removed.abortController.abort("removed")
|
||||
}
|
||||
}
|
||||
if (!this.processing && !this.scheduledStart) {
|
||||
this.scheduledStart = true
|
||||
setTimeout(() => {
|
||||
this.scheduledStart = false
|
||||
this.processQueue()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return new Promise<ResponseItem>((resolve) => {
|
||||
abortController.signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
const { id, task, priority } = item
|
||||
resolve({ id, task, priority, reason: abortController.signal.reason })
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
const onFinished = (finishedId: Item["id"]) => {
|
||||
if (finishedId === item.id) {
|
||||
this.eventEmitter.off(ItemEvent.Finished, onFinished)
|
||||
const { id, task, priority } = item
|
||||
resolve({ id, task, priority })
|
||||
}
|
||||
}
|
||||
this.eventEmitter.on(ItemEvent.Finished, onFinished)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the queue by its ID.
|
||||
* If the item is currently being processed, it will be aborted.
|
||||
* @param itemId
|
||||
*/
|
||||
remove(itemId: Item["id"]) {
|
||||
if (this.currentItem?.id === itemId) {
|
||||
this.currentItem.abortController.abort("removed")
|
||||
} else {
|
||||
this.queue = this.queue.filter((item) => {
|
||||
if (item.id === itemId) {
|
||||
item.abortController.abort("removed")
|
||||
return false
|
||||
}
|
||||
return item.id !== itemId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the queue and aborts all currently processing items.
|
||||
* This will stop any ongoing tasks and remove all items from the queue.
|
||||
*/
|
||||
clear() {
|
||||
for (const item of [this.currentItem, ...this.queue]) {
|
||||
item?.abortController.abort("removed")
|
||||
}
|
||||
this.queue = []
|
||||
this.currentItem = undefined
|
||||
this.processing = false
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ const appPathType = {
|
||||
pureBackups: ["pure", "phone", "backups"],
|
||||
pureBackupTemp: ["pure", "phone", "backup-temp"],
|
||||
helpV2: ["help-v2.json"],
|
||||
filePreview: ["file-preview"],
|
||||
} as const
|
||||
|
||||
export type AppPathType = keyof typeof appPathType
|
||||
|
||||
170
package-lock.json
generated
170
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"js-crc": "^0.2.0",
|
||||
"jschardet": "^3.1.2",
|
||||
"motion": "^12.23.10",
|
||||
"node-ipc": "10.1.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"react-is": "18.2.0",
|
||||
@@ -2975,7 +2976,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
|
||||
"integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.8.1"
|
||||
}
|
||||
@@ -2984,7 +2985,7 @@
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
|
||||
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.11.1",
|
||||
@@ -15778,10 +15779,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
||||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"possible-typed-array-names": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -18877,16 +18881,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.0",
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -18908,6 +18911,22 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -25749,12 +25768,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-callable": "^1.1.3"
|
||||
"is-callable": "^1.2.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/for-in": {
|
||||
@@ -25950,6 +25975,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.10",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.10.tgz",
|
||||
"integrity": "sha512-ziXHr+C91FhgdSV65YA9SNbLy7uIDQ0pq7pEWlMP6Bh9UJAHFUNUvKWzE41g1B7YuvgJtUUNLgNmZyHd/YQ2gA==",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.9",
|
||||
"motion-utils": "^12.23.6",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -29056,12 +29107,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-typed-array": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
|
||||
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"which-typed-array": "^1.1.11"
|
||||
"which-typed-array": "^1.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -36136,6 +36187,44 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/motion": {
|
||||
"version": "12.23.10",
|
||||
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.10.tgz",
|
||||
"integrity": "sha512-pE3WsRXbRjVQaB2vCmVLt8gYiZdX11KYIoWblc49JnG2dTn6oHobIw+bORL31ZfiYBZD8BYmqkvEZp+HLPBBVw==",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.23.10",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.9",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.9.tgz",
|
||||
"integrity": "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.23.6"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.23.6",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -38262,6 +38351,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
||||
@@ -39493,7 +39591,7 @@
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
@@ -41741,7 +41839,7 @@
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
@@ -45070,14 +45168,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
|
||||
"integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"get-intrinsic": "^1.2.1",
|
||||
"is-typed-array": "^1.1.10"
|
||||
"call-bound": "^1.0.3",
|
||||
"es-errors": "^1.3.0",
|
||||
"is-typed-array": "^1.1.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -47258,16 +47356,18 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
|
||||
"integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"available-typed-arrays": "^1.0.5",
|
||||
"call-bind": "^1.0.4",
|
||||
"for-each": "^0.3.3",
|
||||
"gopd": "^1.0.1",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
"available-typed-arrays": "^1.0.7",
|
||||
"call-bind": "^1.0.8",
|
||||
"call-bound": "^1.0.4",
|
||||
"for-each": "^0.3.5",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-tostringtag": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
||||
@@ -290,6 +290,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"js-crc": "^0.2.0",
|
||||
"jschardet": "^3.1.2",
|
||||
"motion": "^12.23.10",
|
||||
"node-ipc": "10.1.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"react-is": "18.2.0",
|
||||
|
||||
Reference in New Issue
Block a user