From c0ec76e76b15313fb0801145cb6984f6c5acdb38 Mon Sep 17 00:00:00 2001 From: ashika112 Date: Fri, 30 May 2025 15:44:42 -0700 Subject: [PATCH 1/5] update workflow --- .github/workflows/codeql.yml | 2 +- .github/workflows/test-internal-prs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f2edd1f7b15..0b12bf94391 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,7 @@ name: 'CodeQL' on: push: - branches: ['main'] + branches: feat-sb/multi-file-download/main jobs: analyze: diff --git a/.github/workflows/test-internal-prs.yml b/.github/workflows/test-internal-prs.yml index ee41622aaa6..febb57fadf6 100644 --- a/.github/workflows/test-internal-prs.yml +++ b/.github/workflows/test-internal-prs.yml @@ -13,7 +13,7 @@ concurrency: on: pull_request: - branches: [main, hotfix] + branches: feat-sb/multi-file-download/main types: [opened, synchronize, labeled] jobs: From 892cf6ad3ed8edfb9eff821e7520635322e167db Mon Sep 17 00:00:00 2001 From: ashika112 <155593080+ashika112@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:35:55 -0700 Subject: [PATCH 2/5] feat(storage-browser): Multi File Download (#6553) * download multiple view * cleanup download resolvers * lint fix * add unit tests * add integ test * Create brave-queens-clean.md * update broken tests --- .changeset/brave-queens-clean.md | 5 + .../storage-browser/action-menu.feature | 16 + packages/react-storage/package.json | 4 +- .../__snapshots__/defaults.spec.ts.snap | 11 +- .../configs/__tests__/defaults.spec.ts | 22 ++ .../actions/configs/defaults.tsx | 15 +- .../StorageBrowser/actions/configs/types.ts | 5 +- .../actions/handlers/download.ts | 3 +- .../__tests__/StorageBrowserDefault.spec.tsx | 1 + .../createStorageBrowser.tsx | 2 + .../createStorageBrowser/types.ts | 2 + .../displayText/__tests__/context.spec.tsx | 4 + .../displayText/__tests__/utils.spec.ts | 17 +- .../StorageBrowser/displayText/context.tsx | 5 + .../StorageBrowser/displayText/index.ts | 7 +- .../__snapshots__/deleteView.spec.ts.snap | 16 +- .../__snapshots__/downloadView.spec.ts.snap | 73 +++++ .../libraries/en/__tests__/deleteView.spec.ts | 2 +- .../en/__tests__/downloadView.spec.ts | 17 ++ .../displayText/libraries/en/default.ts | 2 + .../displayText/libraries/en/downloadView.ts | 26 ++ .../libraries/en/locationDetailView.ts | 2 + .../StorageBrowser/displayText/types.ts | 9 + .../StorageBrowser/displayText/utils.ts | 13 +- .../StorageBrowser/useAction/utils.ts | 5 +- .../DeleteView/DeleteViewProvider.tsx | 1 - .../DownloadView/DownloadView.tsx | 57 ++++ .../DownloadView/DownloadViewProvider.tsx | 81 +++++ .../__tests__/DownloadView.spec.tsx | 204 +++++++++++++ .../__tests__/useDownloadView.spec.ts | 180 ++++++++++++ .../LocationActionView/DownloadView/index.ts | 3 + .../LocationActionView/DownloadView/types.ts | 29 ++ .../DownloadView/useDownloadView.ts | 72 +++++ .../views/LocationActionView/index.ts | 6 + .../__snapshots__/useView.spec.ts.snap | 2 +- .../views/__tests__/useView.spec.ts | 2 +- .../views/context/actionViews.tsx | 2 + .../components/StorageBrowser/views/index.ts | 2 + .../components/StorageBrowser/views/types.ts | 13 +- .../StorageBrowser/views/useView.ts | 10 +- .../StorageBrowser/views/utils/index.ts | 1 + .../tableResolvers/__testUtils__/tasks.ts | 21 +- .../__tests__/copyActionTask.spec.ts | 6 +- .../__tests__/deleteActionTask.spec.ts | 26 +- .../__tests__/downloadActionTask.spec.ts | 278 ++++++++++++++++++ .../tableResolvers/__tests__/utils.spec.ts | 6 +- .../utils/tableResolvers/downloadResolvers.ts | 46 +++ .../views/utils/tableResolvers/index.ts | 3 +- .../views/utils/tableResolvers/types.ts | 13 +- 49 files changed, 1281 insertions(+), 67 deletions(-) create mode 100644 .changeset/brave-queens-clean.md create mode 100644 packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/downloadView.spec.ts.snap create mode 100644 packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/downloadView.spec.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/downloadView.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.tsx create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.tsx create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/DownloadView.spec.tsx create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/useDownloadView.spec.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/index.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/types.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/downloadActionTask.spec.ts create mode 100644 packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/downloadResolvers.ts diff --git a/.changeset/brave-queens-clean.md b/.changeset/brave-queens-clean.md new file mode 100644 index 00000000000..2f7183fbded --- /dev/null +++ b/.changeset/brave-queens-clean.md @@ -0,0 +1,5 @@ +--- +"@aws-amplify/ui-react-storage": minor +--- + +feat(storage-browser): Multi File Download diff --git a/packages/e2e/features/ui/components/storage/storage-browser/action-menu.feature b/packages/e2e/features/ui/components/storage/storage-browser/action-menu.feature index e2e4f2dcd32..a1c1a89ab9e 100644 --- a/packages/e2e/features/ui/components/storage/storage-browser/action-menu.feature +++ b/packages/e2e/features/ui/components/storage/storage-browser/action-menu.feature @@ -74,6 +74,13 @@ Feature: Create folder with Storage Browser When I click the "Exit" button # list uploaded file Then I see "1" files with random names + # download file + Then I click checkbox for with "1" files with random names + When I click the "Menu Toggle" button + Then I click the "Download" menuitem + Then I click the "Download" button + Then I see "All files downloaded" + When I click the "Exit" button # copy file Then I click checkbox for with "1" files with random names When I click the "Menu Toggle" button @@ -174,3 +181,12 @@ Feature: Create folder with Storage Browser When A network failure occurs Then I click the "Delete" button Then I see "All files failed to delete" + @react + Scenario: Download file shows a Network error if offline + When I click the first button containing "public" + Then I click checkbox for file "001_dont_delete_file.txt" + When I click the "Menu Toggle" button + Then I click the "Download" menuitem + When A network failure occurs + Then I click the "Download" button + Then I see "All files failed to download" \ No newline at end of file diff --git a/packages/react-storage/package.json b/packages/react-storage/package.json index 86ab74c0805..25d65fba3de 100644 --- a/packages/react-storage/package.json +++ b/packages/react-storage/package.json @@ -66,7 +66,7 @@ "name": "createStorageBrowser", "path": "dist/esm/browser.mjs", "import": "{ createStorageBrowser }", - "limit": "64.5 kB", + "limit": "64.6 kB", "ignore": [ "@aws-amplify/storage" ] @@ -75,7 +75,7 @@ "name": "StorageBrowser", "path": "dist/esm/index.mjs", "import": "{ StorageBrowser }", - "limit": "87 kB" + "limit": "88 kB" }, { "name": "FileUploader", diff --git a/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/__snapshots__/defaults.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/__snapshots__/defaults.spec.ts.snap index 8cb6a31c908..61d1469a754 100644 --- a/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/__snapshots__/defaults.spec.ts.snap +++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/__snapshots__/defaults.spec.ts.snap @@ -31,7 +31,16 @@ exports[`defaultActionConfigs matches expected shape 1`] = ` "handler": [Function], "viewName": "DeleteView", }, - "download": [Function], + "download": { + "actionListItem": { + "disable": [Function], + "hide": [Function], + "icon": "download", + "label": "Download", + }, + "handler": [Function], + "viewName": "DownloadView", + }, "listLocationItems": [Function], "upload": { "actionListItem": { diff --git a/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/defaults.spec.ts b/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/defaults.spec.ts index 6feaeab7d8f..21f9adb6b12 100644 --- a/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/defaults.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/__tests__/defaults.spec.ts @@ -79,6 +79,28 @@ describe('defaultActionConfigs', () => { }); }); + describe('download', () => { + const { disable, hide } = defaultActionConfigs.download.actionListItem; + it('hides the action list item as expected', () => { + for (const permissionsWithoutDownload of generateCombinations( + LOCATION_PERMISSION_VALUES.filter((value) => value !== 'get') + )) { + const permissionsWithDownload = [ + ...permissionsWithoutDownload, + 'get' as const, + ]; + expect(hide?.(permissionsWithoutDownload)).toBe(true); + expect(hide?.(permissionsWithDownload)).toBe(false); + } + }); + + it('is disabled when no files are selected', () => { + expect(disable?.(undefined)).toBe(true); + expect(disable?.([])).toBe(true); + expect(disable?.([file])).toBe(false); + }); + }); + describe('copy', () => { const { disable, hide } = defaultActionConfigs.copy.actionListItem; it('hides the action list item as expected', () => { diff --git a/packages/react-storage/src/components/StorageBrowser/actions/configs/defaults.tsx b/packages/react-storage/src/components/StorageBrowser/actions/configs/defaults.tsx index 9ab60e82d57..f223ad76d31 100644 --- a/packages/react-storage/src/components/StorageBrowser/actions/configs/defaults.tsx +++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/defaults.tsx @@ -4,6 +4,7 @@ import type { CopyActionConfig, CreateFolderActionConfig, DeleteActionConfig, + DownloadActionConfig, UploadActionConfig, } from './types'; @@ -49,12 +50,22 @@ export const uploadActionConfig: UploadActionConfig = { handler: defaultHandlers.upload, }; +export const downloadActionConfig: DownloadActionConfig = { + viewName: 'DownloadView', + actionListItem: { + disable: (selected) => !selected || selected.length === 0, + hide: (permissions) => !permissions.includes('get'), + icon: 'download', + label: 'Download', + }, + handler: defaultHandlers.download, +}; + // Action view configs only, does not include `listLocationItems` export const defaultActionViewConfigs = { copy: copyActionConfig, createFolder: createFolderActionConfig, - // provide `download` handler only; `download` does not have a dedicated view/config - download: defaultHandlers.download, + download: downloadActionConfig, delete: deleteActionConfig, upload: uploadActionConfig, }; diff --git a/packages/react-storage/src/components/StorageBrowser/actions/configs/types.ts b/packages/react-storage/src/components/StorageBrowser/actions/configs/types.ts index ad9360feaaf..94140aa169e 100644 --- a/packages/react-storage/src/components/StorageBrowser/actions/configs/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/types.ts @@ -128,6 +128,9 @@ export interface CopyActionConfig export interface CreateFolderActionConfig extends ActionViewConfig {} +export interface DownloadActionConfig + extends ActionViewConfig {} + export interface ListActionConfig { /** * action handler @@ -140,7 +143,7 @@ export interface DefaultActionConfigs { listLocationItems?: ListLocationItemsHandler; upload?: UploadActionConfig; delete?: DeleteActionConfig; - download?: DownloadHandler; + download?: DownloadHandler | DownloadActionConfig; copy?: CopyActionConfig; } diff --git a/packages/react-storage/src/components/StorageBrowser/actions/handlers/download.ts b/packages/react-storage/src/components/StorageBrowser/actions/handlers/download.ts index 11cf158f255..2ee25810b47 100644 --- a/packages/react-storage/src/components/StorageBrowser/actions/handlers/download.ts +++ b/packages/react-storage/src/components/StorageBrowser/actions/handlers/download.ts @@ -1,5 +1,6 @@ import { getUrl } from '../../storage-internal'; import type { + OptionalFileData, TaskData, TaskHandler, TaskHandlerInput, @@ -9,7 +10,7 @@ import type { import { constructBucket } from './utils'; -export interface DownloadHandlerData extends TaskData { +export interface DownloadHandlerData extends OptionalFileData, TaskData { fileKey: string; } diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/StorageBrowserDefault.spec.tsx b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/StorageBrowserDefault.spec.tsx index 44786a2c668..26c3ebc1b92 100644 --- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/StorageBrowserDefault.spec.tsx +++ b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/StorageBrowserDefault.spec.tsx @@ -16,6 +16,7 @@ jest.spyOn(ViewsModule, 'useViews').mockReturnValue({ action: { copy: () =>
, createFolder: () =>
, + download: () =>
, delete: () =>
, upload: () =>
, }, diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.tsx b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.tsx index e8576e5de7e..64871c5df52 100644 --- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.tsx +++ b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.tsx @@ -12,6 +12,7 @@ import { CopyView, CreateFolderView, DeleteView, + DownloadView, LocationActionView, LocationDetailView, LocationsView, @@ -81,6 +82,7 @@ export default function createStorageBrowser< StorageBrowser.CopyView = CopyView; StorageBrowser.CreateFolderView = CreateFolderView; StorageBrowser.DeleteView = DeleteView; + StorageBrowser.DownloadView = DownloadView; StorageBrowser.UploadView = UploadView; StorageBrowser.Provider = Provider; diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts index 65768f67e90..1d9d4a5873f 100644 --- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts @@ -24,6 +24,7 @@ import type { CopyViewType, CreateFolderViewType, DeleteViewType, + DownloadViewType, UploadViewType, LocationActionViewType, LocationDetailViewType, @@ -292,6 +293,7 @@ export interface StorageBrowserType { CopyView: CopyViewType; CreateFolderView: CreateFolderViewType; DeleteView: DeleteViewType; + DownloadView: DownloadViewType; UploadView: UploadViewType; } diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/context.spec.tsx b/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/context.spec.tsx index ed62b9b6803..34172c95d62 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/context.spec.tsx +++ b/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/context.spec.tsx @@ -51,6 +51,9 @@ describe('resolveDisplayText', () => { DeleteView: { title: 'DeleteViewTitle', }, + DownloadView: { + title: 'DownloadViewTitle', + }, LocationDetailView: { getTitle: () => 'LocationDetailViewTitle', }, @@ -65,6 +68,7 @@ describe('resolveDisplayText', () => { expect(result.CopyView.title).toBe('CopyViewTitle'); expect(result.CreateFolderView.title).toBe('CreateFolderViewTitle'); expect(result.DeleteView.title).toBe('DeleteViewTitle'); + expect(result.DownloadView.title).toBe('DownloadViewTitle'); expect(result.LocationDetailView.getTitle({} as LocationState)).toBe( 'LocationDetailViewTitle' ); diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/utils.spec.ts b/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/utils.spec.ts index 9563c517333..90967d73ceb 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/utils.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/__tests__/utils.spec.ts @@ -1,4 +1,8 @@ -import { isCopyViewDisplayTextKey, isDeleteViewDisplayTextKey } from '../utils'; +import { + isCopyViewDisplayTextKey, + isDeleteViewDisplayTextKey, + isDownloadViewDisplayTextKey, +} from '../utils'; describe('display text utils', () => { describe('isCopyViewDisplayTextKey', () => { @@ -22,4 +26,15 @@ describe('display text utils', () => { expect(output).toBe(false); }); }); + + describe('isDownloadViewDisplayTextKey', () => { + it('returns `true` when provided a valid key', () => { + const output = isDownloadViewDisplayTextKey('statusDisplayCanceledLabel'); + expect(output).toBe(true); + }); + it('returns `false` when provided an invalid key', () => { + const output = isDownloadViewDisplayTextKey('invalid'); + expect(output).toBe(false); + }); + }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/context.tsx b/packages/react-storage/src/components/StorageBrowser/displayText/context.tsx index 62a5e28e198..9f8fdb9468d 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/context.tsx +++ b/packages/react-storage/src/components/StorageBrowser/displayText/context.tsx @@ -24,6 +24,7 @@ export function resolveDisplayText( CopyView, CreateFolderView, DeleteView, + DownloadView, LocationDetailView, LocationsView, UploadView, @@ -38,6 +39,10 @@ export function resolveDisplayText( ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.DeleteView, ...DeleteView, }, + DownloadView: { + ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.DownloadView, + ...DownloadView, + }, LocationDetailView: { ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.LocationDetailView, ...LocationDetailView, diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/index.ts b/packages/react-storage/src/components/StorageBrowser/displayText/index.ts index 461c6b19a15..bf2d35317cc 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/index.ts @@ -7,6 +7,11 @@ export type { CopyViewDisplayText, UploadViewDisplayText, DeleteViewDisplayText, + DownloadViewDisplayText, StorageBrowserDisplayText, } from './types'; -export { isCopyViewDisplayTextKey, isDeleteViewDisplayTextKey } from './utils'; +export { + isCopyViewDisplayTextKey, + isDeleteViewDisplayTextKey, + isDownloadViewDisplayTextKey, +} from './utils'; diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/deleteView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/deleteView.spec.ts.snap index f2a42149a41..1bf6af76d61 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/deleteView.spec.ts.snap +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/deleteView.spec.ts.snap @@ -1,55 +1,55 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the all failed scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the all failed scenario 1`] = ` { "content": "All files failed to delete.", "type": "error", } `; -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the all prevented overwrites scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the all prevented overwrites scenario 1`] = ` { "content": "0 files deleted, 0 files failed to delete.", "type": "error", } `; -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the all success scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the all success scenario 1`] = ` { "content": "All files deleted.", "type": "success", } `; -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the no failures, some prevented overwrites, some success scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the no failures, some prevented overwrites, some success scenario 1`] = ` { "content": "2 files deleted, 0 files failed to delete.", "type": "error", } `; -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the some failed scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the some failed scenario 1`] = ` { "content": "8 files deleted, 3 files failed to delete.", "type": "error", } `; -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, no success scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, no success scenario 1`] = ` { "content": "0 files deleted, 2 files failed to delete.", "type": "error", } `; -exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, some success scenario 1`] = ` +exports[`DeleteView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, some success scenario 1`] = ` { "content": "8 files deleted, 2 files failed to delete.", "type": "error", } `; -exports[`CopyView display text values should match snapshot values 1`] = ` +exports[`DeleteView display text values should match snapshot values 1`] = ` { "actionCancelLabel": "Cancel", "actionDestinationLabel": "Destination", diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/downloadView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/downloadView.spec.ts.snap new file mode 100644 index 00000000000..1418b26f479 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/downloadView.spec.ts.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the all failed scenario 1`] = ` +{ + "content": "All files failed to download.", + "type": "error", +} +`; + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the all prevented overwrites scenario 1`] = ` +{ + "content": "0 files downloaded, 0 files failed to download.", + "type": "error", +} +`; + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the all success scenario 1`] = ` +{ + "content": "All files downloaded.", + "type": "success", +} +`; + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the no failures, some prevented overwrites, some success scenario 1`] = ` +{ + "content": "2 files downloaded, 0 files failed to download.", + "type": "error", +} +`; + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the some failed scenario 1`] = ` +{ + "content": "8 files downloaded, 3 files failed to download.", + "type": "error", +} +`; + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, no success scenario 1`] = ` +{ + "content": "0 files downloaded, 2 files failed to download.", + "type": "error", +} +`; + +exports[`DownloadView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, some success scenario 1`] = ` +{ + "content": "8 files downloaded, 2 files failed to download.", + "type": "error", +} +`; + +exports[`DownloadView display text values should match snapshot values 1`] = ` +{ + "actionCancelLabel": "Cancel", + "actionDestinationLabel": "Destination", + "actionExitLabel": "Exit", + "actionStartLabel": "Download", + "getActionCompleteMessage": [Function], + "statusDisplayCanceledLabel": "Canceled", + "statusDisplayCompletedLabel": "Completed", + "statusDisplayFailedLabel": "Failed", + "statusDisplayInProgressLabel": "In progress", + "statusDisplayQueuedLabel": "Not started", + "statusDisplayTotalLabel": "Total", + "tableColumnCancelHeader": "", + "tableColumnFolderHeader": "Folder", + "tableColumnNameHeader": "Name", + "tableColumnSizeHeader": "Size", + "tableColumnStatusHeader": "Status", + "tableColumnTypeHeader": "Type", + "title": "Download", +} +`; diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/deleteView.spec.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/deleteView.spec.ts index 1f3c4488fc2..0a6e8b94fd6 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/deleteView.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/deleteView.spec.ts @@ -1,7 +1,7 @@ import { ACTION_SCENARIOS } from './scenarios'; import { DEFAULT_DELETE_VIEW_DISPLAY_TEXT } from '../deleteView'; -describe('CopyView display text values', () => { +describe('DeleteView display text values', () => { it('should match snapshot values', () => { expect(DEFAULT_DELETE_VIEW_DISPLAY_TEXT).toMatchSnapshot(); }); diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/downloadView.spec.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/downloadView.spec.ts new file mode 100644 index 00000000000..9d0f984665a --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/downloadView.spec.ts @@ -0,0 +1,17 @@ +import { ACTION_SCENARIOS } from './scenarios'; +import { DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT } from '../downloadView'; + +describe('DownloadView display text values', () => { + it('should match snapshot values', () => { + expect(DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT).toMatchSnapshot(); + }); + + it.each(ACTION_SCENARIOS)( + '`getActionCompleteMessage` returns the expected values in the %s scenario', + (_, counts) => { + const { getActionCompleteMessage } = DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT; + + expect(getActionCompleteMessage({ counts })).toMatchSnapshot(); + } + ); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/default.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/default.ts index 0b451385ef0..01d6ff1a017 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/default.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/default.ts @@ -6,12 +6,14 @@ import { DEFAULT_DELETE_VIEW_DISPLAY_TEXT } from './deleteView'; import { DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT } from './locationDetailView'; import { DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT } from './locationsView'; import { DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT } from './uploadView'; +import { DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT } from './downloadView'; export const DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT: DefaultStorageBrowserDisplayText = { CopyView: DEFAULT_COPY_VIEW_DISPLAY_TEXT, CreateFolderView: DEFAULT_CREATE_FOLDER_VIEW_DISPLAY_TEXT, DeleteView: DEFAULT_DELETE_VIEW_DISPLAY_TEXT, + DownloadView: DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT, LocationDetailView: DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT, LocationsView: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT, UploadView: DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT, diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/downloadView.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/downloadView.ts new file mode 100644 index 00000000000..684f783f886 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/downloadView.ts @@ -0,0 +1,26 @@ +import { DEFAULT_ACTION_VIEW_DISPLAY_TEXT } from './shared'; +import type { DefaultDownloadViewDisplayText } from '../../types'; + +export const DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT: DefaultDownloadViewDisplayText = + { + ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT, + title: 'Download', + actionStartLabel: 'Download', + getActionCompleteMessage: (data) => { + const { counts } = data ?? {}; + const { COMPLETE, FAILED, TOTAL } = counts ?? {}; + + if (COMPLETE === TOTAL) { + return { content: 'All files downloaded.', type: 'success' }; + } + + if (FAILED === TOTAL) { + return { content: 'All files failed to download.', type: 'error' }; + } + + return { + content: `${COMPLETE} files downloaded, ${FAILED} files failed to download.`, + type: 'error', + }; + }, + }; diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationDetailView.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationDetailView.ts index f3d5a8a2080..9d3685e2b11 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationDetailView.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationDetailView.ts @@ -69,6 +69,8 @@ export const DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT: DefaultLocationDetailVie return 'Create folder'; case 'Upload': return 'Upload'; + case 'Download': + return 'Download'; default: return key; } diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/types.ts b/packages/react-storage/src/components/StorageBrowser/displayText/types.ts index c9669d32513..5924c4be884 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/types.ts @@ -9,6 +9,7 @@ import type { TaskData, UploadHandlerData, LocationPermissions, + DownloadHandlerData, } from '../actions'; import type { MessageType } from '../components'; import type { FileItems } from '../fileItems'; @@ -141,6 +142,9 @@ export interface DefaultDeleteViewDisplayText tableColumnProgressHeader?: string; } +export interface DefaultDownloadViewDisplayText + extends DefaultActionViewDisplayText {} + export interface DefaultUploadViewDisplayText extends DefaultActionViewDisplayText { addFilesLabel: string; @@ -157,6 +161,7 @@ export interface DefaultStorageBrowserDisplayText { CopyView: DefaultCopyViewDisplayText; CreateFolderView: DefaultCreateFolderViewDisplayText; DeleteView: DefaultDeleteViewDisplayText; + DownloadView: DefaultDownloadViewDisplayText; LocationsView: DefaultLocationsViewDisplayText; LocationDetailView: DefaultLocationDetailViewDisplayText; UploadView: DefaultUploadViewDisplayText; @@ -171,6 +176,9 @@ export interface CopyViewDisplayText export interface DeleteViewDisplayText extends Partial {} +export interface DownloadViewDisplayText + extends Partial {} + export interface LocationsViewDisplayText extends Partial {} @@ -188,6 +196,7 @@ export interface StorageBrowserDisplayText { LocationDetailView?: LocationDetailViewDisplayText; UploadView?: UploadViewDisplayText; DeleteView?: DeleteViewDisplayText; + DownloadView?: DownloadViewDisplayText; CopyView?: CopyViewDisplayText; CreateFolderView?: CreateFolderViewDisplayText; } diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/utils.ts b/packages/react-storage/src/components/StorageBrowser/displayText/utils.ts index ab932ab066a..069b53ced73 100644 --- a/packages/react-storage/src/components/StorageBrowser/displayText/utils.ts +++ b/packages/react-storage/src/components/StorageBrowser/displayText/utils.ts @@ -1,6 +1,12 @@ import { DEFAULT_COPY_VIEW_DISPLAY_TEXT } from './libraries/en/copyView'; import { DEFAULT_DELETE_VIEW_DISPLAY_TEXT } from './libraries/en/deleteView'; -import type { CopyViewDisplayText, DeleteViewDisplayText } from './types'; +import { DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT } from './libraries/en/downloadView'; + +import type { + CopyViewDisplayText, + DeleteViewDisplayText, + DownloadViewDisplayText, +} from './types'; export const isCopyViewDisplayTextKey = ( value: string @@ -11,3 +17,8 @@ export const isDeleteViewDisplayTextKey = ( value: string ): value is keyof DeleteViewDisplayText => !!DEFAULT_DELETE_VIEW_DISPLAY_TEXT[value as keyof DeleteViewDisplayText]; + +export const isDownloadViewDisplayTextKey = ( + value: string +): value is keyof DownloadViewDisplayText => + !!DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT[value as keyof DownloadViewDisplayText]; diff --git a/packages/react-storage/src/components/StorageBrowser/useAction/utils.ts b/packages/react-storage/src/components/StorageBrowser/useAction/utils.ts index 60d27756519..a7aec1dc33a 100644 --- a/packages/react-storage/src/components/StorageBrowser/useAction/utils.ts +++ b/packages/react-storage/src/components/StorageBrowser/useAction/utils.ts @@ -24,7 +24,7 @@ export const getActionHandlers = < copy: copyConfig, createFolder: createFolderConfig, delete: deleteConfig, - download, + download: downloadConfig, upload: uploadConfig, listLocationItems, listLocations, @@ -34,7 +34,8 @@ export const getActionHandlers = < copy: copyConfig.handler, createFolder: createFolderConfig.handler, delete: deleteConfig.handler, - download, + download: + 'handler' in downloadConfig ? downloadConfig.handler : downloadConfig, listLocationItems, listLocations, upload: uploadConfig.handler, diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.tsx index 2d329be769a..ee8fbf8ac09 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.tsx @@ -30,7 +30,6 @@ export function DeleteViewProvider({ isProcessingComplete, statusCounts, tasks: items, - onActionCancel, onActionStart, onActionExit, diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.tsx new file mode 100644 index 00000000000..339f5e0d054 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { ViewElement } from '../../../components/elements'; +import { ActionCancelControl } from '../../../controls/ActionCancelControl'; +import { ActionExitControl } from '../../../controls/ActionExitControl'; +import { ActionStartControl } from '../../../controls/ActionStartControl'; +import { DataTableControl } from '../../../controls/DataTableControl'; +import { MessageControl } from '../../../controls/MessageControl'; +import { StatusDisplayControl } from '../../../controls/StatusDisplayControl'; +import { TitleControl } from '../../../controls/TitleControl'; + +import { STORAGE_BROWSER_BLOCK } from '../../../components'; + +import { DownloadViewProvider } from './DownloadViewProvider'; +import { useDownloadView } from './useDownloadView'; +import type { DownloadViewType } from './types'; +import { classNames } from '@aws-amplify/ui'; + +export const DownloadView: DownloadViewType = ({ className, ...props }) => { + const state = useDownloadView(props); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +DownloadView.displayName = 'DownloadView'; + +DownloadView.Provider = DownloadViewProvider; + +DownloadView.Cancel = ActionCancelControl; +DownloadView.Exit = ActionExitControl; +DownloadView.Message = MessageControl; +DownloadView.Start = ActionStartControl; +DownloadView.Statuses = StatusDisplayControl; +DownloadView.TasksTable = DataTableControl; +DownloadView.Title = TitleControl; diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.tsx new file mode 100644 index 00000000000..8fd0cc9eee9 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import { ControlsContextProvider } from '../../../controls/context'; +import { useDisplayText } from '../../../displayText'; + +import { useResolveTableData } from '../../hooks/useResolveTableData'; +import { + FILE_DATA_ITEM_TABLE_KEYS, + DOWNLOAD_TABLE_RESOLVERS, +} from '../../utils'; + +import type { DownloadViewProviderProps } from './types'; + +export function DownloadViewProvider({ + children, + ...props +}: DownloadViewProviderProps): React.JSX.Element { + const { DownloadView: displayText } = useDisplayText(); + + const { + actionCancelLabel, + actionExitLabel, + actionStartLabel, + title, + statusDisplayCanceledLabel, + statusDisplayCompletedLabel, + statusDisplayFailedLabel, + statusDisplayQueuedLabel, + getActionCompleteMessage, + } = displayText; + + const { + isProcessing, + isProcessingComplete, + statusCounts, + tasks: items, + onActionCancel, + onActionStart, + onActionExit, + onTaskRemove, + } = props; + + const message = isProcessingComplete + ? getActionCompleteMessage({ counts: statusCounts }) + : undefined; + + const tableData = useResolveTableData( + FILE_DATA_ITEM_TABLE_KEYS, + DOWNLOAD_TABLE_RESOLVERS, + { + items, + props: { displayText, isProcessing, onTaskRemove }, + } + ); + + return ( + + {children} + + ); +} diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/DownloadView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/DownloadView.spec.tsx new file mode 100644 index 00000000000..cd24f5277c7 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/DownloadView.spec.tsx @@ -0,0 +1,204 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { LocationData } from '../../../../actions'; +import { INITIAL_STATUS_COUNTS } from '../../../../tasks'; +import * as Config from '../../../../configuration'; +import * as UseDownloadViewModule from '../useDownloadView'; + +import { DownloadViewState } from '../types'; +import { DownloadView } from '../DownloadView'; + +jest.spyOn(Config, 'useGetActionInput').mockReturnValue(() => ({ + accountId: '123456789012', + bucket: 'XXXXXXXXXXX', + credentials: jest.fn(), + region: 'us-west-2', +})); + +jest.mock('../../../../displayText', () => ({ + ...jest.requireActual( + '../../../../displayText' + ), + useDisplayText: () => ({ + DownloadView: { getActionCompleteMessage: jest.fn() }, + }), +})); + +const mockControlsContextProvider = jest.fn( + (_: any) => 'ControlsContextProvider' +); +jest.mock('../../../../controls/context', () => ({ + ControlsContextProvider: (ctx: any) => mockControlsContextProvider(ctx), + useControlsContext: () => ({ actionConfig: {}, data: {} }), +})); + +const location: LocationData = { + bucket: 'bucket', + id: 'id', + permissions: ['get'], + prefix: `prefix/`, + type: 'PREFIX', +}; + +const onActionCancel = jest.fn(); +const onActionExit = jest.fn(); +const onActionStart = jest.fn(); + +const actionCallbacks = { onActionCancel, onActionExit, onActionStart }; + +const taskOne = { + status: 'QUEUED', + data: { + id: 'id', + key: 'some-prefix/test-item', + fileKey: 'test-item', + lastModified: new Date(), + size: 1000, + type: 'FILE', + }, + cancel: jest.fn(), + progress: undefined, + remove: jest.fn(), + message: undefined, +} as const; +const taskTwo = { + status: 'QUEUED', + data: { + id: 'id2', + key: 'some-prefix/test-item2', + fileKey: 'test-item2', + lastModified: new Date(), + size: 1000, + type: 'FILE', + }, + cancel: jest.fn(), + progress: undefined, + remove: jest.fn(), + message: undefined, +} as const; +const taskThree = { + status: 'QUEUED', + data: { + id: 'id3', + key: 'some-prefix/test-item3', + fileKey: 'test-item3', + lastModified: new Date(), + size: 1000, + type: 'FILE', + }, + cancel: jest.fn(), + progress: undefined, + remove: jest.fn(), + message: undefined, +} as const; + +const defaultViewState: DownloadViewState = { + ...actionCallbacks, + location: { + current: location, + path: 'some-prefix/', + key: 'prefix/some-prefix/', + }, + isProcessingComplete: false, + isProcessing: false, + statusCounts: { ...INITIAL_STATUS_COUNTS, QUEUED: 3, TOTAL: 3 }, + tasks: [taskOne, taskTwo, taskThree], +}; + +const useDownloadViewSpy = jest + .spyOn(UseDownloadViewModule, 'useDownloadView') + .mockReturnValue(defaultViewState); + +describe('DownloadView', () => { + afterEach(jest.clearAllMocks); + + it('has the expected composable components', () => { + expect(DownloadView.Cancel).toBeDefined(); + expect(DownloadView.Exit).toBeDefined(); + expect(DownloadView.Message).toBeDefined(); + expect(DownloadView.Start).toBeDefined(); + expect(DownloadView.Statuses).toBeDefined(); + expect(DownloadView.TasksTable).toBeDefined(); + expect(DownloadView.Title).toBeDefined(); + }); + + it('provides the expected values to `ControlsContextProvider` on initial render', () => { + render(); + + const { calls } = mockControlsContextProvider.mock; + expect(calls).toHaveLength(1); + expect(calls[0][0]).toMatchObject({ + data: { + isActionCancelDisabled: true, + isActionStartDisabled: false, + isActionExitDisabled: false, + statusCounts: defaultViewState.statusCounts, + }, + ...actionCallbacks, + }); + }); + + it('provides the expected values to `ControlsContextProvider` while processing', () => { + const processingViewState: DownloadViewState = { + ...defaultViewState, + isProcessing: true, + statusCounts: { ...defaultViewState.statusCounts, QUEUED: 0, PENDING: 3 }, + tasks: [ + { ...taskOne, status: 'PENDING' }, + { ...taskTwo, status: 'PENDING' }, + { ...taskThree, status: 'PENDING' }, + ], + }; + + useDownloadViewSpy.mockReturnValueOnce(processingViewState); + + render(); + + const { calls } = mockControlsContextProvider.mock; + expect(calls).toHaveLength(1); + expect(calls[0][0]).toMatchObject({ + data: { + isActionCancelDisabled: false, + isActionStartDisabled: true, + isActionExitDisabled: true, + statusCounts: processingViewState.statusCounts, + }, + ...actionCallbacks, + }); + }); + + it('provides the expected values to `ControlsContextProvider` post processing in the happy path', () => { + const postProcessingViewState: DownloadViewState = { + ...defaultViewState, + isProcessing: false, + isProcessingComplete: true, + statusCounts: { + ...defaultViewState.statusCounts, + QUEUED: 0, + COMPLETE: 3, + }, + tasks: [ + { ...taskOne, status: 'COMPLETE' }, + { ...taskTwo, status: 'COMPLETE' }, + { ...taskThree, status: 'COMPLETE' }, + ], + }; + + useDownloadViewSpy.mockReturnValue(postProcessingViewState); + + render(); + + const { calls } = mockControlsContextProvider.mock; + expect(calls).toHaveLength(1); + expect(calls[0][0]).toMatchObject({ + data: { + isActionCancelDisabled: true, + isActionStartDisabled: true, + isActionExitDisabled: false, + statusCounts: postProcessingViewState.statusCounts, + }, + ...actionCallbacks, + }); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/useDownloadView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/useDownloadView.spec.ts new file mode 100644 index 00000000000..e569b53eb6e --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/__tests__/useDownloadView.spec.ts @@ -0,0 +1,180 @@ +import { act, renderHook } from '@testing-library/react'; + +import type { FileDataItem } from '../../../../actions'; +import { useLocationItems } from '../../../../locationItems'; +import { useStore } from '../../../../store'; +import { INITIAL_STATUS_COUNTS } from '../../../../tasks'; +import { useAction } from '../../../../useAction'; + +import { useDownloadView } from '../useDownloadView'; + +jest.mock('../../../../fileItems'); +jest.mock('../../../../locationItems'); +jest.mock('../../../../store'); +jest.mock('../../../../useAction'); + +const fileDataItems: FileDataItem[] = [ + { + key: 'pretend-prefix/test-file.txt', + fileKey: 'test-file.txt', + lastModified: new Date(), + id: 'id-1', + size: 10, + type: 'FILE', + }, + { + key: 'pretend-prefix/deeply-nested/test-file.txt', + fileKey: 'test-file.txt', + lastModified: new Date(), + id: 'id-2', + size: 10, + type: 'FILE', + }, +]; + +const mockLocationItemsState = { fileDataItems }; + +describe('useDownloadView', () => { + const mockUseAction = jest.mocked(useAction); + const mockUseLocationItems = jest.mocked(useLocationItems); + const mockUseStore = jest.mocked(useStore); + + const mockCancel = jest.fn(); + const mockStoreDispatch = jest.fn(); + const mockLocationItemsDispatch = jest.fn(); + const mockHandleDownload = jest.fn(); + const mockReset = jest.fn(); + + beforeEach(() => { + mockUseLocationItems.mockReturnValue([ + mockLocationItemsState, + mockLocationItemsDispatch, + ]); + mockUseStore.mockReturnValue([ + { + actionType: 'DOWNLOAD', + location: { + current: { + prefix: 'test-prefix/', + bucket: 'bucket', + id: 'id', + permissions: ['get'], + type: 'PREFIX', + }, + path: '', + key: 'test-prefix/', + }, + }, + mockStoreDispatch, + ]); + + mockUseAction.mockReturnValue([ + { + isProcessing: false, + isProcessingComplete: false, + reset: mockReset, + statusCounts: { ...INITIAL_STATUS_COUNTS, QUEUED: 3, TOTAL: 3 }, + tasks: [ + { + status: 'QUEUED', + data: { key: 'test-item', id: 'id' }, + cancel: mockCancel, + message: 'test-message', + progress: undefined, + }, + { + status: 'QUEUED', + data: { key: 'test-item2', id: 'id2' }, + cancel: mockCancel, + message: 'test-message', + progress: undefined, + }, + { + status: 'QUEUED', + data: { key: 'test-item3', id: 'id3' }, + cancel: mockCancel, + message: 'test-message', + progress: undefined, + }, + ], + }, + mockHandleDownload, + ]); + }); + + afterEach(jest.clearAllMocks); + + it('should return the correct initial state', () => { + const { result } = renderHook(() => useDownloadView()); + + expect(result.current).toEqual( + expect.objectContaining({ + onActionCancel: expect.any(Function), + onActionExit: expect.any(Function), + onActionStart: expect.any(Function), + tasks: expect.any(Array), + }) + ); + + expect(result.current.statusCounts).toEqual({ + CANCELED: 0, + COMPLETE: 0, + FAILED: 0, + OVERWRITE_PREVENTED: 0, + PENDING: 0, + QUEUED: 3, + TOTAL: 3, + }); + }); + + it('should call processTasks when onActionStart is called', () => { + const { result } = renderHook(() => useDownloadView()); + + act(() => { + result.current.onActionStart(); + }); + + expect(mockHandleDownload).toHaveBeenCalledTimes(1); + }); + + it('should call cancel on tasks when onActionCancel is called', () => { + const { result } = renderHook(() => useDownloadView()); + + act(() => { + result.current.onActionCancel(); + }); + + expect(mockCancel).toHaveBeenCalledTimes(3); + }); + + it('should reset state when onActionExit is called', () => { + const mockOnExit = jest.fn(); + const { result } = renderHook(() => + useDownloadView({ onExit: mockOnExit }) + ); + + act(() => { + result.current.onActionExit(); + }); + + expect(mockOnExit).toHaveBeenCalledTimes(1); + expect(mockLocationItemsDispatch).toHaveBeenCalledTimes(1); + expect(mockLocationItemsDispatch).toHaveBeenCalledWith({ + type: 'RESET_LOCATION_ITEMS', + }); + expect(mockStoreDispatch).toHaveBeenCalledTimes(1); + expect(mockStoreDispatch).toHaveBeenCalledWith({ + type: 'RESET_ACTION_TYPE', + }); + }); + + it('provides the unmodified value of `fileDataItems` to `useAction` as `items`', () => { + renderHook(() => useDownloadView()); + + expect(mockUseAction).toHaveBeenCalledTimes(1); + expect(mockUseAction).toHaveBeenCalledWith( + 'download', + expect.objectContaining({ items: fileDataItems }) + ); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/index.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/index.ts new file mode 100644 index 00000000000..b11c69f98f4 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/index.ts @@ -0,0 +1,3 @@ +export { DownloadView } from './DownloadView'; +export { useDownloadView } from './useDownloadView'; +export * from './types'; diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/types.ts new file mode 100644 index 00000000000..7e8aa1fb743 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/types.ts @@ -0,0 +1,29 @@ +import type { DownloadHandlerData, LocationData } from '../../../actions'; +import type { + ActionViewProps, + ActionViewState, + ActionViewType, +} from '../types'; + +export interface DownloadViewState + extends ActionViewState {} +export interface DownloadViewProps extends ActionViewProps {} +export interface UseDownloadViewOptions { + onExit?: (location?: LocationData) => void; +} + +export interface DownloadViewProviderProps extends DownloadViewState { + children?: React.ReactNode; +} + +export interface DownloadViewType + extends ActionViewType { + Provider: (props: DownloadViewProviderProps) => React.JSX.Element; + Cancel: () => React.JSX.Element | null; + Exit: () => React.JSX.Element | null; + Message: () => React.JSX.Element | null; + Start: () => React.JSX.Element | null; + Statuses: () => React.JSX.Element | null; + TasksTable: () => React.JSX.Element | null; + Title: () => React.JSX.Element | null; +} diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.ts new file mode 100644 index 00000000000..0d7c1f669e2 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.ts @@ -0,0 +1,72 @@ +import React from 'react'; +import { isFunction } from '@aws-amplify/ui'; + +import type { FileDataItem } from '../../../actions'; +import type { Task } from '../../../tasks'; +import { useLocationItems } from '../../../locationItems'; +import { useStore } from '../../../store'; +import { useAction } from '../../../useAction'; + +import type { DownloadViewState, UseDownloadViewOptions } from './types'; + +// assign to constant to ensure referential equality +const EMPTY_ITEMS: FileDataItem[] = []; + +export const useDownloadView = ( + options?: UseDownloadViewOptions +): DownloadViewState => { + const { onExit: _onExit } = options ?? {}; + + const [{ location }, storeDispatch] = useStore(); + const [locationItems, locationItemsDispatch] = useLocationItems(); + const { current } = location; + const { fileDataItems: items = EMPTY_ITEMS } = locationItems; + + const [processState, handleProcess] = useAction('download', { + items, + }); + + const { isProcessing, isProcessingComplete, statusCounts, tasks } = + processState; + + const onActionStart = () => { + if (!current) return; + handleProcess(); + }; + + const onActionCancel = () => { + tasks.forEach((task) => { + // Calling cancel on task works only on queued tasks. + // In case of download, all download presigned url open at once + // When certain threshold is reached for queuing inside StorageBrowser, cancel might be possible. + if (isFunction(task.cancel)) task.cancel(); + }); + }; + + const onActionExit = () => { + // clear files state + locationItemsDispatch({ type: 'RESET_LOCATION_ITEMS' }); + // clear selected action + storeDispatch({ type: 'RESET_ACTION_TYPE' }); + if (isFunction(_onExit)) _onExit(current); + }; + + const onTaskRemove = React.useCallback( + ({ data }: Task) => { + locationItemsDispatch({ type: 'REMOVE_LOCATION_ITEM', id: data.id }); + }, + [locationItemsDispatch] + ); + + return { + isProcessing, + isProcessingComplete, + location, + statusCounts, + tasks, + onActionCancel, + onActionExit, + onActionStart, + onTaskRemove, + }; +}; diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/index.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/index.ts index e580fd4e218..703641a6b8a 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/index.ts @@ -12,6 +12,12 @@ export type { DeleteViewState, } from './DeleteView'; export { DeleteView, useDeleteView } from './DeleteView'; +export type { + DownloadViewProps, + DownloadViewType, + DownloadViewState, +} from './DownloadView'; +export { DownloadView, useDownloadView } from './DownloadView'; export { LocationActionView } from './LocationActionView'; export type { UploadViewProps, diff --git a/packages/react-storage/src/components/StorageBrowser/views/__tests__/__snapshots__/useView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/views/__tests__/__snapshots__/useView.spec.ts.snap index 963383cd9f4..63c54094821 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/__tests__/__snapshots__/useView.spec.ts.snap +++ b/packages/react-storage/src/components/StorageBrowser/views/__tests__/__snapshots__/useView.spec.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`useView throws when provided an unexepected key 1`] = `"Value of \`unexpected!\` cannot be used to index \`useView\`"`; +exports[`useView throws when provided an unexpected key 1`] = `"Value of \`unexpected!\` cannot be used to index \`useView\`"`; diff --git a/packages/react-storage/src/components/StorageBrowser/views/__tests__/useView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/__tests__/useView.spec.ts index 5b1d356e858..a0690936789 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/__tests__/useView.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/__tests__/useView.spec.ts @@ -15,7 +15,7 @@ describe('useView', () => { } ); - it('throws when provided an unexepected key', () => { + it('throws when provided an unexpected key', () => { // turn off console.error logging for unhappy path test case jest.spyOn(console, 'error').mockImplementation(() => {}); diff --git a/packages/react-storage/src/components/StorageBrowser/views/context/actionViews.tsx b/packages/react-storage/src/components/StorageBrowser/views/context/actionViews.tsx index a806743917b..ba03152721d 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/context/actionViews.tsx +++ b/packages/react-storage/src/components/StorageBrowser/views/context/actionViews.tsx @@ -5,6 +5,7 @@ import { UploadView } from '../LocationActionView/UploadView'; import { CreateFolderView } from '../LocationActionView/CreateFolderView'; import { CopyView } from '../LocationActionView/CopyView'; import { DeleteView } from '../LocationActionView/DeleteView'; +import { DownloadView } from '../LocationActionView/DownloadView'; import type { DefaultActionViewsByActionName } from '../types'; @@ -12,6 +13,7 @@ export const DEFAULT_ACTION_VIEWS: DefaultActionViewsByActionName = { createFolder: CreateFolderView, copy: CopyView, delete: DeleteView, + download: DownloadView, upload: UploadView, }; diff --git a/packages/react-storage/src/components/StorageBrowser/views/index.ts b/packages/react-storage/src/components/StorageBrowser/views/index.ts index 9339b81f1e0..2b15b3ec76a 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/index.ts @@ -3,6 +3,7 @@ export type { CopyViewType, CreateFolderViewType, DeleteViewType, + DownloadViewType, LocationActionViewProps, LocationActionViewType, UploadViewType, @@ -10,6 +11,7 @@ export type { export { CopyView, CreateFolderView, + DownloadView, DeleteView, LocationActionView, UploadView, diff --git a/packages/react-storage/src/components/StorageBrowser/views/types.ts b/packages/react-storage/src/components/StorageBrowser/views/types.ts index 78c663da686..5ab912ddca6 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/types.ts @@ -2,11 +2,12 @@ import type React from 'react'; import type { LocationData } from '../actions'; import type { - LocationActionViewProps, - UploadViewProps, - CreateFolderViewProps, CopyViewProps, + CreateFolderViewProps, DeleteViewProps, + DownloadViewProps, + LocationActionViewProps, + UploadViewProps, } from './LocationActionView'; import type { LocationDetailViewProps } from './LocationDetailView'; import type { LocationsViewProps } from './LocationsView'; @@ -41,15 +42,17 @@ export interface PrimaryViews { } export interface DefaultActionViews { - CreateFolderView: (props: CreateFolderViewProps) => React.JSX.Element | null; CopyView: (props: CopyViewProps) => React.JSX.Element | null; + CreateFolderView: (props: CreateFolderViewProps) => React.JSX.Element | null; DeleteView: (props: DeleteViewProps) => React.JSX.Element | null; + DownloadView: (props: DownloadViewProps) => React.JSX.Element | null; UploadView: (props: UploadViewProps) => React.JSX.Element | null; } export interface DefaultActionViewsByActionName { - createFolder: (props: CreateFolderViewProps) => React.JSX.Element | null; copy: (props: CopyViewProps) => React.JSX.Element | null; + createFolder: (props: CreateFolderViewProps) => React.JSX.Element | null; delete: (props: DeleteViewProps) => React.JSX.Element | null; + download: (props: DownloadViewProps) => React.JSX.Element | null; upload: (props: UploadViewProps) => React.JSX.Element | null; } diff --git a/packages/react-storage/src/components/StorageBrowser/views/useView.ts b/packages/react-storage/src/components/StorageBrowser/views/useView.ts index 0d63ad9741c..c8b2f13bbaf 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/useView.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/useView.ts @@ -1,14 +1,16 @@ import type { - UploadViewState, + CopyViewState, CreateFolderViewState, DeleteViewState, - CopyViewState, + DownloadViewState, + UploadViewState, } from './LocationActionView'; import { useCopyView, useCreateFolderView, - useUploadView, useDeleteView, + useDownloadView, + useUploadView, } from './LocationActionView'; import type { LocationDetailViewState } from './LocationDetailView'; import { useLocationDetailView } from './LocationDetailView'; @@ -18,6 +20,7 @@ import { useLocationsView } from './LocationsView'; interface DefaultUseViewStates { Copy: CopyViewState; CreateFolder: CreateFolderViewState; + Download: DownloadViewState; Delete: DeleteViewState; LocationDetail: LocationDetailViewState; Locations: LocationsViewState; @@ -31,6 +34,7 @@ type UseViewHooks = { export const USE_VIEW_HOOKS: UseViewHooks = { Copy: useCopyView, CreateFolder: useCreateFolderView, + Download: useDownloadView, Delete: useDeleteView, LocationDetail: useLocationDetailView, Locations: useLocationsView, diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/index.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/index.ts index 1fd81a99216..f55e6f9f4fe 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/index.ts @@ -4,4 +4,5 @@ export { DELETE_TABLE_RESOLVERS, UPLOAD_TABLE_KEYS, UPLOAD_TABLE_RESOLVERS, + DOWNLOAD_TABLE_RESOLVERS, } from './tableResolvers'; diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__testUtils__/tasks.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__testUtils__/tasks.ts index 6bff0bfee5d..78339b4b40f 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__testUtils__/tasks.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__testUtils__/tasks.ts @@ -1,12 +1,7 @@ import { MULTIPART_UPLOAD_THRESHOLD_BYTES } from '../../../../actions/handlers/constants'; import type { TaskStatus } from '../../../../tasks'; -import type { - FileDataTask, - CopyActionTask, - DeleteActionTask, - UploadActionTask, -} from '../types'; +import type { FileDataTask, CopyActionTask, UploadActionTask } from '../types'; type MockFileDataTaskStatus = Exclude; @@ -88,15 +83,15 @@ export const MOCK_COPY_TASKS: MockFileDataTasks = { }, }; -const MOCK_DELETE_DATA: Omit = { +const MOCK_FILE_DATA: Omit = { ...MOCK_FILE_DATA_ITEM_BASE, key: `${MOCK_PREFIX}file1.txt`, }; -export const MOCK_DELETE_TASKS: MockFileDataTasks = { +export const MOCK_FILE_DATA_TASKS: MockFileDataTasks = { CANCELED: { data: { - ...MOCK_DELETE_DATA, + ...MOCK_FILE_DATA, id: 'CANCELED-ID', }, cancel: undefined, @@ -106,7 +101,7 @@ export const MOCK_DELETE_TASKS: MockFileDataTasks = { }, FAILED: { data: { - ...MOCK_DELETE_DATA, + ...MOCK_FILE_DATA, id: 'FAILED-ID', }, status: 'FAILED', @@ -115,7 +110,7 @@ export const MOCK_DELETE_TASKS: MockFileDataTasks = { }, COMPLETE: { data: { - ...MOCK_DELETE_DATA, + ...MOCK_FILE_DATA, id: 'COMPLETE-ID', }, status: 'COMPLETE', @@ -124,7 +119,7 @@ export const MOCK_DELETE_TASKS: MockFileDataTasks = { }, QUEUED: { data: { - ...MOCK_DELETE_DATA, + ...MOCK_FILE_DATA, id: 'QUEUED-ID', }, cancel: jest.fn(), @@ -134,7 +129,7 @@ export const MOCK_DELETE_TASKS: MockFileDataTasks = { }, PENDING: { data: { - ...MOCK_DELETE_DATA, + ...MOCK_FILE_DATA, id: 'PENDING-ID', }, status: 'PENDING', diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/copyActionTask.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/copyActionTask.spec.ts index 9010f199324..f6184b74fcf 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/copyActionTask.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/copyActionTask.spec.ts @@ -33,7 +33,7 @@ describe('COPY_TABLE_RESOLVERS', () => { describe('getCell', () => { it.each(FILE_DATA_ITEM_TABLE_KEYS)( - 'returns the expect cell `key` for a "%s" table `key`', + 'returns the expected cell `key` for a "%s" table `key`', (key) => { const data = { key, @@ -240,7 +240,7 @@ describe('COPY_TABLE_RESOLVERS', () => { describe('getHeader', () => { // filter out cancel, does not allow sorting it.each(FILE_DATA_ITEM_TABLE_KEYS.filter((key) => key !== 'cancel'))( - 'returns the expect header data for a %s column', + 'returns the expected header data for a %s column', (key) => { const data = { key, props: mockProps }; const output = COPY_TABLE_RESOLVERS.getHeader(data); @@ -253,7 +253,7 @@ describe('COPY_TABLE_RESOLVERS', () => { } ); - it('returns the expect header data for a cancel column', () => { + it('returns the expected header data for a cancel column', () => { const key = 'cancel' as const; const data = { key, props: mockProps }; const output = COPY_TABLE_RESOLVERS.getHeader(data); diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/deleteActionTask.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/deleteActionTask.spec.ts index 2421eb1ae96..a469cfce79d 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/deleteActionTask.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/deleteActionTask.spec.ts @@ -1,7 +1,7 @@ import { capitalize } from '@aws-amplify/ui'; import { DeleteViewDisplayText } from '../../../../displayText'; -import { MOCK_DELETE_TASKS } from '../__testUtils__/tasks'; +import { MOCK_FILE_DATA_TASKS } from '../__testUtils__/tasks'; import { FILE_DATA_ITEM_TABLE_KEYS, STATUS_ICONS, @@ -20,7 +20,7 @@ const mockDisplayText: DeleteViewDisplayText = { tableColumnCancelHeader: 'Cancel', }; -const mockItems = Object.values(MOCK_DELETE_TASKS); +const mockItems = Object.values(MOCK_FILE_DATA_TASKS); const mockProps: DeleteTableResolverProps = { displayText: mockDisplayText, @@ -33,17 +33,17 @@ describe('DELETE_TABLE_RESOLVERS', () => { describe('getCell', () => { it.each(FILE_DATA_ITEM_TABLE_KEYS)( - 'returns the expect cell `key` for a "%s" table `key`', + 'returns the expected cell `key` for a "%s" table `key`', (key) => { const data = { key, - item: MOCK_DELETE_TASKS.QUEUED, + item: MOCK_FILE_DATA_TASKS.QUEUED, props: mockProps, }; const cell = DELETE_TABLE_RESOLVERS.getCell(data); expect(cell).toBeDefined(); - expect(cell.key).toBe(`${key}-${MOCK_DELETE_TASKS.QUEUED.data.id}`); + expect(cell.key).toBe(`${key}-${MOCK_FILE_DATA_TASKS.QUEUED.data.id}`); } ); @@ -128,7 +128,7 @@ describe('DELETE_TABLE_RESOLVERS', () => { const key = 'status'; const item = { - ...MOCK_DELETE_TASKS.FAILED, + ...MOCK_FILE_DATA_TASKS.FAILED, status: 'OVERWRITE_PREVENTED', } as DeleteActionTask; @@ -171,7 +171,7 @@ describe('DELETE_TABLE_RESOLVERS', () => { describe('cancel cell', () => { it('returns the expected cell values for an `item` with "QUEUED" status when `isProcessing` is "true"', () => { const key = 'cancel'; - const item = MOCK_DELETE_TASKS.QUEUED; + const item = MOCK_FILE_DATA_TASKS.QUEUED; const output = DELETE_TABLE_RESOLVERS.getCell({ item, key, @@ -192,7 +192,7 @@ describe('DELETE_TABLE_RESOLVERS', () => { it('returns the expected cell values for an `item` with "QUEUED" status when `isProcessing` is "false"', () => { const key = 'cancel'; - const item = MOCK_DELETE_TASKS.QUEUED; + const item = MOCK_FILE_DATA_TASKS.QUEUED; const output = DELETE_TABLE_RESOLVERS.getCell({ item, key, @@ -211,7 +211,7 @@ describe('DELETE_TABLE_RESOLVERS', () => { }); }); - it.each([MOCK_DELETE_TASKS.PENDING, MOCK_DELETE_TASKS.COMPLETE])( + it.each([MOCK_FILE_DATA_TASKS.PENDING, MOCK_FILE_DATA_TASKS.COMPLETE])( 'returns the expected cell values for an `item` with "$status" status', (item) => { const key = 'cancel'; @@ -240,7 +240,7 @@ describe('DELETE_TABLE_RESOLVERS', () => { describe('getHeader', () => { // filter out cancel, does not allow sorting it.each(FILE_DATA_ITEM_TABLE_KEYS.filter((key) => key !== 'cancel'))( - 'returns the expect header data for a %s column', + 'returns the expected header data for a %s column', (key) => { const data = { key, props: mockProps }; const output = DELETE_TABLE_RESOLVERS.getHeader(data); @@ -253,7 +253,7 @@ describe('DELETE_TABLE_RESOLVERS', () => { } ); - it('returns the expect header data for a cancel column', () => { + it('returns the expected header data for a cancel column', () => { const key = 'cancel' as const; const data = { key, props: mockProps }; const output = DELETE_TABLE_RESOLVERS.getHeader(data); @@ -269,11 +269,11 @@ describe('DELETE_TABLE_RESOLVERS', () => { describe('getRowKey', () => { it('resolves a row key as expected', () => { const rowKey = DELETE_TABLE_RESOLVERS.getRowKey({ - item: MOCK_DELETE_TASKS.PENDING, + item: MOCK_FILE_DATA_TASKS.PENDING, props: mockProps, }); - expect(rowKey).toBe(MOCK_DELETE_TASKS.PENDING.data.id); + expect(rowKey).toBe(MOCK_FILE_DATA_TASKS.PENDING.data.id); }); }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/downloadActionTask.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/downloadActionTask.spec.ts new file mode 100644 index 00000000000..1f73a10b1c3 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/downloadActionTask.spec.ts @@ -0,0 +1,278 @@ +import { capitalize } from '@aws-amplify/ui'; +import { DownloadViewDisplayText } from '../../../../displayText'; + +import { MOCK_FILE_DATA_TASKS } from '../__testUtils__/tasks'; +import { + FILE_DATA_ITEM_TABLE_KEYS, + STATUS_ICONS, + STATUS_LABELS, +} from '../constants'; +import { DownloadActionTask, DownloadTableResolverProps } from '../types'; +import { DOWNLOAD_TABLE_RESOLVERS } from '../downloadResolvers'; + +const mockDisplayText: DownloadViewDisplayText = { + tableColumnNameHeader: 'Name', + tableColumnFolderHeader: 'Folder', + tableColumnTypeHeader: 'Type', + tableColumnSizeHeader: 'Size', + tableColumnStatusHeader: 'Status', + tableColumnCancelHeader: 'Cancel', +}; + +const mockItems = Object.values(MOCK_FILE_DATA_TASKS); + +const mockProps: DownloadTableResolverProps = { + displayText: mockDisplayText, + isProcessing: true, + onTaskRemove: jest.fn(), +}; + +describe('DOWNLOAD_TABLE_RESOLVERS', () => { + beforeEach(jest.clearAllMocks); + + describe('getCell', () => { + it.each(FILE_DATA_ITEM_TABLE_KEYS)( + 'returns the expected cell `key` for a "%s" table `key`', + (key) => { + const data = { + key, + item: MOCK_FILE_DATA_TASKS.QUEUED, + props: mockProps, + }; + const cell = DOWNLOAD_TABLE_RESOLVERS.getCell(data); + + expect(cell).toBeDefined(); + expect(cell.key).toBe(`${key}-${MOCK_FILE_DATA_TASKS.QUEUED.data.id}`); + } + ); + + describe('text cell resolvers', () => { + it.each(mockItems)( + 'returns the expected cell values when `key` is "name" for an item with "$status" status', + (item) => { + const key = 'name'; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'text', + content: { + icon: STATUS_ICONS[item.status], + text: item.data.fileKey, + }, + }); + } + ); + + it.each(mockItems)( + 'returns the expected cell values when `key` is `folder` for an item with "$status" status', + (item) => { + const key = 'folder'; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'text', + content: { text: 'test-folder/' }, + }); + } + ); + + it.each(mockItems)( + 'returns the expected cell values when `key` is `status` for an item with "$status" status', + (item) => { + const key = 'status'; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'text', + content: { text: mockDisplayText[STATUS_LABELS[item.status]] }, + }); + } + ); + + it.each(mockItems)( + 'returns the expected cell values when `key` is `type` for an item with "$status" status', + (item) => { + const key = 'type'; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'text', + content: { text: 'txt' }, + }); + } + ); + + describe('status cell', () => { + it('returns an empty string for `context.text` when `item.status` is "OVERWRITE_PREVENTED"', () => { + const key = 'status'; + + const item = { + ...MOCK_FILE_DATA_TASKS.FAILED, + status: 'OVERWRITE_PREVENTED', + } as DownloadActionTask; + + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'text', + content: { text: '' }, + }); + }); + }); + }); + + describe('number cell resolvers', () => { + it.each(mockItems)( + 'returns the expected cell values when `key` is `size` for an item with "$status" status', + (item) => { + const key = 'size'; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'number', + content: { value: item.data.size, displayValue: '1.0 kB' }, + }); + } + ); + }); + + describe('button cell resolvers', () => { + describe('cancel cell', () => { + it('returns the expected cell values for an `item` with "QUEUED" status when `isProcessing` is "true"', () => { + const key = 'cancel'; + const item = MOCK_FILE_DATA_TASKS.QUEUED; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'button', + content: { + ariaLabel: `Cancel item: ${item.data.fileKey}`, + isDisabled: false, + onClick: expect.any(Function), + icon: 'cancel', + }, + }); + }); + + it('returns the expected cell values for an `item` with "QUEUED" status when `isProcessing` is "false"', () => { + const key = 'cancel'; + const item = MOCK_FILE_DATA_TASKS.QUEUED; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: { ...mockProps, isProcessing: false }, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'button', + content: { + ariaLabel: `Remove item: ${item.data.fileKey}`, + isDisabled: false, + onClick: expect.any(Function), + icon: 'cancel', + }, + }); + }); + + it.each([MOCK_FILE_DATA_TASKS.PENDING, MOCK_FILE_DATA_TASKS.COMPLETE])( + 'returns the expected cell values for an `item` with "$status" status', + (item) => { + const key = 'cancel'; + const output = DOWNLOAD_TABLE_RESOLVERS.getCell({ + item, + key, + props: mockProps, + }); + + expect(output).toStrictEqual({ + key: `${key}-${item.data.id}`, + type: 'button', + content: { + ariaLabel: `Cancel item: ${item.data.fileKey}`, + isDisabled: true, + onClick: undefined, + icon: 'cancel', + }, + }); + } + ); + }); + }); + }); + + describe('getHeader', () => { + // filter out cancel, does not allow sorting + it.each(FILE_DATA_ITEM_TABLE_KEYS.filter((key) => key !== 'cancel'))( + 'returns the expected header data for a %s column', + (key) => { + const data = { key, props: mockProps }; + const output = DOWNLOAD_TABLE_RESOLVERS.getHeader(data); + + expect(output).toStrictEqual({ + key, + type: 'sort', + content: { label: capitalize(key) }, + }); + } + ); + + it('returns the expected header data for a cancel column', () => { + const key = 'cancel' as const; + const data = { key, props: mockProps }; + const output = DOWNLOAD_TABLE_RESOLVERS.getHeader(data); + + expect(output).toStrictEqual({ + key, + type: 'text', + content: { text: capitalize(key) }, + }); + }); + }); + + describe('getRowKey', () => { + it('resolves a row key as expected', () => { + const rowKey = DOWNLOAD_TABLE_RESOLVERS.getRowKey({ + item: MOCK_FILE_DATA_TASKS.PENDING, + props: mockProps, + }); + + expect(rowKey).toBe(MOCK_FILE_DATA_TASKS.PENDING.data.id); + }); + }); +}); diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/utils.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/utils.spec.ts index 59ca391eb0b..fe82ea78d8a 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/utils.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/__tests__/utils.spec.ts @@ -1,6 +1,6 @@ import { MOCK_COPY_TASKS, - MOCK_DELETE_TASKS, + MOCK_FILE_DATA_TASKS, MOCK_UPLOAD_TASKS_SINGLE_PART, } from '../__testUtils__/tasks'; import { @@ -14,8 +14,8 @@ import { describe('table resolver utils', () => { describe.each([ { name: 'copy', tasks: MOCK_COPY_TASKS }, - { name: 'delete', tasks: MOCK_DELETE_TASKS }, - ])('getCopyOrDeleteCancelCellContent with $name tasks', ({ tasks }) => { + { name: 'delete', tasks: MOCK_FILE_DATA_TASKS }, + ])('getFileDataCancelCellContent with $name tasks', ({ tasks }) => { const onTaskRemove = jest.fn(); const props = { displayText: {}, onTaskRemove }; diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/downloadResolvers.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/downloadResolvers.ts new file mode 100644 index 00000000000..1f4e8f53c46 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/downloadResolvers.ts @@ -0,0 +1,46 @@ +import { capitalize } from '@aws-amplify/ui'; + +import { isDownloadViewDisplayTextKey } from '../../../displayText'; + +import { STATUS_LABELS } from './constants'; +import type { FileDataTaskTableResolvers, GetFileDataCell } from './types'; +import { cancel, folder, getFileDataCellKey, name, size, type } from './utils'; + +const status: GetFileDataCell = (data) => { + const key = getFileDataCellKey(data); + const { + item: { status }, + props: { displayText }, + } = data; + + const statusLabelKey = STATUS_LABELS[status]; + + const text = isDownloadViewDisplayTextKey(statusLabelKey) + ? displayText[statusLabelKey] + : ''; + + return { key, type: 'text', content: { text } }; +}; + +const DOWNLOAD_CELL_RESOLVERS = { + name, + folder, + type, + size, + status, + cancel, +}; + +export const DOWNLOAD_TABLE_RESOLVERS: FileDataTaskTableResolvers = { + getCell: (data) => DOWNLOAD_CELL_RESOLVERS[data.key](data), + getHeader: ({ key, props: { displayText } }) => { + const text = displayText[`tableColumn${capitalize(key)}Header`]; + + if (key === 'cancel') { + return { key, type: 'text', content: { text } }; + } + + return { key, type: 'sort', content: { label: text } }; + }, + getRowKey: ({ item }) => item.data.id, +}; diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/index.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/index.ts index be1d39548a6..66bdee68635 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/index.ts @@ -1,4 +1,5 @@ export { COPY_TABLE_RESOLVERS } from './copyResolvers'; -export { UPLOAD_TABLE_KEYS, UPLOAD_TABLE_RESOLVERS } from './uploadResolvers'; export { DELETE_TABLE_RESOLVERS } from './deleteResolvers'; +export { DOWNLOAD_TABLE_RESOLVERS } from './downloadResolvers'; +export { UPLOAD_TABLE_KEYS, UPLOAD_TABLE_RESOLVERS } from './uploadResolvers'; export { FILE_DATA_ITEM_TABLE_KEYS } from './constants'; diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/types.ts b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/types.ts index d3eaa699e64..4dcde54b74a 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/utils/tableResolvers/types.ts @@ -1,13 +1,15 @@ import type { CopyHandlerData, DeleteHandlerData, - UploadHandlerData, - TaskData, + DownloadHandlerData, OptionalFileData, + TaskData, + UploadHandlerData, } from '../../../actions'; import type { CopyViewDisplayText, DeleteViewDisplayText, + DownloadViewDisplayText, UploadViewDisplayText, } from '../../../displayText'; import type { Task } from '../../../tasks'; @@ -15,6 +17,7 @@ import type { DataTableResolvers } from '../../hooks/useResolveTableData'; export interface CopyActionTask extends Task {} export interface DeleteActionTask extends Task {} +export interface DownloadActionTask extends Task {} export interface UploadActionTask extends Task {} export interface FileDataTask extends Task {} @@ -31,6 +34,12 @@ export interface CopyTableResolverProps export interface DeleteTableResolverProps extends ActionTableResolverProps {} +export interface DownloadTableResolverProps + extends ActionTableResolverProps< + DownloadViewDisplayText, + DownloadActionTask + > {} + export interface UploadTableResolverProps extends ActionTableResolverProps { isMultipartUpload: (file: File) => boolean; From 418c7eb73e6c37ff765457c6b06a185de29e8ca3 Mon Sep 17 00:00:00 2001 From: Tiffany Yeung Date: Fri, 30 May 2025 18:07:49 -0700 Subject: [PATCH 3/5] chore(storage-browser): add multi-file download docs examples --- .../storage-browser/examples/Composed.tsx | 3 ++ .../examples/ComposedDownloadView.tsx | 15 +++++++ .../storage-browser/examples/Custom.tsx | 7 ++- .../examples/CustomDeleteView.tsx | 2 +- .../examples/CustomDownloadView.tsx | 23 ++++++++++ .../examples/defaultActions.ts | 27 +++++++++--- .../storage/storage-browser/react.mdx | 44 +++++++++++++++++++ 7 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDownloadView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDownloadView.tsx diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx index d77d67440c8..5b5c5744c50 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx @@ -5,6 +5,7 @@ import { StorageBrowser, useView } from './StorageBrowser'; import { ComposedCopyView } from './ComposedCopyView'; import { ComposedCreateFolderView } from './ComposedCreateFolderView'; import { ComposedDeleteView } from './ComposedDeleteView'; +import { ComposedDownloadView } from './ComposedDownloadView'; import { ComposedUploadView } from './ComposedUploadView'; function LocationsView() { @@ -52,6 +53,8 @@ function MyLocationActionView() { return ; case 'delete': return ; + case 'download': + return ; case 'upload': return ; default: diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDownloadView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDownloadView.tsx new file mode 100644 index 00000000000..d45da749030 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDownloadView.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { StorageBrowser, useView } from './StorageBrowser'; + +export function ComposedDownloadView({ onExit }: { onExit: () => void }) { + const state = useView('Download'); + + return ( + + + + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx index 018f81b0704..e2cffb40ef7 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import { StorageBrowser, useView } from './StorageBrowser'; -import { CustomDeleteView } from './CustomDeleteView'; import { CustomCopyView } from './CustomCopyView'; import { CustomCreateFolderView } from './CustomCreateFolderView'; -import { CustomUploadView } from './CustomUploadView'; +import { CustomDeleteView } from './CustomDeleteView'; +import { CustomDownloadView } from './CustomDownloadView'; import { CustomLocationsView } from './CustomLocationsView'; +import { CustomUploadView } from './CustomUploadView'; function MyLocationActionView() { const state = useView('LocationDetail'); @@ -19,6 +20,8 @@ function MyLocationActionView() { return ; case 'delete': return ; + case 'download': + return ; case 'upload': return ; default: diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx index a355377d0ae..d53239d5f27 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Button, Flex, Text } from '@aws-amplify/ui-react'; -import { StorageBrowser, useView } from './StorageBrowser'; +import { useView } from './StorageBrowser'; export function CustomDeleteView({ onExit }: { onExit: () => void }) { const state = useView('Delete'); diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDownloadView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDownloadView.tsx new file mode 100644 index 00000000000..f79f015dc2f --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDownloadView.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Button, Flex, Text, View } from '@aws-amplify/ui-react'; +import { useView } from './StorageBrowser'; + +export function CustomDownloadView({ onExit }: { onExit: () => void }) { + const state = useView('Download'); + + return ( + + + {state.tasks.map((task) => ( + + {task.data.key} + + ))} + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts index c99b313df37..29a6fd02a95 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts @@ -1,6 +1,7 @@ import { CreateStorageBrowserInput, DeleteHandlerOutput, + DownloadHandlerOutput, UploadHandlerOutput, } from '@aws-amplify/ui-react-storage/browser'; import uniqueId from 'lodash/uniqueId'; @@ -59,13 +60,25 @@ export const defaultActions: CreateStorageBrowserInput['actions']['default'] = { }, viewName: 'DeleteView', }, - download: () => { - return { - result: Promise.resolve({ - status: 'COMPLETE' as const, - value: { url: new URL('') }, - }), - }; + download: { + actionListItem: { + icon: 'download', + label: 'Download', + }, + handler: () => { + const result: DownloadHandlerOutput['result'] = new Promise((resolve) => { + setTimeout(() => { + resolve({ + status: 'COMPLETE' as const, + value: { url: new URL('') }, + }); + }, 500); + }); + return { + result, + }; + }, + viewName: 'DownloadView', }, upload: { actionListItem: { diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx index 84c1de64cf7..8cb42999486 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx @@ -362,6 +362,7 @@ Below you wll find examples of how to compose each view. See [View reference](#v Locations Location Detail Upload + Download Copy CreateFolder Delete @@ -396,6 +397,12 @@ Below you wll find examples of how to compose each view. See [View reference](#v ``` + + + ```jsx file=./examples/ComposedDownloadView.tsx + ``` + + ```jsx file=./examples/ComposedCopyView.tsx @@ -424,6 +431,7 @@ The `useView()` hook takes a single argument which is the name of the view you w StorageBrowser Locations Upload + Download Copy CreateFolder Delete @@ -457,6 +465,12 @@ The `useView()` hook takes a single argument which is the name of the view you w ```jsx file=./examples/CustomUploadView.tsx ``` + + + + ```jsx file=./examples/CustomDownloadView.tsx + ``` + @@ -824,6 +838,36 @@ interface UploadViewState { * `` +### Download view + +#### Download view state + +```ts +interface DownloadViewState { + isProcessing: boolean; + isProcessingComplete: boolean; + location: LocationState; + onActionCancel: () => void; + onActionExit: () => void; + onActionStart: () => void; + onTaskRemove?: (task: Task) => void; + statusCounts: StatusCounts; + tasks: Task[]; +} +``` + +#### Download view components + +* `` +* `` +* `` +* `` +* `` +* `` +* `` +* `` + + ### Copy view #### Copy view state From 6551f1d1fea294dd7abe932e2362d27ae3724999 Mon Sep 17 00:00:00 2001 From: Tiffany Yeung Date: Mon, 9 Jun 2025 16:29:04 -0700 Subject: [PATCH 4/5] fix: replace URL created from empty string to `null`, add wording changes --- .../storage/storage-browser/examples/defaultActions.ts | 4 +++- .../connected-components/storage/storage-browser/react.mdx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts index 29a6fd02a95..fda7d3a7f9f 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts @@ -70,7 +70,9 @@ export const defaultActions: CreateStorageBrowserInput['actions']['default'] = { setTimeout(() => { resolve({ status: 'COMPLETE' as const, - value: { url: new URL('') }, + // creating URL with empty string will throw TypeError, + // preventing promise from resolving + value: { url: null }, }); }, 500); }); diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx index 8cb42999486..2abb88e0129 100644 --- a/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx @@ -422,7 +422,7 @@ Below you wll find examples of how to compose each view. See [View reference](#v The `createStorageBrowser` function returns a `useView()` hook that lets you use the Storage Browser's internal state and event handlers to build your own UI on top the Storage Browser functionality. -The `useView()` hook takes a single argument which is the name of the view you want to get the UI state and event handlers for. The available views are `Locations`, `LocationDetails`, `Copy`, `Upload`, `Delete`, and `CreateFolder`. The return value of the hook will have all the necessary internal state and event handlers required to build that view. In fact, the Storage Browser component itself uses the `useView()` to manage its state, so you can build the UI exactly how we do! +The `useView()` hook takes a single argument which is the name of the view you want to get the UI state and event handlers for. The available views are `Locations`, `LocationDetails`, `Copy`, `Upload`, `Download`, `Delete`, and `CreateFolder`. The return value of the hook will have all the necessary internal state and event handlers required to build that view. In fact, the Storage Browser component itself uses the `useView()` to manage its state, so you can build the UI exactly how we do! @@ -493,7 +493,7 @@ The following `default` actions are invoked by the default UI views: * `copy`: Copy data to another location within a `bucket` or `prefix` in [Copy action view](#copy-view). * `createFolder`: Create a folder within a `bucket` or `prefix` in [CreateFolder action view](#createfolder-view). * `delete`: Delete a given object in [Delete action view](#delete-view). -* `download`: Download a given object in [LocationDetails view](#locationdetails-view) using an [anchor link](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download). +* `download`: Download a given object in [LocationDetails view](#locationdetails-view) using an [anchor link](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download) or in [Download action view](#download-view). * `upload`: Upload a file in [Upload action view](#upload-view). `defaultActionConfigs` allows access to the default implementation of these actions. Below is an example to instrument the default upload action to count the files being uploaded. From 0e37a1406ce7144ff99fffa6c5b90df8d38e5e5c Mon Sep 17 00:00:00 2001 From: Michael Sober Date: Thu, 10 Jul 2025 14:35:26 +0200 Subject: [PATCH 5/5] chore: revert workflows used for testing --- .github/workflows/codeql.yml | 2 +- .github/workflows/test-internal-prs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0b12bf94391..f2edd1f7b15 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,7 @@ name: 'CodeQL' on: push: - branches: feat-sb/multi-file-download/main + branches: ['main'] jobs: analyze: diff --git a/.github/workflows/test-internal-prs.yml b/.github/workflows/test-internal-prs.yml index febb57fadf6..ee41622aaa6 100644 --- a/.github/workflows/test-internal-prs.yml +++ b/.github/workflows/test-internal-prs.yml @@ -13,7 +13,7 @@ concurrency: on: pull_request: - branches: feat-sb/multi-file-download/main + branches: [main, hotfix] types: [opened, synchronize, labeled] jobs: