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/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..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
@@ -1,6 +1,7 @@
import {
CreateStorageBrowserInput,
DeleteHandlerOutput,
+ DownloadHandlerOutput,
UploadHandlerOutput,
} from '@aws-amplify/ui-react-storage/browser';
import uniqueId from 'lodash/uniqueId';
@@ -59,13 +60,27 @@ 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,
+ // creating URL with empty string will throw TypeError,
+ // preventing promise from resolving
+ value: { url: null },
+ });
+ }, 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..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
@@ -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
@@ -415,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!
@@ -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
+ ```
+
@@ -479,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.
@@ -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
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 785bac90960..ed12f6cb998 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;