diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap index 3f5b9c37eb3..002c1beefb9 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap @@ -58,6 +58,7 @@ exports[`CopyView display text values should match snapshot values 1`] = ` "addFilesLabel": "Add files", "addFolderLabel": "Add folder", "getActionCompleteMessage": [Function], + "getFilesValidationMessage": [Function], "overwriteToggleLabel": "Overwrite existing files", "statusDisplayCanceledLabel": "Canceled", "statusDisplayCompletedLabel": "Completed", diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts index 639e22003c3..cba6ae48564 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts @@ -1,5 +1,6 @@ import { DEFAULT_ACTION_VIEW_DISPLAY_TEXT } from './shared'; import { DefaultUploadViewDisplayText } from '../../types'; +import { UPLOAD_FILE_SIZE_LIMIT } from '../../../views/LocationActionView/constants'; export const DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT: DefaultUploadViewDisplayText = { ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT, @@ -79,6 +80,19 @@ export const DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT: DefaultUploadViewDisplayText = { return { content: 'All files uploaded.', type }; }, + getFilesValidationMessage: ({ invalidFiles } = {}) => { + if (!invalidFiles?.length) { + return undefined; + } + const fileNames = invalidFiles + .filter(({ file }) => file.size > UPLOAD_FILE_SIZE_LIMIT) + .map(({ file }) => file.name) + .join(', '); + return { + content: `These files cannot be added to the upload queue due to they are larger than 160GB respectively: ${fileNames}`, + type: 'warning', + }; + }, statusDisplayOverwritePreventedLabel: 'Overwrite prevented', overwriteToggleLabel: 'Overwrite existing files', }; diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/types.ts b/packages/react-storage/src/components/StorageBrowser/displayText/types.ts index 9522acb0536..dfcead979a0 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/types.ts @@ -1,6 +1,7 @@ import { StatusCounts, Tasks } from '../tasks'; import { CopyHandlerData, + CreateFolderHandlerData, DeleteHandlerData, FolderData, LocationData, @@ -11,7 +12,7 @@ import { } from '../actions'; import { LocationState } from '../providers/store/location'; import { MessageType } from '../composables/Message'; -import { CreateFolderHandlerData } from '../actions'; +import { FileItems } from '../providers'; /** * Common list view display text values @@ -136,6 +137,9 @@ export interface DefaultUploadViewDisplayText addFolderLabel: string; statusDisplayOverwritePreventedLabel: string; overwriteToggleLabel: string; + getFilesValidationMessage: (data?: { + invalidFiles?: FileItems; + }) => { content?: string; type?: MessageType } | undefined; } export interface DefaultStorageBrowserDisplayText { diff --git a/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/context.spec.ts b/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/context.spec.ts index d85ec8ce08e..7689355318d 100644 --- a/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/context.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/context.spec.ts @@ -2,6 +2,7 @@ import { act, renderHook } from '@testing-library/react'; import { FilesProvider, useFiles } from '../context'; import * as UIReactModule from '@aws-amplify/ui-react/internal'; +import { UPLOAD_FILE_SIZE_LIMIT } from '../../../../views/LocationActionView/constants'; let uuid = 0; Object.defineProperty(globalThis, 'crypto', { @@ -20,7 +21,7 @@ describe('useFiles', () => { const { result } = renderHook(() => useFiles(), { wrapper: FilesProvider }); const [state, handler] = result.current; - expect(state).toStrictEqual([]); + expect(state).toStrictEqual({ validFiles: [], invalidFiles: [] }); expect(typeof handler).toBe('function'); }); @@ -39,22 +40,30 @@ describe('useFiles', () => { expect(handleFileSelect).toHaveBeenCalledTimes(1); }); - it('adds files as as expected', () => { + it('adds files as expected', () => { const fileOne = new File([], 'file-one'); const fileTwo = new File([], 'file-two'); + const invalidFileThree = { + ...new File([], 'file-three-invalid'), + size: UPLOAD_FILE_SIZE_LIMIT + 1, + }; const { result } = renderHook(() => useFiles(), { wrapper: FilesProvider }); const [initState, handler] = result.current; - expect(initState).toStrictEqual([]); + expect(initState).toStrictEqual({ validFiles: [], invalidFiles: [] }); act(() => { - handler({ type: 'ADD_FILE_ITEMS', files: [fileOne, fileTwo] }); + handler({ + type: 'ADD_FILE_ITEMS', + files: [fileOne, fileTwo, invalidFileThree], + }); }); const [nextState] = result.current; - expect(nextState).toHaveLength(2); + expect(nextState?.validFiles).toHaveLength(2); + expect(nextState?.invalidFiles).toHaveLength(1); }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/utils.spec.ts b/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/utils.spec.ts index 3d205e0eacc..d7e0266e0e8 100644 --- a/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/utils.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/providers/store/files/__tests__/utils.spec.ts @@ -1,4 +1,5 @@ -import { FileItems, FileItem } from '../types'; +import { UPLOAD_FILE_SIZE_LIMIT } from '../../../../views/LocationActionView/constants'; +import { FileItem } from '../types'; import { resolveFiles, filesReducer, parseFileSelectParams } from '../utils'; let uuid = 0; @@ -14,6 +15,10 @@ Object.defineProperty(globalThis, 'crypto', { const fileOne = new File([], 'file-one'); const fileTwo = new File([], 'file-two'); const fileThree = new File([], 'file-three'); +const invalidFile = { + ...new File([], 'file-invalid'), + size: UPLOAD_FILE_SIZE_LIMIT + 1, +}; const fileItemOne: FileItem = { id: 'item-one', @@ -27,17 +32,29 @@ const fileItemTwo: FileItem = { key: fileTwo.name, }; +const invalidFileItem: FileItem = { + id: 'item-invalid', + file: invalidFile, + key: invalidFile.name, +}; + describe('files context utils', () => { describe('resolveFiles', () => { it('returns the previous `items` when incoming `files` are `undefined`', () => { - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const output = resolveFiles(previous, undefined); expect(output).toBe(previous); }); it('returns the previous `items` when incoming `files` is an empty array', () => { - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const output = resolveFiles(previous, []); expect(output).toBe(previous); @@ -45,21 +62,26 @@ describe('files context utils', () => { it('returns the previous `items` when incoming `files` are all duplicates', () => { const incoming = [fileOne, fileTwo]; - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const output = resolveFiles(previous, incoming); - - expect(output).toBe(previous); + expect(output).toEqual(previous); }); it('filters incoming `files` that exist in previous `items`', () => { const incoming = [fileOne, fileTwo, fileThree]; - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const output = resolveFiles(previous, incoming); expect(output).not.toBe(previous); - expect(output).toHaveLength(3); + expect(output.validFiles).toHaveLength(3); - const newItem = output[1]; + const newItem = output.validFiles[1]; expect(newItem.file).toBe(fileThree); expect(newItem.key).toBe(fileThree.name); @@ -68,11 +90,11 @@ describe('files context utils', () => { it('returns the sorted next `items` when previous `items` are `undefined`', () => { const incoming = [fileTwo, fileOne]; - const previous: FileItems = []; + const previous = { validFiles: [], invalidFiles: [] }; const output = resolveFiles(previous, incoming); - expect(output).toHaveLength(2); - const [itemOne, itemTwo] = output; + expect(output.validFiles).toHaveLength(2); + const [itemOne, itemTwo] = output.validFiles; expect(itemOne.file).toBe(fileOne); expect(itemTwo.file).toBe(fileTwo); @@ -80,12 +102,15 @@ describe('files context utils', () => { it('merges, sorts and returns previous and next `items`', () => { const incoming = [fileThree]; - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const output = resolveFiles(previous, incoming); - expect(output).toHaveLength(3); + expect(output.validFiles).toHaveLength(3); - const [itemOne, itemTwo, itemThree] = output; + const [itemOne, itemTwo, itemThree] = output.validFiles; // fileItemOne.key === 'item-one' expect(itemOne.key).toBe(fileItemOne.key); @@ -98,61 +123,108 @@ describe('files context utils', () => { }); it('returns the webKitRelativePath as key when available', () => { - const incoming = [ - { ...fileThree, webkitRelativePath: 'test/file/file-three' }, - ]; - const previous = [fileItemOne, fileItemTwo]; - const output = resolveFiles(previous, incoming); + const newFile = new File([], 'new-file'); + Object.assign(newFile, { webkitRelativePath: 'test/file/new-file' }); + const incoming = [newFile]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; + const { validFiles: output } = resolveFiles(previous, incoming); expect(output).toHaveLength(3); - expect(output[2].key).toBe('test/file/file-three'); + expect(output[2].key).toBe('test/file/new-file'); }); }); describe('filesReducer', () => { it('adds `fileItems` as expected', () => { const incoming = [fileOne, fileTwo, fileThree]; - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const output = filesReducer(previous, { type: 'ADD_FILE_ITEMS', files: incoming, }); - expect(output).toHaveLength(3); + expect(output.validFiles).toHaveLength(3); + expect(output.invalidFiles).toHaveLength(0); + }); + + it('adds `fileItems` that is invalid', () => { + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; + const output = filesReducer(previous, { + type: 'ADD_FILE_ITEMS', + files: [invalidFile], + }); + + expect(output.validFiles).toHaveLength(2); + expect(output.invalidFiles).toHaveLength(1); }); it('removes a `fileItem` as expected', () => { - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const targetId = fileItemOne.id; - const output = filesReducer(previous, { + const { validFiles } = filesReducer(previous, { type: 'REMOVE_FILE_ITEM', id: targetId, }); - expect(output).toHaveLength(1); - expect(output[0]).toBe(fileItemTwo); + expect(validFiles).toHaveLength(1); + expect(validFiles[0]).toBe(fileItemTwo); }); it('returns the previous items on remove when previous and next items are the same length', () => { - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [], + }; const targetId = 'not a real id lol'; - const output = filesReducer(previous, { + const { validFiles: outputValidFiles } = filesReducer(previous, { type: 'REMOVE_FILE_ITEM', id: targetId, }); - expect(output).toHaveLength(2); - expect(output).toBe(previous); + expect(outputValidFiles).toHaveLength(2); + expect(outputValidFiles).toBe(previous.validFiles); }); it('resets `fileItems` as expected', () => { - const previous = [fileItemOne, fileItemTwo]; + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [invalidFileItem], + }; - const output = filesReducer(previous, { type: 'RESET_FILE_ITEMS' }); + const { validFiles, invalidFiles } = filesReducer(previous, { + type: 'RESET_FILE_ITEMS', + }); + + expect(validFiles).toHaveLength(0); + expect(invalidFiles).toHaveLength(0); + }); + + it('resets invalid `fileTimes` as expected', () => { + const previous = { + validFiles: [fileItemOne, fileItemTwo], + invalidFiles: [invalidFileItem], + }; + + const { validFiles, invalidFiles } = filesReducer(previous, { + type: 'RESET_INVALID_FILE_ITEMS', + }); - expect(output).toHaveLength(0); + expect(validFiles).toBe(previous.validFiles); + expect(invalidFiles).toHaveLength(0); }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/providers/store/files/context.tsx b/packages/react-storage/src/components/StorageBrowser/providers/store/files/context.tsx index 64c93209520..141b7ddcea6 100644 --- a/packages/react-storage/src/components/StorageBrowser/providers/store/files/context.tsx +++ b/packages/react-storage/src/components/StorageBrowser/providers/store/files/context.tsx @@ -20,7 +20,10 @@ export const { FilesContext, useFiles } = createContextUtilities({ export function FilesProvider({ children, }: FilesProviderProps): React.JSX.Element { - const [items, dispatch] = React.useReducer(filesReducer, []); + const [items, dispatch] = React.useReducer(filesReducer, { + validFiles: [], + invalidFiles: [], + }); const [fileInput, handleFileSelect] = useFileSelect((nextFiles) => { dispatch({ type: 'ADD_FILE_ITEMS', files: nextFiles }); diff --git a/packages/react-storage/src/components/StorageBrowser/providers/store/files/types.ts b/packages/react-storage/src/components/StorageBrowser/providers/store/files/types.ts index b4076f19274..bf21a765fd2 100644 --- a/packages/react-storage/src/components/StorageBrowser/providers/store/files/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/providers/store/files/types.ts @@ -4,7 +4,8 @@ export type FilesActionType = | { type: 'ADD_FILE_ITEMS'; files?: File[] } | { type: 'REMOVE_FILE_ITEM'; id: string } | { type: 'SELECT_FILES'; selectionType?: SelectionType } - | { type: 'RESET_FILE_ITEMS' }; + | { type: 'RESET_FILE_ITEMS' } + | { type: 'RESET_INVALID_FILE_ITEMS' }; export type HandleFilesAction = (input: FilesActionType) => void; @@ -14,7 +15,12 @@ export interface FileItem extends TaskData { export type FileItems = FileItem[]; -export type FilesContextType = [FileItems | undefined, HandleFilesAction]; +export interface FileItemsState { + validFiles: FileItems; + invalidFiles: FileItems; +} + +export type FilesContextType = [FileItemsState | undefined, HandleFilesAction]; export interface FilesProviderProps { children?: React.ReactNode; diff --git a/packages/react-storage/src/components/StorageBrowser/providers/store/files/utils.ts b/packages/react-storage/src/components/StorageBrowser/providers/store/files/utils.ts index f093f5f7c21..d662634bff9 100644 --- a/packages/react-storage/src/components/StorageBrowser/providers/store/files/utils.ts +++ b/packages/react-storage/src/components/StorageBrowser/providers/store/files/utils.ts @@ -4,46 +4,75 @@ import { isEmpty, isString, isUndefined } from '@aws-amplify/ui'; import { HandleFileSelect } from '@aws-amplify/ui-react/internal'; import { SelectionType } from '../../../actions/configs'; +// FIXME move to closer constant file. +import { UPLOAD_FILE_SIZE_LIMIT } from '../../../views/LocationActionView/constants'; -import { FileItem, FileItems, FilesActionType } from './types'; +import { FileItem, FileItemsState, FilesActionType } from './types'; const compareFileItems = (prev: FileItem, next: FileItem) => prev.key.localeCompare(next.key); +const isValidFile = (file: File) => file.size <= UPLOAD_FILE_SIZE_LIMIT; + +const isSameFiles = (prev: File, next: File) => + prev.name === next.name && + prev.webkitRelativePath === next.webkitRelativePath; + +const generateFileItem = (file: File): FileItem => ({ + key: isEmpty(file.webkitRelativePath) ? file.name : file.webkitRelativePath, + id: crypto.randomUUID(), + file, +}); + export const resolveFiles = ( - prevItems: FileItems, + prevItems: FileItemsState, files: File[] | undefined -): FileItems => { +): FileItemsState => { if (!files?.length) return prevItems; - // construct `nextItems` and filter out existing `file` entries - const nextItems = files.reduce((items: FileItems, file) => { - const { name, webkitRelativePath } = file; + const { validFiles: prevValidFiles, invalidFiles: prevInvalidFiles } = + prevItems; + + const nextValidFiles = files + .filter(isValidFile) + .filter( + (file) => + !prevValidFiles.some(({ file: existing }) => + isSameFiles(existing, file) + ) + ) + .map(generateFileItem); - return prevItems.some( - ({ file: existing }) => - existing.name === name && - existing.webkitRelativePath === webkitRelativePath + const nextInvalidFiles = files + .filter((file) => !isValidFile(file)) + .filter( + (file) => + !prevInvalidFiles.some(({ file: existing }) => + isSameFiles(existing, file) + ) ) - ? items - : items.concat({ - key: isEmpty(webkitRelativePath) ? name : webkitRelativePath, - id: crypto.randomUUID(), - file, - }); - }, []); - - if (!nextItems.length) return prevItems; - - if (!prevItems.length) { - return nextItems.sort(compareFileItems); + .map(generateFileItem); + + if (!prevValidFiles.length) { + nextValidFiles.sort(compareFileItems); + } + + if (!prevInvalidFiles.length) { + nextInvalidFiles.sort(compareFileItems); } - return prevItems.concat(nextItems).sort(compareFileItems); + return { + validFiles: nextValidFiles.length + ? prevValidFiles.concat(nextValidFiles).sort(compareFileItems) + : prevValidFiles, + invalidFiles: nextInvalidFiles.length + ? prevInvalidFiles.concat(nextInvalidFiles).sort(compareFileItems) + : prevInvalidFiles, + }; }; export const filesReducer: React.Reducer< - FileItems, + FileItemsState, Exclude > = (prevItems, input) => { switch (input.type) { @@ -51,16 +80,23 @@ export const filesReducer: React.Reducer< return resolveFiles(prevItems, input.files); } case 'REMOVE_FILE_ITEM': { - const filteredItems = prevItems.filter(({ id }) => id !== input.id); + const filteredValidFiles = prevItems.validFiles.filter( + ({ id }) => id !== input.id + ); - return filteredItems.length === prevItems.length + return filteredValidFiles.length === prevItems.validFiles.length ? prevItems - : filteredItems; + : { + validFiles: filteredValidFiles, + invalidFiles: prevItems.invalidFiles, + }; } case 'RESET_FILE_ITEMS': { - return []; + return { validFiles: [], invalidFiles: [] }; + } + case 'RESET_INVALID_FILE_ITEMS': { + return { ...prevItems, invalidFiles: [] }; } - // TODO: clear message } }; diff --git a/packages/react-storage/src/components/StorageBrowser/providers/store/useStore.ts b/packages/react-storage/src/components/StorageBrowser/providers/store/useStore.ts index 7ca935c82a9..b151d9203e9 100644 --- a/packages/react-storage/src/components/StorageBrowser/providers/store/useStore.ts +++ b/packages/react-storage/src/components/StorageBrowser/providers/store/useStore.ts @@ -1,7 +1,7 @@ import React from 'react'; import { ActionTypeAction, useActionType } from './actionType'; -import { FileItems, FilesActionType, useFiles } from './files'; +import { FileItemsState, FilesActionType, useFiles } from './files'; import { LocationActionType, LocationState, useLocation } from './location'; import { LocationItemsAction, @@ -11,7 +11,7 @@ import { export interface UseStoreState { actionType: string | undefined; - files: FileItems | undefined; + files: FileItemsState | undefined; location: LocationState; locationItems: LocationItemsState; } @@ -36,7 +36,8 @@ export function useStore(): [UseStoreState, HandleStoreAction] { case 'ADD_FILE_ITEMS': case 'REMOVE_FILE_ITEM': case 'SELECT_FILES': - case 'RESET_FILE_ITEMS': { + case 'RESET_FILE_ITEMS': + case 'RESET_INVALID_FILE_ITEMS': { dispatchFilesAction(action); break; } diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx index 807008ffa09..f18497f694d 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx @@ -43,6 +43,7 @@ export function UploadView({ overwriteToggleLabel, title, getActionCompleteMessage, + getFilesValidationMessage, } = displayText; const { @@ -52,6 +53,7 @@ export function UploadView({ location, tasks, statusCounts, + invalidFiles, onActionStart, onActionCancel, onDropFiles, @@ -68,12 +70,15 @@ export function UploadView({ const isAddFolderDisabled = isProcessing || isProcessingComplete; const isActionExitDisabled = isProcessing; const destinationList = (location.key || '/').split('/'); - - const message = isProcessingComplete + const actionCompleteMessage = isProcessingComplete ? getActionCompleteMessage({ counts: statusCounts, }) : undefined; + const filesValidationMessage = + invalidFiles && !isProcessing + ? getFilesValidationMessage({ invalidFiles }) + : undefined; return (
@@ -92,7 +97,7 @@ export function UploadView({ isOverwriteToggleDisabled: isProcessing || isProcessingComplete, isOverwritingEnabled, overwriteToggleLabel, - message, + message: actionCompleteMessage ?? filesValidationMessage, statusCounts, statusDisplayCanceledLabel, statusDisplayCompletedLabel, diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx index 099a8ed67da..f6fdefe17b7 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx @@ -10,7 +10,10 @@ import { UploadView } from '../UploadView'; jest.mock('../../../../displayText', () => ({ useDisplayText: () => ({ - UploadView: { getActionCompleteMessage: jest.fn() }, + UploadView: { + getActionCompleteMessage: jest.fn(), + getFilesValidationMessage: jest.fn(), + }, }), })); @@ -42,6 +45,11 @@ const statusCounts = { ...INITIAL_STATUS_COUNTS }; const testFile = new File([], 'test-ooo'); const data = { id: 'some-uuid', file: testFile, key: testFile.name }; +const invalidFileData = { + file: new File([], 'very-big-file'), + id: 'uuid', + key: 'very-big-file', +}; const taskOne = { data, @@ -67,6 +75,7 @@ const initialViewState: UploadViewState = { isProcessingComplete: false, isProcessing: false, tasks: [], + invalidFiles: undefined, statusCounts, }; @@ -74,6 +83,7 @@ const preprocessingViewState: UploadViewState = { ...initialViewState, tasks: [taskOne], statusCounts: { ...statusCounts, QUEUED: 1, TOTAL: 1 }, + invalidFiles: [invalidFileData], }; const processingViewState: UploadViewState = { diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts index 63883f432a0..7fcc41f38d7 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts @@ -4,6 +4,7 @@ import { LocationData } from '../../../../actions'; import * as ConfigModule from '../../../../providers/configuration'; import * as StoreModule from '../../../../providers/store'; import * as TasksModule from '../../../../tasks'; +import { UPLOAD_FILE_SIZE_LIMIT } from '../../constants'; const useStoreSpy = jest.spyOn(StoreModule, 'useStore'); @@ -16,13 +17,12 @@ const rootLocation: LocationData = { type: 'BUCKET', }; +const mockUserStoreState = { + location: { current: rootLocation, path: '', key: '' }, + files: undefined, +} as StoreModule.UseStoreState; const dispatchStoreAction = jest.fn(); -useStoreSpy.mockReturnValue([ - { - location: { current: rootLocation, path: '', key: '' }, - } as StoreModule.UseStoreState, - dispatchStoreAction, -]); +useStoreSpy.mockReturnValue([mockUserStoreState, dispatchStoreAction]); const credentials = jest.fn(); const config: ConfigModule.GetActionInput = jest.fn(() => ({ @@ -43,6 +43,15 @@ const fileItemTwo = { file: testFileTwo, key: testFileTwo.name, }; +const invalidFile = { + ...new File([], 'invalid-file'), + size: UPLOAD_FILE_SIZE_LIMIT + 1, +}; +const invalidFileItem = { + id: 'invalid-file-uuid', + file: invalidFile, + key: invalidFile.name, +}; jest.spyOn(ConfigModule, 'useGetActionInput').mockReturnValue(config); const handleProcessTasks = jest.fn(); @@ -77,6 +86,7 @@ const useProcessTasksSpy = jest describe('useUploadView', () => { afterEach(() => { + mockUserStoreState.files = undefined; jest.clearAllMocks(); }); @@ -94,6 +104,37 @@ describe('useUploadView', () => { }); }); + it('should return invalidFiles from store', () => { + mockUserStoreState.files = { + validFiles: [], + invalidFiles: [invalidFileItem], + }; + const { result } = renderHook(() => useUploadView()); + + expect(result.current.invalidFiles).toEqual([invalidFileItem]); + }); + + it('should clear invalidFiles after upload is started', () => { + mockUserStoreState.files = { + validFiles: [fileItemOne], + invalidFiles: [invalidFileItem], + }; + renderHook(() => useUploadView()); + + expect(useProcessTasksSpy).toHaveBeenCalledTimes(1); + const onTaskProgressCallback = + useProcessTasksSpy.mock.calls[0][2]?.onTaskProgress; + expect(onTaskProgressCallback).toBeDefined(); + act(() => { + // @ts-expect-error Input is not needed + onTaskProgressCallback?.(); + }); + expect(dispatchStoreAction).toHaveBeenCalledTimes(1); + expect(dispatchStoreAction).toHaveBeenCalledWith({ + type: 'RESET_INVALID_FILE_ITEMS', + }); + }); + it('should dispatchStoreAction when onSelectFiles is invoked with different types', () => { const { result } = renderHook(() => useUploadView()); @@ -134,6 +175,7 @@ describe('useUploadView', () => { destinationPrefix: '', }); }); + it('should call cancel on each pending task when onCancel is invoked', () => { const tasks: TasksModule.Task[] = [ { ...taskOne, status: 'PENDING' }, diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts index 167c9669153..d7a8b88254d 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts @@ -1,5 +1,5 @@ import { LocationData, UploadHandlerData } from '../../../actions'; -import { FileItem } from '../../../providers'; +import { FileItem, FileItems } from '../../../providers'; import { ActionViewComponent, ActionViewProps, @@ -11,6 +11,7 @@ export interface UploadViewState extends ActionViewState { onDropFiles: (files: File[]) => void; onSelectFiles: (type: 'FILE' | 'FOLDER') => void; onToggleOverwrite: () => void; + invalidFiles: FileItems | undefined; } export interface UploadViewProps diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts index 674fa9cb4a3..8a148063a6d 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts @@ -17,6 +17,7 @@ export const useUploadView = ( const getInput = useGetActionInput(); const [{ files, location }, dispatchStoreAction] = useStore(); const { current, key } = location; + const { validFiles, invalidFiles } = files ?? {}; const [isOverwritingEnabled, setIsOverwritingEnabled] = React.useState( DEFAULT_OVERWRITE_ENABLED @@ -25,8 +26,14 @@ export const useUploadView = ( const [ { isProcessing, isProcessingComplete, statusCounts, tasks }, handleProcess, - ] = useProcessTasks(uploadHandler, files, { + ] = useProcessTasks(uploadHandler, validFiles, { concurrency: DEFAULT_ACTION_CONCURRENCY, + // TODO: in reality, this should be on `onTaskStart` hood. But we don't have it today. + onTaskProgress: () => { + if (invalidFiles) { + dispatchStoreAction({ type: 'RESET_INVALID_FILE_ITEMS' }); + } + }, }); const onDropFiles = React.useCallback( @@ -81,6 +88,7 @@ export const useUploadView = ( isProcessingComplete, isOverwritingEnabled, location, + invalidFiles, statusCounts, tasks, onActionCancel, diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/constants.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/constants.ts index 3ec4f59938a..a7eb48005dd 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/constants.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/constants.ts @@ -1 +1,2 @@ export const DEFAULT_ACTION_CONCURRENCY = 4; +export const UPLOAD_FILE_SIZE_LIMIT = 160 * 1000 * 1000 * 1000;