diff --git a/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx
index 696dceb46fd..d6b93e23d3a 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/composable-playground/index.page.tsx
@@ -1,16 +1,18 @@
import React from 'react';
import {
+ createManagedAuthAdapter,
CreateStorageBrowserInput,
createStorageBrowser,
} from '@aws-amplify/ui-react-storage/browser';
-import { auth, managedAuthAdapter } from '../managedAuthAdapter';
+import { Auth } from '../managedAuthAdapter';
import { Button, Flex, Breadcrumbs } from '@aws-amplify/ui-react';
-import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
+import '@aws-amplify/ui-react/styles/reset.css';
import '@aws-amplify/ui-react-storage/styles.css';
+import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
const components: CreateStorageBrowserInput['components'] = {
Navigation: ({ items }) => (
@@ -26,15 +28,64 @@ const components: CreateStorageBrowserInput['components'] = {
),
};
-const { StorageBrowser } = createStorageBrowser({
+export const auth = new Auth({ persistCredentials: true });
+
+const config = createManagedAuthAdapter({
+ credentialsProvider: auth.credentialsProvider,
+ region: process.env.NEXT_PUBLIC_MANAGED_AUTH_REGION,
+ accountId: process.env.NEXT_PUBLIC_MANAGED_AUTH_ACCOUNT_ID,
+ registerAuthListener: auth.registerAuthListener,
+});
+
+const { StorageBrowser, useView } = createStorageBrowser({
components,
- config: managedAuthAdapter,
+ config,
});
-function LocationActionView() {
+const { CreateFolderView, DeleteView, LocationActionView } = StorageBrowser;
+
+const MyCreateFolderView = () => {
+ const viewState = useView('CreateFolder');
+ const { isProcessing } = viewState;
+ return (
+
+ {isProcessing ? Folder creation in progress
: null}
+
+
+
+ );
+};
+
+const MyDeleteView = () => {
+ const viewState = useView('Delete');
+ const { isProcessing } = viewState;
+ return (
+
+ {isProcessing ? Delete in progress
: null}
+
+
+
+ );
+};
+
+function MyLocationActionView({ type }: { type?: string }) {
+ let DialogContent = null;
+ if (!type) return DialogContent;
+
+ switch (type) {
+ case 'createFolder':
+ DialogContent = MyCreateFolderView;
+ break;
+ case 'delete':
+ DialogContent = MyDeleteView;
+ break;
+ default:
+ DialogContent = LocationActionView;
+ }
+
return (
);
}
@@ -50,11 +101,12 @@ function MyStorageBrowser() {
{
+ console.log(actionType);
setActionType(actionType);
}}
/>
- {type ? : null}
+
);
}
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx
index 5913451f2ab..2e3be8cc907 100644
--- a/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx
+++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/index.page.tsx
@@ -2,8 +2,15 @@ import React from 'react';
import { Amplify } from 'aws-amplify';
import { signOut } from 'aws-amplify/auth';
-import { Button, Flex, View, withAuthenticator } from '@aws-amplify/ui-react';
+import {
+ Button,
+ Flex,
+ IconsProvider,
+ View,
+ withAuthenticator,
+} from '@aws-amplify/ui-react';
import { StorageBrowser } from '@aws-amplify/ui-react-storage';
+
import '@aws-amplify/ui-react/styles/reset.css';
import '@aws-amplify/ui-react-storage/styles.css';
import '@aws-amplify/ui-react-storage/storage-browser-styles.css';
@@ -12,6 +19,26 @@ import config from './aws-exports';
Amplify.configure(config);
+const IndeterminateIcon = () => (
+
+
+
+);
+
function Example() {
return (
-
+ },
+ }}
+ >
+
+
);
diff --git a/packages/e2e/cypress/integration/common/shared.ts b/packages/e2e/cypress/integration/common/shared.ts
index ff0202bc76a..d53ef3fdbc6 100644
--- a/packages/e2e/cypress/integration/common/shared.ts
+++ b/packages/e2e/cypress/integration/common/shared.ts
@@ -288,6 +288,12 @@ Then('I see the button containing {string}', (name: string) => {
}).should('exist');
});
+Then('I do not see the button containing {string}', (name: string) => {
+ cy.findByRole('button', {
+ name: new RegExp(`${escapeRegExp(name)}`, 'i'),
+ }).should('not.exist');
+});
+
Then('I see the first button containing {string}', (name: string) => {
cy.findAllByRole('button', {
name: new RegExp(`${escapeRegExp(name)}`, 'i'),
@@ -546,6 +552,13 @@ When('I type my new password', () => {
cy.findInputField('New Password').type(Cypress.env('VALID_PASSWORD'));
});
+When(
+ 'I see input with placeholder {string} and type {string}',
+ (name: string, value: string) => {
+ cy.findByPlaceholderText(name).type(value);
+ }
+);
+
Then('I click the submit button', () => {
/**
* Submit button text differs on React/Vue vs Angular. Testing for both for
diff --git a/packages/e2e/features/ui/components/storage/storage-browser/drag-and-drop.feature b/packages/e2e/features/ui/components/storage/storage-browser/drag-and-drop.feature
index 9e15eb17adc..616dd8c9a20 100644
--- a/packages/e2e/features/ui/components/storage/storage-browser/drag-and-drop.feature
+++ b/packages/e2e/features/ui/components/storage/storage-browser/drag-and-drop.feature
@@ -10,7 +10,7 @@ Feature: Drag and drop files within Storage Browser
Then I click the "Sign in" button
When I click the first button containing "public"
When I drag and drop a file into the storage browser with file name "test.txt"
- Then I see "Upload Files"
+ Then I see "Upload"
Then I see "test.txt"
@react
@@ -20,23 +20,22 @@ Feature: Drag and drop files within Storage Browser
Then I click the "Sign in" button
When I click the first button containing "public"
When I drag and drop a folder into the storage browser with name "test"
- Then I see "Upload Folder"
+ Then I see "Upload"
Then I see "test"
- """
- Comment out for now upload is integrated
+
@react
Scenario: Drag and drop file into Upload Action view
When I type my "email" with status "CONFIRMED"
Then I type my password
Then I click the "Sign in" button
When I click the first button containing "public"
- Then I see the "Actions" button
- When I click the "Actions" button
- Then I see the "Upload Files" menuitem
- Then I click the "Upload Files" menuitem
+ Then I see the "Menu Toggle" button
+ When I click the "Menu Toggle" button
+ Then I see the "Upload" menuitem
+ Then I click the "Upload" menuitem
# Close the file select menu
Then I press the "{esc}" key
When I drag and drop a file into the storage browser with file name "test.txt"
Then I see "test.txt"
- """
\ No newline at end of file
+
\ No newline at end of file
diff --git a/packages/e2e/features/ui/components/storage/storage-browser/filter-locations.feature b/packages/e2e/features/ui/components/storage/storage-browser/filter-locations.feature
new file mode 100644
index 00000000000..8fd101cbebe
--- /dev/null
+++ b/packages/e2e/features/ui/components/storage/storage-browser/filter-locations.feature
@@ -0,0 +1,18 @@
+Feature: StorageBrowser Filter Locations
+
+ Background:
+ Given I'm running the example "ui/components/storage/storage-browser/default-auth"
+
+ @react
+ Scenario: Filter locations
+ When I type my "email" with status "CONFIRMED"
+ Then I type my password
+ Then I click the "Sign in" button
+ Then I see the first button containing "private"
+ When I see input with placeholder "Filter folders and files" and type "pu"
+ Then I click the "Submit" button
+ Then I see the first button containing "public"
+ Then I do not see the button containing "private"
+ When I click the button containing "Clear search"
+ Then I see the first button containing "private"
+
diff --git a/packages/react-core/src/utils/createContextUtilities.tsx b/packages/react-core/src/utils/createContextUtilities.tsx
index 4b424203b01..5df48495451 100644
--- a/packages/react-core/src/utils/createContextUtilities.tsx
+++ b/packages/react-core/src/utils/createContextUtilities.tsx
@@ -99,7 +99,11 @@ export default function createContextUtilities<
throw new Error(INVALID_OPTIONS_MESSAGE);
}
+ const contextDisplayName = `${contextName}Context`;
+ const providerDisplayName = `${contextName}Provider`;
+
const Context = React.createContext(defaultValue);
+ Context.displayName = contextDisplayName;
function Provider(props: React.PropsWithChildren) {
const { children, ...context } = props;
@@ -113,8 +117,7 @@ export default function createContextUtilities<
return {children};
}
- Provider.displayName = `${contextName}Provider`;
-
+ Provider.displayName = providerDisplayName;
return {
[`use${contextName}`]: function (params?: HookParams) {
const context = React.useContext(Context);
@@ -125,7 +128,7 @@ export default function createContextUtilities<
return context;
},
- [`${contextName}Provider`]: Provider,
- [`${contextName}Context`]: Context,
+ [providerDisplayName]: Provider,
+ [contextDisplayName]: Context,
} as CreateContextUtilitiesReturn;
}
diff --git a/packages/react-storage/package.json b/packages/react-storage/package.json
index d9a251c0096..1dbfd2ce78c 100644
--- a/packages/react-storage/package.json
+++ b/packages/react-storage/package.json
@@ -70,7 +70,7 @@
"name": "createStorageBrowser",
"path": "dist/esm/browser.mjs",
"import": "{ createStorageBrowser }",
- "limit": "30 kB",
+ "limit": "37 kB",
"ignore": [
"@aws-amplify/storage"
]
@@ -79,7 +79,7 @@
"name": "FileUploader",
"path": "dist/esm/index.mjs",
"import": "{ FileUploader }",
- "limit": "21.6 kB"
+ "limit": "25.00 kB"
},
{
"name": "StorageImage",
diff --git a/packages/react-storage/src/components/StorageBrowser/ComponentsProvider.tsx b/packages/react-storage/src/components/StorageBrowser/ComponentsProvider.tsx
index afa19875b3d..3548bff11dd 100644
--- a/packages/react-storage/src/components/StorageBrowser/ComponentsProvider.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/ComponentsProvider.tsx
@@ -5,9 +5,7 @@ import { ElementsProvider } from '@aws-amplify/ui-react-core/elements';
import { ComposablesProvider, Composables } from './composables';
import { StorageBrowserElements } from './context/elements';
-export interface Components
- // omitted values have not yet been integrated with views
- extends Omit, 'Message'> {}
+export interface Components extends Partial {}
export interface ComponentsProviderProps {
children?: React.ReactNode;
diff --git a/packages/react-storage/src/components/StorageBrowser/StorageBrowserAmplify.tsx b/packages/react-storage/src/components/StorageBrowser/StorageBrowserAmplify.tsx
index 6a85c7986b7..74902d5dd8d 100644
--- a/packages/react-storage/src/components/StorageBrowser/StorageBrowserAmplify.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/StorageBrowserAmplify.tsx
@@ -1,10 +1,8 @@
import React from 'react';
import { elementsDefault } from './context/elements';
-import {
- createStorageBrowser,
- StorageBrowserProps as StorageBrowserPropsBase,
-} from './createStorageBrowser';
+import { createStorageBrowser } from './createStorageBrowser';
+import { StorageBrowserProps as StorageBrowserPropsBase } from './types';
import { createAmplifyAuthAdapter } from './adapters';
import { TextField } from '@aws-amplify/ui-react';
diff --git a/packages/react-storage/src/components/StorageBrowser/__tests__/adapters/managedAuthConfigAdapter/createListLocationsHandler.test.ts b/packages/react-storage/src/components/StorageBrowser/__tests__/adapters/managedAuthConfigAdapter/createListLocationsHandler.test.ts
deleted file mode 100644
index ceaa8d864ae..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/__tests__/adapters/managedAuthConfigAdapter/createListLocationsHandler.test.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { createListLocationsHandler } from '../../../adapters/createManagedAuthAdapter/createListLocationsHandler';
-import { listCallerAccessGrants } from '../../../storage-internal';
-
-jest.mock('../../../storage-internal');
-
-jest.mocked(listCallerAccessGrants).mockResolvedValue({
- locations: [],
-});
-
-describe('createListLocationsHandler', () => {
- it('should parse the underlying API with right parameters', async () => {
- const mockAccountId = '1234567890';
- const mockRegion = 'us-foo-1';
- const mockCredentialsProvider = jest.fn();
- const mockCustomEndpoint = 'mock-endpoint';
- const mockNextToken = '123';
- const mockPageSize = 123;
- const handler = createListLocationsHandler({
- accountId: mockAccountId,
- customEndpoint: mockCustomEndpoint,
- region: mockRegion,
- credentialsProvider: mockCredentialsProvider,
- });
- await handler({ nextToken: mockNextToken, pageSize: mockPageSize });
- expect(listCallerAccessGrants).toHaveBeenCalledWith({
- accountId: mockAccountId,
- region: mockRegion,
- credentialsProvider: mockCredentialsProvider,
- customEndpoint: mockCustomEndpoint,
- nextToken: mockNextToken,
- pageSize: mockPageSize,
- });
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/__tests__/createStorageBrowser.spec.tsx b/packages/react-storage/src/components/StorageBrowser/__tests__/createStorageBrowser.spec.tsx
index 62212ba7532..bfb6c7fbea3 100644
--- a/packages/react-storage/src/components/StorageBrowser/__tests__/createStorageBrowser.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/__tests__/createStorageBrowser.spec.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
-import * as ActionsModule from '../do-not-import-from-here/actions';
import * as ProvidersModule from '../providers';
import { createStorageBrowser } from '../createStorageBrowser';
@@ -12,26 +11,6 @@ const createConfigurationProviderSpy = jest.spyOn(
'createConfigurationProvider'
);
-jest.spyOn(ActionsModule, 'useLocationsData').mockReturnValue([
- {
- isLoading: false,
- data: { result: [], nextToken: undefined },
- hasError: false,
- message: undefined,
- },
- jest.fn(),
-]);
-
-jest.spyOn(ActionsModule, 'useAction').mockReturnValue([
- {
- data: { result: [], nextToken: undefined },
- hasError: false,
- isLoading: false,
- message: undefined,
- },
- jest.fn(),
-]);
-
const accountId = '012345678901';
const customEndpoint = 'mock-endpoint';
const getLocationCredentials = jest.fn();
@@ -78,6 +57,7 @@ describe('createStorageBrowser', () => {
region: config.region,
registerAuthListener: config.registerAuthListener,
actions: {
+ copy: expect.any(Object),
createFolder: expect.any(Object),
delete: expect.any(Object),
listLocationItems: expect.any(Object),
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 b7f26d0cf29..eca8ae88ffc 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
@@ -2,6 +2,17 @@
exports[`defaultActionConfigs matches expected shape 1`] = `
{
+ "copy": {
+ "actionsListItemConfig": {
+ "disable": [Function],
+ "hide": [Function],
+ "icon": "download",
+ "label": "Copy Files",
+ },
+ "componentName": "CopyView",
+ "displayName": "Copy",
+ "handler": [Function],
+ },
"createFolder": {
"actionsListItemConfig": {
"disable": [Function],
@@ -18,7 +29,7 @@ exports[`defaultActionConfigs matches expected shape 1`] = `
"actionsListItemConfig": {
"disable": [Function],
"hide": [Function],
- "icon": "delete",
+ "icon": "delete-file",
"label": "Delete Files",
},
"componentName": "DeleteView",
@@ -36,22 +47,13 @@ exports[`defaultActionConfigs matches expected shape 1`] = `
"handler": [Function],
},
"upload": {
- "actionsListItemConfig": [
- {
- "disable": [Function],
- "fileSelection": "FILE",
- "hide": [Function],
- "icon": "upload-file",
- "label": "Upload File",
- },
- {
- "disable": [Function],
- "fileSelection": "FOLDER",
- "hide": [Function],
- "icon": "upload-folder",
- "label": "Upload FOLDER",
- },
- ],
+ "actionsListItemConfig": {
+ "disable": [Function],
+ "fileSelection": "FILE",
+ "hide": [Function],
+ "icon": "upload-file",
+ "label": "Upload",
+ },
"componentName": "UploadView",
"displayName": "Upload",
"handler": [Function],
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 b37f137a26d..67d151e73cb 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
@@ -65,8 +65,7 @@ describe('defaultActionConfigs', () => {
describe('uploadActionConfig', () => {
it('hides the action list item as expected', () => {
- const [uploadFileListItem, uploadFolderListItem] =
- uploadActionConfig.actionsListItemConfig as ActionListItemConfig[];
+ const uploadFileListItem = uploadActionConfig.actionsListItemConfig!;
for (const permissionsWithoutWrite of generateCombinations(
permissionValuesWithoutWrite
@@ -77,20 +76,14 @@ describe('defaultActionConfigs', () => {
];
expect(uploadFileListItem.hide?.(permissionsWithoutWrite)).toBe(true);
expect(uploadFileListItem.hide?.(permissionsWithWrite)).toBe(false);
- expect(uploadFolderListItem.hide?.(permissionsWithoutWrite)).toBe(true);
- expect(uploadFolderListItem.hide?.(permissionsWithWrite)).toBe(false);
}
});
it('disables the action list item as expected', () => {
- const [uploadFileListItem, uploadFolderListItem] =
- uploadActionConfig.actionsListItemConfig as ActionListItemConfig[];
+ const uploadFileListItem = uploadActionConfig.actionsListItemConfig!;
expect(uploadFileListItem.disable?.([file])).toBe(true);
expect(uploadFileListItem.disable?.(undefined)).toBe(false);
-
- expect(uploadFolderListItem.disable?.([file])).toBe(true);
- expect(uploadFolderListItem.disable?.(undefined)).toBe(false);
});
});
});
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 ca419aada7e..c4e72a3f7ac 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/configs/defaults.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/defaults.tsx
@@ -34,7 +34,7 @@ export const deleteActionConfig: DeleteActionConfig = {
actionsListItemConfig: {
disable: (selected) => !selected,
hide: (permissions) => !permissions.includes('delete'),
- icon: 'delete',
+ icon: 'delete-file',
label: 'Delete Files',
},
displayName: 'Delete',
@@ -76,33 +76,39 @@ export const listLocationsActionConfig: ListLocationsActionConfig = {
export const uploadActionConfig: UploadActionConfig = {
componentName: 'UploadView',
- actionsListItemConfig: [
- {
- disable: (selectedValues) => !!selectedValues,
- fileSelection: 'FILE',
- hide: (permissions) => !permissions.includes('write'),
- icon: 'upload-file',
- label: 'Upload File',
- },
- {
- disable: (selectedValues) => !!selectedValues,
- fileSelection: 'FOLDER',
- hide: (permissions) => !permissions.includes('write'),
- icon: 'upload-folder',
- label: 'Upload FOLDER',
- },
- ],
+ actionsListItemConfig: {
+ disable: (selectedValues) => !!selectedValues,
+ fileSelection: 'FILE',
+ hide: (permissions) => !permissions.includes('write'),
+ icon: 'upload-file',
+ label: 'Upload',
+ },
isCancelable: true,
includeProgress: true,
handler: uploadHandler,
displayName: 'Upload',
};
-export const defaultActionConfigs = {
- // copy: copyActionConfig,
+export const defaultActionViewConfigs = {
+ copy: copyActionConfig,
createFolder: createFolderActionConfig,
delete: deleteActionConfig,
+ upload: uploadActionConfig,
+};
+
+export type DefaultActionViewType = keyof typeof defaultActionViewConfigs;
+
+export const DEFAULT_ACTION_VIEW_TYPES = Object.keys(
+ defaultActionViewConfigs
+) as DefaultActionViewType[];
+
+export const isDefaultActionViewType = (
+ value?: string
+): value is DefaultActionViewType =>
+ DEFAULT_ACTION_VIEW_TYPES.some((type) => type === value);
+
+export const defaultActionConfigs = {
+ ...defaultActionViewConfigs,
listLocationItems: listLocationItemsActionConfig,
listLocations: listLocationsActionConfig,
- upload: uploadActionConfig,
};
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/configs/index.ts b/packages/react-storage/src/components/StorageBrowser/actions/configs/index.ts
index 90f657bb2f9..b94b3f540be 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/configs/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/index.ts
@@ -3,5 +3,9 @@ export {
ActionConfigsProviderProps,
useActionConfig,
} from './context';
-export { defaultActionConfigs } from './defaults';
+export {
+ defaultActionConfigs,
+ defaultActionViewConfigs,
+ isDefaultActionViewType,
+} from './defaults';
export * from './types';
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 90bbbffd016..acf5a1988bf 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/configs/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/configs/types.ts
@@ -64,7 +64,7 @@ export interface ActionListItemConfig {
/**
* list item icon
*/
- icon: IconVariant | Exclude;
+ icon: IconVariant;
/**
* list item label
@@ -82,7 +82,7 @@ export interface TaskActionConfig
* configure action list item behavior. provide multiple configs
* to create additional list items for a single action
*/
- actionsListItemConfig?: ActionListItemConfig | ActionListItemConfig[];
+ actionsListItemConfig?: ActionListItemConfig;
/**
* whether the provided `handler` allow inflight cancellation
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/createViews.tsx b/packages/react-storage/src/components/StorageBrowser/actions/createViews.tsx
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/handlers/__tests__/listLocations.spec.ts b/packages/react-storage/src/components/StorageBrowser/actions/handlers/__tests__/listLocations.spec.ts
index 31f53367992..60fd72da488 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/handlers/__tests__/listLocations.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/handlers/__tests__/listLocations.spec.ts
@@ -22,18 +22,17 @@ const generateMockLocations = (size: number, mockLocations: LocationAccess) =>
const accountId = 'account-id';
const credentials: LocationCredentialsProvider = jest.fn();
const region = 'region';
-const bucket = 'bucket';
+
const customEndpoint = 'mock-endpoint';
const DEFAULT_PAGE_SIZE = 5;
const input: ListLocationsHandlerInput = {
- config: { accountId, credentials, customEndpoint, region, bucket },
+ config: { accountId, credentials, customEndpoint, region },
options: {
pageSize: DEFAULT_PAGE_SIZE,
nextToken: undefined,
exclude: { exactPermissions: ['get', 'list'] },
},
- prefix: 'prefix',
};
describe('listLocationsHandler', () => {
@@ -51,7 +50,7 @@ describe('listLocationsHandler', () => {
it('should fetch a single page of results successfully', async () => {
const mockOutput: ListLocationsOutput = {
locations: [
- { scope: 's3://bucket/prefix', permission: 'READ', type: 'PREFIX' },
+ { scope: 's3://bucket/prefix/*', permission: 'READ', type: 'PREFIX' },
],
nextToken: undefined,
};
@@ -77,8 +76,8 @@ describe('listLocationsHandler', () => {
it('should fetch multiple pages of results successfully', async () => {
const mockLocation: LocationAccess = {
- scope: 's3://bucket/prefix1',
- permission: 'READ',
+ scope: 's3://bucket/prefix1/*',
+ permission: 'READWRITE',
type: 'PREFIX',
};
const mockOutputPage1: ListLocationsOutput = {
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/handlers/listLocations.ts b/packages/react-storage/src/components/StorageBrowser/actions/handlers/listLocations.ts
index 3f1467851aa..b85ba3c3460 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/handlers/listLocations.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/handlers/listLocations.ts
@@ -1,29 +1,54 @@
import {
- ListLocationsOutput,
listCallerAccessGrants,
+ LocationCredentialsProvider,
} from '../../storage-internal';
import { assertAccountId } from '../../validators';
import {
ListHandlerOptions,
- ListHandlerInput,
- ListHandlerOutput,
ListHandler,
+ ListLocationsExcludeOptions,
+ LocationData,
} from './types';
-
-import { ListLocationsExcludeOptions, LocationData } from './types';
import { getFilteredLocations } from './utils';
const DEFAULT_PAGE_SIZE = 1000;
+export interface ListLocationsOptions extends ListLocationsHandlerOptions {}
+
+export interface ListLocationsInput {
+ options?: ListLocationsOptions;
+}
+
+export interface ListLocationsOutput {
+ items: LocationData[];
+ nextToken: string | undefined;
+}
+
+// `ListLocations` and its associated input/output types are the types
+// used `Config` option of `CreateStorageBrowser` that do not require
+// `config` values as they are provided through higher-order functions
+// defined in the default and managed auth adapters
+export interface ListLocations
+ extends ListHandler {}
+
export interface ListLocationsHandlerOptions
extends ListHandlerOptions {}
-export interface ListLocationsHandlerInput
- extends ListHandlerInput {}
+export interface ListLocationsHandlerInput {
+ options?: ListLocationsHandlerOptions;
+ config: {
+ accountId?: string;
+ credentials: LocationCredentialsProvider;
+ customEndpoint?: string;
+ region: string;
+ };
+}
-export interface ListLocationsHandlerOutput
- extends ListHandlerOutput {}
+export interface ListLocationsHandlerOutput {
+ items: LocationData[];
+ nextToken: string | undefined;
+}
export interface ListLocationsHandler
extends ListHandler {}
@@ -36,10 +61,7 @@ export const listLocationsHandler: ListLocationsHandler = async (input) => {
const fetchLocations = async (
accumulatedItems: LocationData[],
locationsNextToken: ListLocationsOutput['nextToken']
- ): Promise<{
- items: LocationData[];
- nextToken: ListLocationsOutput['nextToken'];
- }> => {
+ ): Promise => {
const remainingPageSize = pageSize - accumulatedItems.length;
assertAccountId(accountId);
@@ -61,7 +83,7 @@ export const listLocationsHandler: ListLocationsHandler = async (input) => {
return fetchLocations(items, output.nextToken);
}
- return { items: items, nextToken: output.nextToken };
+ return { items, nextToken: output.nextToken };
};
return fetchLocations([], nextToken);
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/handlers/types.ts b/packages/react-storage/src/components/StorageBrowser/actions/handlers/types.ts
index 0835b51bbf8..887945ec64b 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/handlers/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/handlers/types.ts
@@ -36,11 +36,6 @@ export interface LocationData {
type: LocationType;
}
-export interface ListLocationsExcludeOptions {
- exactPermissions?: LocationPermissions;
- type?: LocationType | LocationType[];
-}
-
export interface FolderData {
key: string;
id: string;
@@ -124,3 +119,8 @@ export interface ListHandlerOutput {
}
export type ListHandler = (input: T) => Promise;
+
+export interface ListLocationsExcludeOptions {
+ exactPermissions?: LocationPermissions;
+ type?: LocationType | LocationType[];
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/handlers/utils.ts b/packages/react-storage/src/components/StorageBrowser/actions/handlers/utils.ts
index a3d7568386b..99610f61541 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/handlers/utils.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/handlers/utils.ts
@@ -1,12 +1,12 @@
import { TransferProgressEvent } from 'aws-amplify/storage';
import { LocationAccess as AccessGrantLocation } from '../../storage-internal';
+import { ListLocationsExcludeOptions } from './types';
import {
ActionInputConfig,
FileData,
FileDataItem,
FileItem,
- ListLocationsExcludeOptions,
LocationData,
LocationPermissions,
LocationType,
@@ -20,32 +20,6 @@ export const constructBucket = ({
region: string;
} => ({ bucketName, region });
-// FIXME: this may not need to be exported if do-not-import-from-here actions are migrated.
-export const parseAccessGrantLocationScope = (
- scope: string,
- type: LocationType
-): { bucket: string; prefix: string } => {
- const slicedScope = scope.slice(5);
- if (type === 'BUCKET') {
- // { scope: 's3://bucket/*', type: 'BUCKET', },
- const bucket = slicedScope.slice(0, -2);
- const prefix = '';
- return { bucket, prefix };
- } else if (type === 'PREFIX') {
- // { scope: 's3://bucket/path/*', type: 'PREFIX', },
- const bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
- const prefix = `${slicedScope.slice(bucket.length + 1, -1)}`;
- return { bucket, prefix };
- } else if (type === 'OBJECT') {
- // { scope: 's3://bucket/path/to/object', type: 'OBJECT', },
- const bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
- const prefix = slicedScope.slice(bucket.length + 1);
- return { bucket, prefix };
- } else {
- throw new Error(`Invalid location type: ${type}`);
- }
-};
-
export const parseAccessGrantLocation = (
location: AccessGrantLocation
): LocationData => {
@@ -128,11 +102,16 @@ const isSameType = (
export const shouldExcludeLocation = (
{ permissions, type }: LocationData,
exclude?: ListLocationsExcludeOptions
-): boolean =>
- Boolean(
+): boolean => {
+ const excludedByPermssions = !!(
exclude?.exactPermissions &&
- isSamePermissions(exclude.exactPermissions, permissions)
- ) || Boolean(exclude?.type && isSameType(exclude.type, type));
+ isSamePermissions(exclude.exactPermissions, permissions)
+ );
+
+ const excludedByType = !!(exclude?.type && isSameType(exclude.type, type));
+
+ return excludedByPermssions || excludedByType;
+};
export const getFilteredLocations = (
locations: AccessGrantLocation[],
@@ -141,9 +120,19 @@ export const getFilteredLocations = (
locations.reduce(
(filteredLocations: LocationData[], location: AccessGrantLocation) => {
const parsedLocation = parseAccessGrantLocation(location);
- if (shouldExcludeLocation(parsedLocation, exclude)) {
+
+ const isNonFolderLikePrefix =
+ !parsedLocation.prefix.endsWith('/') &&
+ parsedLocation.type === 'PREFIX';
+
+ if (isNonFolderLikePrefix) {
+ return filteredLocations;
+ }
+
+ if (!shouldExcludeLocation(parsedLocation, exclude)) {
filteredLocations.push(parsedLocation);
}
+
return filteredLocations;
},
[]
@@ -164,6 +153,8 @@ export const createFileDataItemFromLocation = (
type: 'FILE',
key: data.prefix,
fileKey: getFileKey(data.prefix),
+ // `lastModified` and `size` included to satisfy
+ // expected shape of `FileDataItem`
lastModified: new Date(),
size: 0,
});
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/index.ts b/packages/react-storage/src/components/StorageBrowser/actions/index.ts
index 1218ae873ab..a6fb6f1f009 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/index.ts
@@ -5,7 +5,9 @@ export {
ComponentName,
defaultActionConfigs,
DefaultActionConfigs,
+ defaultActionViewConfigs,
DefaultActionKey,
+ isDefaultActionViewType,
SelectionType,
TaskActionConfig,
useActionConfig,
@@ -18,6 +20,7 @@ export {
CopyHandlerData,
CopyHandlerInput,
CopyHandlerOutput,
+ createFileDataItemFromLocation,
createFileDataItem,
createFolderHandler,
CreateFolderHandler,
@@ -51,6 +54,10 @@ export {
ListLocationItemsHandlerInput,
ListLocationItemsHandlerOptions,
ListLocationItemsHandlerOutput,
+ ListLocationsExcludeOptions,
+ ListLocations,
+ ListLocationsInput,
+ ListLocationsOutput,
listLocationsHandler,
ListLocationsHandler,
ListLocationsHandlerInput,
@@ -72,3 +79,7 @@ export {
UploadHandlerOptions,
UploadHandlerOutput,
} from './handlers';
+
+export { ActionState } from './types';
+
+export { useListLocations, UseListLocationsState } from './useAction';
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/__tests__/createEnhancedListHandler.spec.ts b/packages/react-storage/src/components/StorageBrowser/actions/useAction/__tests__/createEnhancedListHandler.spec.ts
similarity index 99%
rename from packages/react-storage/src/components/StorageBrowser/actions/__tests__/createEnhancedListHandler.spec.ts
rename to packages/react-storage/src/components/StorageBrowser/actions/useAction/__tests__/createEnhancedListHandler.spec.ts
index 6ac4660be1d..e50307394a5 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/__tests__/createEnhancedListHandler.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/useAction/__tests__/createEnhancedListHandler.spec.ts
@@ -7,7 +7,7 @@ import {
ListHandler,
ListHandlerInput,
ListHandlerOutput,
-} from '../handlers';
+} from '../../handlers';
const mockAction = jest.fn();
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/__tests__/search.spec.ts b/packages/react-storage/src/components/StorageBrowser/actions/useAction/__tests__/search.spec.ts
similarity index 100%
rename from packages/react-storage/src/components/StorageBrowser/actions/__tests__/search.spec.ts
rename to packages/react-storage/src/components/StorageBrowser/actions/useAction/__tests__/search.spec.ts
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/createEnhancedListHandler.ts b/packages/react-storage/src/components/StorageBrowser/actions/useAction/createEnhancedListHandler.ts
similarity index 93%
rename from packages/react-storage/src/components/StorageBrowser/actions/createEnhancedListHandler.ts
rename to packages/react-storage/src/components/StorageBrowser/actions/useAction/createEnhancedListHandler.ts
index 2d0eec52ddf..db4303ca23b 100644
--- a/packages/react-storage/src/components/StorageBrowser/actions/createEnhancedListHandler.ts
+++ b/packages/react-storage/src/components/StorageBrowser/actions/useAction/createEnhancedListHandler.ts
@@ -5,7 +5,7 @@ import {
ListHandlerOptions,
ListHandlerInput,
ListHandlerOutput,
-} from './handlers';
+} from '../handlers';
type KeyWithStringValue = keyof {
[P in keyof T as T[P] extends string ? P : never]: T[P];
@@ -36,14 +36,17 @@ export interface SearchOutput {
hasExhaustedSearch: boolean;
}
-interface EnhancedListHandlerOutput extends ListHandlerOutput {
+export interface EnhancedListHandlerOutput extends ListHandlerOutput {
search?: SearchOutput;
}
-interface EnhancedListHandler
+export interface EnhancedListHandlerInput
+ extends ListHandlerInput> {}
+
+export interface EnhancedListHandler
extends AsyncDataAction<
EnhancedListHandlerOutput,
- ListHandlerInput>
+ EnhancedListHandlerInput
> {}
type ListItem = Action extends ListHandler<
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/useAction/index.ts b/packages/react-storage/src/components/StorageBrowser/actions/useAction/index.ts
new file mode 100644
index 00000000000..8b7ee5535f2
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/actions/useAction/index.ts
@@ -0,0 +1 @@
+export { useListLocations, UseListLocationsState } from './useListLocations';
diff --git a/packages/react-storage/src/components/StorageBrowser/actions/useAction/useListLocations.ts b/packages/react-storage/src/components/StorageBrowser/actions/useAction/useListLocations.ts
new file mode 100644
index 00000000000..9de7697950e
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/actions/useAction/useListLocations.ts
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import { useDataState } from '@aws-amplify/ui-react-core';
+
+import { useActionConfig } from '../configs';
+import {
+ ListLocations,
+ LocationData,
+ ListLocationsExcludeOptions,
+} from '../handlers';
+import { ActionState } from '../types';
+
+import {
+ createEnhancedListHandler,
+ EnhancedListHandlerInput,
+ EnhancedListHandlerOutput,
+} from './createEnhancedListHandler';
+
+// Utility type functioning as a shim to allow for the outputted
+// enhanced `ListLocations` handler to not require `config` and `prefix`
+// in usage, which are required by the signature of `createEnhancedListHandler`
+type RemoveConfigAndPrefix = Omit;
+
+export interface UseListLocationsState
+ extends ActionState<
+ EnhancedListHandlerOutput,
+ RemoveConfigAndPrefix<
+ EnhancedListHandlerInput
+ >
+ > {}
+
+export const useListLocations = (): UseListLocationsState => {
+ const { handler } = useActionConfig('listLocations');
+ const enhancedHandler = React.useMemo(
+ () => createEnhancedListHandler(handler as ListLocations),
+ [handler]
+ );
+
+ return useDataState(enhancedHandler, {
+ items: [],
+ nextToken: undefined,
+ }) as UseListLocationsState;
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/__tests__/adapters/permissionParsers.spec.ts b/packages/react-storage/src/components/StorageBrowser/adapters/__tests__/permissionParsers.spec.ts
similarity index 100%
rename from packages/react-storage/src/components/StorageBrowser/__tests__/adapters/permissionParsers.spec.ts
rename to packages/react-storage/src/components/StorageBrowser/adapters/__tests__/permissionParsers.spec.ts
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/createAmplifyListLocationsHandler.spec.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/createAmplifyListLocationsHandler.spec.ts
index 35d89cddb2f..a1c51182774 100644
--- a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/createAmplifyListLocationsHandler.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/createAmplifyListLocationsHandler.spec.ts
@@ -1,7 +1,8 @@
-import { createAmplifyListLocationsHandler } from '../createAmplifyListLocationsHandler';
-import { getPaginatedLocations } from '../getPaginatedLocations';
+import { ListLocations, LocationData } from '../../../actions';
import { listPaths, ListPathsOutput } from '../../../storage-internal';
-import { LocationAccess, ListLocations } from '../../types';
+
+import { getPaginatedLocations } from '../getPaginatedLocations';
+import { createAmplifyListLocationsHandler } from '../createAmplifyListLocationsHandler';
jest.mock('../../../storage-internal', () => ({
listPaths: jest.fn(),
@@ -15,7 +16,14 @@ jest.mock(
describe('createAmplifyListLocationsHandler', () => {
const mockListPaths = jest.mocked(listPaths);
- const mockGetPaginatedLocations = jest.mocked(getPaginatedLocations);
+ const mockGetPaginatedItems = jest.mocked(getPaginatedLocations);
+ const mockId = 'intentionally-static-test-id';
+
+ beforeAll(() => {
+ Object.defineProperty(globalThis, 'crypto', {
+ value: { randomUUID: () => mockId },
+ });
+ });
beforeEach(() => {
jest.clearAllMocks();
@@ -27,75 +35,79 @@ describe('createAmplifyListLocationsHandler', () => {
{
bucket: 'bucket1',
permission: ['read'],
- prefix: 'prefix1',
+ prefix: 'prefix1/*',
type: 'PREFIX',
},
];
- const sanitizedLocation: LocationAccess[] = [
+ const sanitizedLocations: LocationData[] = [
{
- scope: 's3://bucket1/prefix1',
+ prefix: 'prefix1/',
+ bucket: 'bucket1',
+ id: mockId,
permissions: ['get', 'list'],
type: 'PREFIX',
},
];
- const input = { pageSize: 10, nextToken: undefined };
+ const input = { options: { pageSize: 10, nextToken: undefined } };
const paginatedResult = {
- locations: sanitizedLocation,
+ items: sanitizedLocations,
nextToken: undefined,
};
mockListPaths.mockResolvedValueOnce({ locations: fetchedLocations });
- mockGetPaginatedLocations.mockReturnValueOnce(paginatedResult);
+ mockGetPaginatedItems.mockReturnValueOnce(paginatedResult);
const result = await handler(input);
expect(result).toEqual(paginatedResult);
expect(mockListPaths).toHaveBeenCalledTimes(1);
- expect(mockGetPaginatedLocations).toHaveBeenCalledWith({
- locations: sanitizedLocation,
- pageSize: input.pageSize,
- nextToken: input.nextToken,
+ expect(mockGetPaginatedItems).toHaveBeenCalledWith({
+ items: sanitizedLocations,
+ pageSize: input.options.pageSize,
+ nextToken: input.options.nextToken,
});
});
it('should fetch locations from the cache', async () => {
const handler: ListLocations = createAmplifyListLocationsHandler();
- const input = { pageSize: 10, nextToken: undefined };
+ const input = { options: { pageSize: 10, nextToken: undefined } };
const fetchedLocations: ListPathsOutput['locations'] = [
{
bucket: 'bucket1',
permission: ['read'],
- prefix: 'prefix1',
+ prefix: 'prefix1/*',
type: 'PREFIX',
},
];
mockListPaths.mockResolvedValueOnce({ locations: fetchedLocations });
await handler(input);
- const cachedLocations: LocationAccess[] = [
+ const cachedItems: LocationData[] = [
{
- scope: 's3://bucket1/prefix1',
+ prefix: 'prefix1/',
+ bucket: 'bucket1',
+ id: mockId,
permissions: ['get', 'list'],
type: 'PREFIX',
},
];
const paginatedResult = {
- locations: cachedLocations,
+ items: cachedItems,
nextToken: undefined,
};
- mockGetPaginatedLocations.mockReturnValueOnce(paginatedResult);
+ mockGetPaginatedItems.mockReturnValueOnce(paginatedResult);
const result = await handler(input);
expect(result).toEqual(paginatedResult);
- expect(mockGetPaginatedLocations).toHaveBeenCalledWith({
- locations: cachedLocations,
- pageSize: input.pageSize,
- nextToken: input.nextToken,
+ expect(mockGetPaginatedItems).toHaveBeenCalledWith({
+ items: cachedItems,
+ pageSize: input.options.pageSize,
+ nextToken: input.options.nextToken,
});
expect(mockListPaths).toHaveBeenCalledTimes(1);
});
@@ -106,47 +118,51 @@ describe('createAmplifyListLocationsHandler', () => {
{
bucket: 'bucket1',
permission: ['read'],
- prefix: 'prefix1',
+ prefix: 'prefix1/*',
type: 'PREFIX',
},
{
bucket: 'bucket2',
permission: ['read'],
- prefix: 'prefix2',
+ prefix: 'prefix2/*',
type: 'PREFIX',
},
];
- const sanitizedLocation: LocationAccess[] = [
+ const sanitizedLocations: LocationData[] = [
{
- scope: 's3://bucket1/prefix1',
+ prefix: 'prefix1/',
+ bucket: 'bucket1',
+ id: mockId,
permissions: ['get', 'list'],
type: 'PREFIX',
},
{
- scope: 's3://bucket2/prefix2',
+ prefix: 'prefix2/',
+ bucket: 'bucket2',
+ id: mockId,
permissions: ['get', 'list'],
type: 'PREFIX',
},
];
- const input = { pageSize: 1, nextToken: undefined };
+ const input = { options: { pageSize: 1, nextToken: undefined } };
const paginatedResult = {
- locations: [sanitizedLocation[0]],
+ items: [{ ...sanitizedLocations }[0]],
nextToken: 'token1',
};
mockListPaths.mockResolvedValueOnce({ locations: fetchedLocations });
- mockGetPaginatedLocations.mockReturnValueOnce(paginatedResult);
+ mockGetPaginatedItems.mockReturnValueOnce(paginatedResult);
const result = await handler(input);
- expect(result.locations).toEqual(paginatedResult.locations);
+ expect(result.items).toEqual(paginatedResult.items);
expect(mockListPaths).toHaveBeenCalledTimes(1);
- expect(mockGetPaginatedLocations).toHaveBeenCalledWith({
- locations: sanitizedLocation,
- pageSize: input.pageSize,
- nextToken: input.nextToken,
+ expect(mockGetPaginatedItems).toHaveBeenCalledWith({
+ items: sanitizedLocations,
+ pageSize: input.options.pageSize,
+ nextToken: input.options.nextToken,
});
});
});
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/getPaginatedLocations.test.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/getPaginatedLocations.test.ts
index f53c07eceba..057d6d1b84c 100644
--- a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/getPaginatedLocations.test.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/__tests__/getPaginatedLocations.test.ts
@@ -1,86 +1,92 @@
import { getPaginatedLocations } from '../getPaginatedLocations';
-import { ListLocationsOutput } from '../../types';
+import { ListLocationsHandlerOutput } from '../../../actions';
describe('getPaginatedLocations', () => {
- const mockLocations: ListLocationsOutput['locations'] = [
+ const mockItems: ListLocationsHandlerOutput['items'] = [
{
type: 'PREFIX',
permissions: ['list'],
- scope: 's3://bucket1/path1/',
+ prefix: 'path1/',
+ bucket: 'bucket1',
+ id: '1',
},
{
type: 'PREFIX',
permissions: ['get'],
- scope: 's3://bucket2/path2/',
+ prefix: 'path2/',
+ bucket: 'bucket2',
+ id: '2',
},
{
type: 'PREFIX',
permissions: ['write'],
- scope: 's3://bucket3/path3/',
+ prefix: 'path3/',
+ bucket: 'bucket3',
+ id: '3',
},
];
it('should return all locations when no pagination is specified', () => {
- const result = getPaginatedLocations({ locations: mockLocations });
- expect(result).toEqual({ locations: mockLocations });
+ const result = getPaginatedLocations({ items: mockItems });
+ expect(result).toEqual({ items: mockItems });
});
it('should return paginated locations when pageSize is specified', () => {
const result = getPaginatedLocations({
- locations: mockLocations,
+ items: mockItems,
pageSize: 2,
});
expect(result).toEqual({
- locations: mockLocations.slice(0, 2),
+ items: mockItems.slice(0, 2),
nextToken: '1',
});
});
it('should return paginated locations when pageSize and nextToken are specified', () => {
const result = getPaginatedLocations({
- locations: mockLocations,
+ items: mockItems,
pageSize: 1,
nextToken: '2',
});
expect(result).toEqual({
- locations: mockLocations.slice(1, 2),
+ items: mockItems.slice(1, 2),
nextToken: '1',
});
});
it('should return empty locations when locations array is empty', () => {
- const result = getPaginatedLocations({ locations: [], pageSize: 2 });
- expect(result).toEqual({ locations: [] });
+ const result = getPaginatedLocations({ items: [], pageSize: 2 });
+ expect(result).toEqual({ items: [] });
});
it('should return empty location when nextToken is beyond array length', () => {
const result = getPaginatedLocations({
- locations: mockLocations,
+ items: mockItems,
pageSize: 2,
nextToken: '5',
});
- expect(result).toEqual({ locations: [], nextToken: undefined });
+ expect(result).toEqual({ items: [], nextToken: undefined });
});
it('should return all remaining location when page size is greater than remaining locations length', () => {
const result = getPaginatedLocations({
- locations: mockLocations,
+ items: mockItems,
pageSize: 5,
nextToken: '2',
});
expect(result).toEqual({
- locations: mockLocations.slice(-2),
+ items: mockItems.slice(-2),
nextToken: undefined,
});
});
it('should return undefined nextToken when end of array is reached', () => {
const result = getPaginatedLocations({
- locations: mockLocations,
+ items: mockItems,
pageSize: 5,
});
expect(result).toEqual({
- locations: mockLocations.slice(0, 3),
+ items: mockItems.slice(0, 3),
nextToken: undefined,
});
});
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.ts
index 053ecec7d68..58c87c26af5 100644
--- a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.ts
@@ -1,17 +1,20 @@
+import { LocationData } from '../../actions';
import { listPaths, ListPathsOutput } from '../../storage-internal';
+import { ListLocations, ListLocationsInput } from '../../actions';
+
import { parseAmplifyAuthPermission } from '../permissionParsers';
-import { ListLocations, ListLocationsOutput } from '../types';
import { getPaginatedLocations } from './getPaginatedLocations';
export const createAmplifyListLocationsHandler = (): ListLocations => {
- let cachedLocations: ListLocationsOutput['locations'] = [];
+ let cachedItems: LocationData[] = [];
- return async function listLocations(input = {}) {
- const { pageSize, nextToken } = input;
+ return async function listLocations(input: ListLocationsInput) {
+ const { options } = input ?? {};
+ const { nextToken, pageSize } = options ?? {};
- if (cachedLocations.length > 0) {
+ if (cachedItems.length > 0) {
return getPaginatedLocations({
- locations: cachedLocations,
+ items: cachedItems,
pageSize,
nextToken,
});
@@ -20,20 +23,22 @@ export const createAmplifyListLocationsHandler = (): ListLocations => {
const { locations }: { locations: ListPathsOutput['locations'] } =
await listPaths();
- const sanitizedLocations = locations.map(
+ const sanitizedItems: LocationData[] = locations.map(
({ bucket, permission, prefix, type }) => {
return {
type,
permissions: parseAmplifyAuthPermission(permission),
- scope: `s3://${bucket}/${prefix}`,
+ bucket,
+ prefix: prefix.endsWith('*') ? prefix.slice(0, -1) : prefix,
+ id: crypto.randomUUID(),
};
}
);
- cachedLocations = sanitizedLocations;
+ cachedItems = sanitizedItems;
return getPaginatedLocations({
- locations: cachedLocations,
+ items: cachedItems,
pageSize,
nextToken,
});
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/getPaginatedLocations.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/getPaginatedLocations.ts
index e9eb5b4c4f9..c9d7f743fc1 100644
--- a/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/getPaginatedLocations.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/createAmplifyAuthAdapter/getPaginatedLocations.ts
@@ -1,38 +1,34 @@
-import { ListLocationsOutput } from '../types';
+import { ListLocationsHandlerOutput, LocationData } from '../../actions';
export const getPaginatedLocations = ({
- locations,
+ items,
pageSize,
nextToken,
}: {
- locations: ListLocationsOutput['locations'];
+ items: LocationData[];
pageSize?: number;
nextToken?: string;
-}): { locations: ListLocationsOutput['locations']; nextToken?: string } => {
+}): ListLocationsHandlerOutput => {
if (pageSize) {
if (nextToken) {
- if (Number(nextToken) > locations.length) {
- return { locations: [], nextToken: undefined };
+ if (Number(nextToken) > items.length) {
+ return { items: [], nextToken: undefined };
}
const start = -nextToken;
const end = start + pageSize < 0 ? start + pageSize : undefined;
return {
- locations: locations.slice(start, end),
+ items: items.slice(start, end),
nextToken: end ? `${-end}` : undefined,
};
}
return {
- locations: locations.slice(0, pageSize),
+ items: items.slice(0, pageSize),
nextToken:
- locations.length > pageSize
- ? `${locations.length - pageSize}`
- : undefined,
+ items.length > pageSize ? `${items.length - pageSize}` : undefined,
};
}
- return {
- locations,
- };
+ return { items, nextToken: undefined };
};
diff --git a/packages/react-storage/src/components/StorageBrowser/__tests__/adapters/managedAuthConfigAdapter/createManagedAuthConfigAdapter.test.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/__tests__/createManagedAuthConfigAdapter.test.ts
similarity index 76%
rename from packages/react-storage/src/components/StorageBrowser/__tests__/adapters/managedAuthConfigAdapter/createManagedAuthConfigAdapter.test.ts
rename to packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/__tests__/createManagedAuthConfigAdapter.test.ts
index 728f959b00d..f01e4f9cd26 100644
--- a/packages/react-storage/src/components/StorageBrowser/__tests__/adapters/managedAuthConfigAdapter/createManagedAuthConfigAdapter.test.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/__tests__/createManagedAuthConfigAdapter.test.ts
@@ -1,15 +1,10 @@
import { createManagedAuthAdapter } from '../../../adapters/createManagedAuthAdapter/createManagedAuthAdapter';
-import { createListLocationsHandler } from '../../../adapters/createManagedAuthAdapter/createListLocationsHandler';
import { createLocationCredentialsHandler } from '../../../adapters/createManagedAuthAdapter/createLocationCredentialsHandler';
-jest.mock(
- '../../../adapters/createManagedAuthAdapter/createListLocationsHandler'
-);
jest.mock(
'../../../adapters/createManagedAuthAdapter/createLocationCredentialsHandler'
);
-const mockCreateListLocationsHandler = jest.mocked(createListLocationsHandler);
const mockCreateLocationCredentialsHandler = jest.mocked(
createLocationCredentialsHandler
);
@@ -19,14 +14,10 @@ describe('createManagedAuthConfigAdapter', () => {
const accountId = 'XXXXXXXXXXXX';
const credentialsProvider = jest.fn();
const customEndpoint = 'mock-endpoint';
- const mockCreatedListLocationsHandler = jest.fn();
const mockCreatedLocationCredentialsHandler = jest.fn();
const mockRegisterAuthListener = jest.fn();
beforeEach(() => {
- mockCreateListLocationsHandler.mockReturnValue(
- mockCreatedListLocationsHandler
- );
mockCreateLocationCredentialsHandler.mockReturnValue(
mockCreatedLocationCredentialsHandler
);
@@ -60,13 +51,7 @@ describe('createManagedAuthConfigAdapter', () => {
registerAuthListener: mockRegisterAuthListener,
})
).toMatchObject({
- listLocations: mockCreatedListLocationsHandler,
- });
- expect(mockCreateListLocationsHandler).toHaveBeenCalledWith({
- region,
- accountId,
- credentialsProvider,
- customEndpoint,
+ listLocations: expect.any(Function),
});
});
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createListLocationsHandler.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createListLocationsHandler.ts
deleted file mode 100644
index 0ade0c82107..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createListLocationsHandler.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ListLocations } from '../types';
-import {
- listCallerAccessGrants,
- CredentialsProvider,
-} from '../../storage-internal';
-import { parseAccessGrantPermission } from '../permissionParsers';
-
-interface CreateListLocationsHandlerInput {
- accountId: string;
- credentialsProvider: CredentialsProvider;
- region: string;
- customEndpoint?: string;
-}
-
-export const createListLocationsHandler = (
- handlerInput: CreateListLocationsHandlerInput
-): ListLocations => {
- return async function listLocations(input = {}) {
- const { locations, nextToken } = await listCallerAccessGrants({
- ...input,
- ...handlerInput,
- });
-
- return {
- nextToken,
- locations: locations.map((location) => ({
- ...location,
- permissions: parseAccessGrantPermission(location.permission),
- })),
- };
- };
-};
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.ts b/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.ts
index cfb77772394..b242f42a4e8 100644
--- a/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.ts
@@ -1,9 +1,9 @@
-import { createListLocationsHandler } from './createListLocationsHandler';
import { createLocationCredentialsHandler } from './createLocationCredentialsHandler';
import {
StorageBrowserAuthAdapter,
CreateManagedAuthAdapterInput,
} from '../types';
+import { listLocationsHandler, ListLocationsInput } from '../../actions';
/**
* Create configuration including handlers to call S3 Access Grant APIs to list and get
@@ -19,12 +19,15 @@ export const createManagedAuthAdapter = ({
region,
registerAuthListener,
}: CreateManagedAuthAdapterInput): StorageBrowserAuthAdapter => {
- const listLocations = createListLocationsHandler({
- credentialsProvider,
+ const config = {
accountId,
+ credentials: credentialsProvider,
customEndpoint,
region,
- });
+ };
+
+ const listLocations = ({ options }: ListLocationsInput = {}) =>
+ listLocationsHandler({ config, options });
const getLocationCredentials = createLocationCredentialsHandler({
credentialsProvider,
diff --git a/packages/react-storage/src/components/StorageBrowser/adapters/types.ts b/packages/react-storage/src/components/StorageBrowser/adapters/types.ts
index e6d4af2b089..521c2fd5b49 100644
--- a/packages/react-storage/src/components/StorageBrowser/adapters/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/adapters/types.ts
@@ -4,12 +4,7 @@ import {
CredentialsLocation,
} from '../credentials/types';
import { CredentialsProvider } from '../storage-internal';
-import { LocationType } from '../actions';
-
-export interface ListLocationsInput {
- pageSize?: number;
- nextToken?: string;
-}
+import { LocationType, ListLocations } from '../actions';
export interface LocationAccess extends CredentialsLocation {
/**
@@ -21,15 +16,6 @@ export interface LocationAccess extends CredentialsLocation {
readonly type: LocationType;
}
-export interface ListLocationsOutput {
- locations: LocationAccess[];
- nextToken?: string;
-}
-
-export interface ListLocations {
- (input: ListLocationsInput): Promise;
-}
-
export interface CreateManagedAuthAdapterInput {
accountId: string;
region: string;
diff --git a/packages/react-storage/src/components/StorageBrowser/components/BreadcrumbNavigation.tsx b/packages/react-storage/src/components/StorageBrowser/components/BreadcrumbNavigation.tsx
index 6249ff6d9f0..328e9185435 100644
--- a/packages/react-storage/src/components/StorageBrowser/components/BreadcrumbNavigation.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/components/BreadcrumbNavigation.tsx
@@ -10,6 +10,7 @@ import {
import { STORAGE_BROWSER_BLOCK_TO_BE_UPDATED } from '../constants';
import { isFunction } from '@aws-amplify/ui';
+import { Separator } from './Separator';
export interface BreadcrumbProps {
isCurrent?: boolean;
@@ -19,19 +20,9 @@ export interface BreadcrumbProps {
interface BreadcrumbNavigationProps {
breadcrumbs: BreadcrumbProps[];
+ role?: React.AriaRole;
}
-const Separator = () => {
- return (
-
- /
-
- );
-};
-
export const Breadcrumb = ({
isCurrent,
name,
@@ -55,7 +46,7 @@ export const Breadcrumb = ({
) : (
{name}
@@ -66,13 +57,15 @@ export const Breadcrumb = ({
);
};
-export const BreadcrumbNavigation = ({
+export function BreadcrumbNavigation({
breadcrumbs,
-}: BreadcrumbNavigationProps): React.JSX.Element => {
+ role = 'navigation',
+}: BreadcrumbNavigationProps): React.JSX.Element {
return (
);
-};
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/components/DescriptionList.tsx b/packages/react-storage/src/components/StorageBrowser/components/DescriptionList.tsx
index 59d34226aa0..7cf95ce1d46 100644
--- a/packages/react-storage/src/components/StorageBrowser/components/DescriptionList.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/components/DescriptionList.tsx
@@ -10,8 +10,8 @@ import {
import { STORAGE_BROWSER_BLOCK_TO_BE_UPDATED } from '../constants';
export interface DescriptionItemProps {
- term?: string;
- details?: string | JSX.Element;
+ term?: string | string[];
+ details?: string | string[];
}
interface DescriptionProps {
diff --git a/packages/react-storage/src/components/StorageBrowser/components/DropdownMenu.tsx b/packages/react-storage/src/components/StorageBrowser/components/DropdownMenu.tsx
index 81fe1a14b10..8b4d8b5ece0 100644
--- a/packages/react-storage/src/components/StorageBrowser/components/DropdownMenu.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/components/DropdownMenu.tsx
@@ -96,6 +96,7 @@ export function DropdownMenu({
icon={icon}
label={label}
onClick={() => {
+ setIsOpen(false);
onItemSelect?.(id);
}}
/>
diff --git a/packages/react-storage/src/components/StorageBrowser/components/Separator.tsx b/packages/react-storage/src/components/StorageBrowser/components/Separator.tsx
new file mode 100644
index 00000000000..058ebbc899f
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/components/Separator.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { SpanElement } from '../context/elements';
+
+import { STORAGE_BROWSER_BLOCK_TO_BE_UPDATED } from '../constants';
+
+export function Separator(): React.JSX.Element {
+ return (
+
+ /
+
+ );
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/composables/ActionDestination.tsx b/packages/react-storage/src/components/StorageBrowser/composables/ActionDestination.tsx
new file mode 100644
index 00000000000..d3a52d75fc9
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/composables/ActionDestination.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+
+import { STORAGE_BROWSER_BLOCK_TO_BE_UPDATED } from '../constants';
+import {
+ DescriptionListElement,
+ DescriptionTermElement,
+ DescriptionDetailsElement,
+ SpanElement,
+ ViewElement,
+} from '../context/elements';
+import { Separator } from '../components/Separator';
+import { NavigationProps } from './Navigation';
+import { BreadcrumbNavigation } from '../components/BreadcrumbNavigation';
+
+export interface ActionDestinationProps {
+ isNavigable?: boolean;
+ items: NavigationProps['items'];
+ label?: string;
+}
+
+export const ActionDestination = ({
+ isNavigable,
+ items,
+ label,
+}: ActionDestinationProps): React.JSX.Element | null => {
+ if (!items.length) {
+ return null;
+ }
+
+ return (
+
+ {isNavigable ? (
+ <>
+ {`${label}:`}
+
+ >
+ ) : (
+
+
+ {`${label}:`}
+
+ {items.map(({ name }, index) => {
+ return (
+
+
+ {name}
+
+ {index === items.length - 1 ? null : }
+
+ );
+ })}
+
+ )}
+
+ );
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/composables/ActionExit.tsx b/packages/react-storage/src/components/StorageBrowser/composables/ActionExit.tsx
index a1e9a0255f5..5cdf32ff1b5 100644
--- a/packages/react-storage/src/components/StorageBrowser/composables/ActionExit.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/composables/ActionExit.tsx
@@ -15,7 +15,7 @@ export const ActionExit = ({
label,
}: ActionExitProps): React.JSX.Element => (
({
+ BreadcrumbNavigation: () => ,
+}));
+
+describe('ActionDestination', () => {
+ const item = 'Destination item';
+ const items = [
+ { name: `${item} 1`, onNavigate: jest.fn() },
+ { name: `${item} 2`, onNavigate: jest.fn(), isCurrent: true },
+ ];
+ const label = 'Destination label';
+
+ it('renders', () => {
+ render();
+
+ const list = screen.getByRole('list');
+ const term = screen.getByRole('term');
+ const definitions = screen.getAllByRole('definition');
+
+ expect(list).toBeInTheDocument();
+ expect(term).toHaveTextContent(label);
+ expect(definitions[0]).toHaveTextContent(`${item} 1`);
+ expect(definitions[1]).toHaveTextContent(`${item} 2`);
+ });
+
+ it('renders a breadcrumbs navigation if destination should be navigable', () => {
+ render();
+
+ const navigation = screen.getByTestId('breadcrumb-navigation');
+
+ expect(navigation).toBeInTheDocument();
+ expect(navigation.previousSibling).toHaveTextContent(label);
+ });
+
+ it('returns null if there are no navigation items', () => {
+ render();
+
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/composables/defaults.ts b/packages/react-storage/src/components/StorageBrowser/composables/defaults.ts
index 867d3260ee9..1681570811a 100644
--- a/packages/react-storage/src/components/StorageBrowser/composables/defaults.ts
+++ b/packages/react-storage/src/components/StorageBrowser/composables/defaults.ts
@@ -1,4 +1,5 @@
import { ActionCancel } from './ActionCancel';
+import { ActionDestination } from './ActionDestination';
import { ActionExit } from './ActionExit';
import { ActionStart } from './ActionStart';
import { ActionsList } from './ActionsList';
@@ -22,6 +23,7 @@ import { Composables } from './types';
export const DEFAULT_COMPOSABLES: Composables = {
ActionCancel,
+ ActionDestination,
ActionExit,
ActionStart,
ActionsList,
diff --git a/packages/react-storage/src/components/StorageBrowser/composables/types.ts b/packages/react-storage/src/components/StorageBrowser/composables/types.ts
index 664884b18ee..d3a31a4be19 100644
--- a/packages/react-storage/src/components/StorageBrowser/composables/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/composables/types.ts
@@ -1,4 +1,5 @@
import { ActionCancelProps } from './ActionCancel';
+import { ActionDestinationProps } from './ActionDestination';
import { ActionExitProps } from './ActionExit';
import { ActionStartProps } from './ActionStart';
import { ActionsListProps } from './ActionsList';
@@ -20,6 +21,7 @@ import { TitleProps } from './Title';
export interface Composables {
ActionCancel: React.ComponentType;
+ ActionDestination: React.ComponentType;
ActionExit: React.ComponentType;
ActionStart: React.ComponentType;
ActionsList: React.ComponentType;
diff --git a/packages/react-storage/src/components/StorageBrowser/context/elements/IconElement.tsx b/packages/react-storage/src/components/StorageBrowser/context/elements/IconElement.tsx
index 0ca9056a35f..80eabddd54e 100644
--- a/packages/react-storage/src/components/StorageBrowser/context/elements/IconElement.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/context/elements/IconElement.tsx
@@ -1,9 +1,8 @@
-import {
- defineBaseElementWithRef,
- withBaseElementProps,
-} from '@aws-amplify/ui-react-core/elements';
import React from 'react';
+import { defineBaseElement } from '@aws-amplify/ui-react-core/elements';
+import { useIcons } from '@aws-amplify/ui-react/internal';
+
export type IconElementProps = React.ComponentProps;
export type IconVariant =
@@ -136,11 +135,7 @@ const DEFAULT_ICON_ATTRIBUTES = {
role: 'img',
};
-export const BaseIconElement = defineBaseElementWithRef<
- 'svg',
- never,
- IconVariant
->({
+export const BaseIconElement = defineBaseElement<'svg', never, IconVariant>({
type: 'svg',
displayName: 'Icon',
});
@@ -162,4 +157,14 @@ const getIconProps = ({
};
};
-export const IconElement = withBaseElementProps(BaseIconElement, getIconProps);
+export const IconElement = (props: IconElementProps): React.JSX.Element => {
+ const { variant } = props;
+ const icons = useIcons('storageBrowser');
+
+ const icon = variant ? icons?.[variant] : undefined;
+ if (icon) {
+ return <>{icon}>;
+ }
+
+ return ;
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/context/elements/defaults.tsx b/packages/react-storage/src/components/StorageBrowser/context/elements/defaults.tsx
index 17fcaaf2377..9cfea0f4537 100644
--- a/packages/react-storage/src/components/StorageBrowser/context/elements/defaults.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/context/elements/defaults.tsx
@@ -130,7 +130,7 @@ function Heading(props: HeadingElementProps): React.JSX.Element {
function Span(props: SpanElementProps): React.JSX.Element {
const { variant } = props;
- if (variant === 'navigate-text') {
+ if (variant === 'navigation-text' || variant === 'destination-text') {
return (
<_View
{...props}
diff --git a/packages/react-storage/src/components/StorageBrowser/context/elements/definitions.ts b/packages/react-storage/src/components/StorageBrowser/context/elements/definitions.ts
index 266bb3cf0f4..d7b2ab3879a 100644
--- a/packages/react-storage/src/components/StorageBrowser/context/elements/definitions.ts
+++ b/packages/react-storage/src/components/StorageBrowser/context/elements/definitions.ts
@@ -79,7 +79,7 @@ export const LabelElement = defineBaseElement<'label', 'htmlFor'>({
export interface NavElementProps
extends React.ComponentProps {}
-export const NavElement = defineBaseElement({
+export const NavElement = defineBaseElement<'nav', 'role'>({
type: 'nav',
displayName: 'Nav',
});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/ActionCancelControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/ActionCancelControl.tsx
index bd01b4ff6e2..f028f5b9080 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/ActionCancelControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/ActionCancelControl.tsx
@@ -1,15 +1,13 @@
import React from 'react';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { ActionCancel } from '../composables/ActionCancel';
+
import { useActionCancel } from './hooks/useActionCancel';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
export const ActionCancelControl = (): React.JSX.Element => {
const props = useActionCancel();
- const ResolvedActionCancel = useResolvedComposable(
- ActionCancel,
- 'ActionCancel'
- );
+ const Resolved = useResolvedComposable(ActionCancel, 'ActionCancel');
- return ;
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/ActionDestinationControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/ActionDestinationControl.tsx
new file mode 100644
index 00000000000..b281b7f139f
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/ActionDestinationControl.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { ActionDestination } from '../composables/ActionDestination';
+
+import { useActionDestination } from './hooks/useActionDestination';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
+
+export const ActionDestinationControl = (): React.JSX.Element => {
+ const props = useActionDestination();
+
+ const Resolved = useResolvedComposable(
+ ActionDestination,
+ 'ActionDestination'
+ );
+
+ return ;
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/ActionExitControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/ActionExitControl.tsx
index af8d95b6b76..574131dd6e0 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/ActionExitControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/ActionExitControl.tsx
@@ -1,12 +1,13 @@
import React from 'react';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { ActionExit } from '../composables/ActionExit';
+
import { useActionExit } from './hooks/useActionExit';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
export const ActionExitControl = (): React.JSX.Element => {
const props = useActionExit();
- const ResolvedActionExit = useResolvedComposable(ActionExit, 'ActionExit');
+ const Resolved = useResolvedComposable(ActionExit, 'ActionExit');
- return ;
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx
index b0d4ad1a574..ccef982bb1b 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/ActionStartControl.tsx
@@ -1,12 +1,13 @@
import React from 'react';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { ActionStart } from '../composables/ActionStart';
+
import { useActionStart } from './hooks/useActionStart';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
export const ActionStartControl = (): React.JSX.Element => {
const props = useActionStart();
- const ResolvedActionStart = useResolvedComposable(ActionStart, 'ActionStart');
+ const Resolved = useResolvedComposable(ActionStart, 'ActionStart');
- return ;
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/ActionsListControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/ActionsListControl.tsx
index aff538ef0f4..0b4abe94ab5 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/ActionsListControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/ActionsListControl.tsx
@@ -1,20 +1,13 @@
import React from 'react';
-import { ControlProps } from './types';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { ActionsList } from '../composables/ActionsList';
+
import { useActionsList } from './hooks/useActionsList';
-import { ViewElement } from '../context/elements';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const ActionsListControl = ({
- className,
-}: ControlProps): React.JSX.Element => {
+export const ActionsListControl = (): React.JSX.Element => {
const props = useActionsList();
- const ResolvedActionsList = useResolvedComposable(ActionsList, 'ActionsList');
+ const Resolved = useResolvedComposable(ActionsList, 'ActionsList');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/AddFilesControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/AddFilesControl.tsx
index cfbd5c74ce9..b9f6fdfe8ce 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/AddFilesControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/AddFilesControl.tsx
@@ -1,20 +1,13 @@
import React from 'react';
-import { ControlProps } from './types';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { AddFiles } from '../composables/AddFiles';
+
import { useAddFiles } from './hooks/useAddFiles';
-import { ViewElement } from '../context/elements';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const AddFilesControl = ({
- className,
-}: ControlProps): React.JSX.Element => {
+export const AddFilesControl = (): React.JSX.Element => {
const props = useAddFiles();
- const ResolvedAddFiles = useResolvedComposable(AddFiles, 'AddFiles');
+ const Resolved = useResolvedComposable(AddFiles, 'AddFiles');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/AddFolderControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/AddFolderControl.tsx
index ebe8e70665f..b22b41de476 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/AddFolderControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/AddFolderControl.tsx
@@ -1,20 +1,13 @@
import React from 'react';
-import { ControlProps } from './types';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { AddFolder } from '../composables/AddFolder';
+
import { useAddFolder } from './hooks/useAddFolder';
-import { ViewElement } from '../context/elements';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const AddFolderControl = ({
- className,
-}: ControlProps): React.JSX.Element => {
+export const AddFolderControl = (): React.JSX.Element => {
const props = useAddFolder();
- const ResolvedAddFolder = useResolvedComposable(AddFolder, 'AddFolder');
+ const Resolved = useResolvedComposable(AddFolder, 'AddFolder');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/DataRefreshControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/DataRefreshControl.tsx
index b7d7bad6583..d2ff2019a94 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/DataRefreshControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/DataRefreshControl.tsx
@@ -1,20 +1,13 @@
import React from 'react';
import { DataRefresh } from '../composables/DataRefresh';
-import { ViewElement } from '../context/elements';
-import { ControlProps } from './types';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
+
import { useDataRefresh } from './hooks/useDataRefresh';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const DataRefreshControl = ({
- className,
-}: ControlProps): React.JSX.Element => {
+export const DataRefreshControl = (): React.JSX.Element => {
const props = useDataRefresh();
- const ResolvedDataRefresh = useResolvedComposable(DataRefresh, 'DataRefresh');
+ const Resolved = useResolvedComposable(DataRefresh, 'DataRefresh');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/DataTableControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/DataTableControl.tsx
index 5d505d7874a..c297d0e9a00 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/DataTableControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/DataTableControl.tsx
@@ -1,13 +1,14 @@
import React from 'react';
import { DataTable } from '../composables/DataTable';
+
import { useDataTable } from './hooks/useDataTable';
import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const DataTableControl = (): React.JSX.Element | null => {
+export const DataTableControl = (): React.JSX.Element => {
const props = useDataTable();
- const ResolvedDataTable = useResolvedComposable(DataTable, 'DataTable');
+ const Resolved = useResolvedComposable(DataTable, 'DataTable');
- return ;
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/DropZoneControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/DropZoneControl.tsx
index a0ea1d6a862..3cf4a5683f9 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/DropZoneControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/DropZoneControl.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { DropZone } from '../composables/DropZone';
+
import { useDropZone } from './hooks/useDropZone';
import { useResolvedComposable } from './hooks/useResolvedComposable';
@@ -8,10 +9,10 @@ export const DropZoneControl = ({
children,
}: {
children: React.ReactNode;
-}): React.JSX.Element | null => {
+}): React.JSX.Element => {
const props = useDropZone();
- const ResolvedDropZone = useResolvedComposable(DropZone, 'DropZone');
+ const Resolved = useResolvedComposable(DropZone, 'DropZone');
- return {children};
+ return {children};
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/FolderNameFieldControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/FolderNameFieldControl.tsx
index 6def95a6afc..fc6b0739bb2 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/FolderNameFieldControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/FolderNameFieldControl.tsx
@@ -1,21 +1,14 @@
import React from 'react';
import { FolderNameField } from '../composables/FolderNameField';
-import { ViewElement } from '../context/elements';
+
import { useFolderNameField } from './hooks/useFolderNameField';
import { useResolvedComposable } from './hooks/useResolvedComposable';
-import { ControlProps } from './types';
-export const FolderNameFieldControl = ({
- className,
-}: ControlProps): React.JSX.Element | null => {
+export const FolderNameFieldControl = (): React.JSX.Element => {
const props = useFolderNameField();
const Resolved = useResolvedComposable(FolderNameField, 'FolderNameField');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/LoadingIndicatorControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/LoadingIndicatorControl.tsx
index 9fe41f9fcab..524931c2198 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/LoadingIndicatorControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/LoadingIndicatorControl.tsx
@@ -1,21 +1,14 @@
import React from 'react';
import { LoadingIndicator } from '../composables/LoadingIndicator';
-import { ViewElement } from '../context/elements';
+
import { useLoadingIndicator } from './hooks/useLoadingIndicator';
import { useResolvedComposable } from './hooks/useResolvedComposable';
-import { ControlProps } from './types';
-export const LoadingIndicatorControl = ({
- className,
-}: ControlProps): React.JSX.Element | null => {
+export const LoadingIndicatorControl = (): React.JSX.Element => {
const props = useLoadingIndicator();
const Resolved = useResolvedComposable(LoadingIndicator, 'LoadingIndicator');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/MessageControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/MessageControl.tsx
index 133aa09630b..bce2408bea1 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/MessageControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/MessageControl.tsx
@@ -1,10 +1,11 @@
import React from 'react';
import { Message } from '../composables/Message';
+
import { useMessage } from './hooks/useMessage';
import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const MessageControl = (): React.JSX.Element | null => {
+export const MessageControl = (): React.JSX.Element => {
const props = useMessage();
const Resolved = useResolvedComposable(Message, 'Message');
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/NavigationControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/NavigationControl.tsx
index 692e7a1459d..4c8665e1217 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/NavigationControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/NavigationControl.tsx
@@ -1,21 +1,14 @@
import React from 'react';
import { Navigation } from '../composables/Navigation';
-import { ViewElement } from '../context/elements';
-import { ControlProps } from './types';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
+
import { useNavigation } from './hooks/useNavigation';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const NavigationControl = ({
- className,
-}: ControlProps): React.JSX.Element | null => {
+export const NavigationControl = (): React.JSX.Element => {
const props = useNavigation();
- const ResolvedNavigation = useResolvedComposable(Navigation, 'Navigation');
+ const Resolved = useResolvedComposable(Navigation, 'Navigation');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/OverwriteToggleControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/OverwriteToggleControl.tsx
index c06875aeb30..9dd5eada0a1 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/OverwriteToggleControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/OverwriteToggleControl.tsx
@@ -1,23 +1,13 @@
import React from 'react';
-import { ControlProps } from './types';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
import { OverwriteToggle } from '../composables/OverwriteToggle';
+
import { useOverwriteToggle } from './hooks/useOverwriteToggle';
-import { ViewElement } from '../context/elements';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const OverwriteToggleControl = ({
- className,
-}: ControlProps): React.JSX.Element => {
+export const OverwriteToggleControl = (): React.JSX.Element => {
const props = useOverwriteToggle();
- const ResolvedOverwriteToggle = useResolvedComposable(
- OverwriteToggle,
- 'OverwriteToggle'
- );
+ const Resolved = useResolvedComposable(OverwriteToggle, 'OverwriteToggle');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/PaginationControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/PaginationControl.tsx
index 529bc39f82c..dc9cd66b5de 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/PaginationControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/PaginationControl.tsx
@@ -1,21 +1,13 @@
import React from 'react';
import { Pagination } from '../composables/Pagination';
-import { ViewElement } from '../context/elements';
-import { ControlProps } from './types';
import { useResolvedComposable } from './hooks/useResolvedComposable';
import { useControlsContext } from './context';
-export const PaginationControl = ({
- className,
-}: ControlProps): React.JSX.Element | null => {
+export const PaginationControl = (): React.JSX.Element => {
const { data } = useControlsContext();
- const ResolvedPagination = useResolvedComposable(Pagination, 'Pagination');
+ const Resolved = useResolvedComposable(Pagination, 'Pagination');
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/SearchControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/SearchControl.tsx
index fc30d017ef4..22c8e75a434 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/SearchControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/SearchControl.tsx
@@ -1,10 +1,11 @@
import React from 'react';
import { SearchField } from '../composables/SearchField';
-import { useResolvedComposable } from './hooks/useResolvedComposable';
+
import { useControlsContext } from './context';
+import { useResolvedComposable } from './hooks/useResolvedComposable';
-export const SearchControl = (): React.JSX.Element | null => {
+export const SearchControl = (): React.JSX.Element => {
const { data, onSearch, onSearchQueryChange, onSearchClear } =
useControlsContext();
const {
@@ -13,10 +14,10 @@ export const SearchControl = (): React.JSX.Element | null => {
searchQuery,
searchSubmitLabel,
} = data;
- const ResolvedSearch = useResolvedComposable(SearchField, 'SearchField');
+ const Resolved = useResolvedComposable(SearchField, 'SearchField');
return (
- {
+export const SearchSubfoldersToggleControl = (): React.JSX.Element => {
const props = useSearchSubfoldersToggle();
- const ResolvedSearchSubfoldersToggle = useResolvedComposable(
+ const Resolved = useResolvedComposable(
SearchSubfoldersToggle,
'SearchSubfoldersToggle'
);
- return (
-
-
-
- );
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/StatusDisplayControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/StatusDisplayControl.tsx
index 03240475e67..1f13ef7e0fb 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/StatusDisplayControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/StatusDisplayControl.tsx
@@ -1,16 +1,14 @@
import React from 'react';
import { StatusDisplay } from '../composables/StatusDisplay';
+
import { useResolvedComposable } from './hooks/useResolvedComposable';
import { useStatusDisplay } from './hooks/useStatusDisplay';
-export const StatusDisplayControl = (): React.JSX.Element | null => {
+export const StatusDisplayControl = (): React.JSX.Element => {
const props = useStatusDisplay();
- const ResolvedStatusDisplay = useResolvedComposable(
- StatusDisplay,
- 'StatusDisplay'
- );
+ const Resolved = useResolvedComposable(StatusDisplay, 'StatusDisplay');
- return ;
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/TitleControl.tsx b/packages/react-storage/src/components/StorageBrowser/controls/TitleControl.tsx
index 58afcc84a72..90e44eb2373 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/TitleControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/TitleControl.tsx
@@ -1,12 +1,13 @@
import React from 'react';
import { Title } from '../composables/Title';
+
import { useResolvedComposable } from './hooks/useResolvedComposable';
import { useTitle } from './hooks/useTitle';
export const TitleControl = (): React.JSX.Element => {
const props = useTitle();
- const ResolvedTitle = useResolvedComposable(Title, 'Title');
+ const Resolved = useResolvedComposable(Title, 'Title');
- return ;
+ return ;
};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/__tests__/ActionDestinationControl.spec.tsx b/packages/react-storage/src/components/StorageBrowser/controls/__tests__/ActionDestinationControl.spec.tsx
new file mode 100644
index 00000000000..778864d2583
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/__tests__/ActionDestinationControl.spec.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { ActionDestinationControl } from '../ActionDestinationControl';
+import { useActionDestination } from '../hooks/useActionDestination';
+import { useResolvedComposable } from '../hooks/useResolvedComposable';
+
+jest.mock('../hooks/useActionDestination');
+jest.mock('../hooks/useResolvedComposable');
+jest.mock('../../composables/ActionDestination', () => ({
+ ActionDestination: () => ,
+}));
+
+describe('ActionDestinationControl', () => {
+ const mockUseActionDestination = jest.mocked(useActionDestination);
+ const mockUseResolvedComposable = jest.mocked(useResolvedComposable);
+
+ beforeAll(() => {
+ mockUseResolvedComposable.mockImplementation(
+ (component) => component as () => React.JSX.Element
+ );
+ });
+
+ afterEach(() => {
+ mockUseActionDestination.mockClear();
+ });
+
+ it('renders', () => {
+ render();
+
+ const ActionDestination = screen.getByTestId('action-destination');
+
+ expect(ActionDestination).toBeInTheDocument();
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/__tests__/StatusDisplayControl.spec.tsx b/packages/react-storage/src/components/StorageBrowser/controls/__tests__/StatusDisplayControl.spec.tsx
index 1256ba7d91e..9847230c7e1 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/__tests__/StatusDisplayControl.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/controls/__tests__/StatusDisplayControl.spec.tsx
@@ -41,14 +41,4 @@ describe('StatusDisplayControl', () => {
expect(bar).toHaveTextContent('2/6');
expect(qux).toHaveTextContent('3/6');
});
-
- it('returns null without props', () => {
- mockUseStatusDisplay.mockReturnValue({});
-
- render();
-
- expect(screen.queryByRole('list')).not.toBeInTheDocument();
- expect(screen.queryByRole('term')).not.toBeInTheDocument();
- expect(screen.queryByRole('definition')).not.toBeInTheDocument();
- });
});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareButtonData.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareButtonData.spec.ts
deleted file mode 100644
index f91b817a414..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareButtonData.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { SortDirection } from '../../../composables/DataTable';
-import { compareButtonData } from '../compareButtonData';
-
-describe('compareButtonData', () => {
- const emptyContent = { type: 'button' as const, content: {} };
- const a = { ...emptyContent, content: { label: 'a' } };
- const b = { ...emptyContent, content: { label: 'b' } };
- const getComparisonResults = (direction: SortDirection) => [
- compareButtonData(emptyContent, emptyContent, direction),
- compareButtonData(emptyContent, b, direction),
- compareButtonData(a, emptyContent, direction),
- compareButtonData(a, a, direction),
- compareButtonData(a, b, direction),
- compareButtonData(b, a, direction),
- ];
-
- it('should compare button data in ascending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('ascending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeGreaterThan(0);
- expect(bIsUndefined).toBeLessThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeLessThan(0);
- expect(bIsBeforeA).toBeGreaterThan(0);
- });
-
- it('should compare button data in descending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('descending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeLessThan(0);
- expect(bIsUndefined).toBeGreaterThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeGreaterThan(0);
- expect(bIsBeforeA).toBeLessThan(0);
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareDateData.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareDateData.spec.ts
deleted file mode 100644
index a163c46f9b0..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareDateData.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { SortDirection } from '../../../composables/DataTable';
-import { compareDateData } from '../compareDateData';
-
-describe('compareDateData', () => {
- const emptyContent = { type: 'date' as const, content: {} };
- const a = { ...emptyContent, content: { date: new Date(1600387200000) } };
- const b = { ...emptyContent, content: { date: new Date(1702339200000) } };
- const getComparisonResults = (direction: SortDirection) => [
- compareDateData(emptyContent, emptyContent, direction),
- compareDateData(emptyContent, b, direction),
- compareDateData(a, emptyContent, direction),
- compareDateData(a, a, direction),
- compareDateData(a, b, direction),
- compareDateData(b, a, direction),
- ];
-
- it('should compare date data in ascending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('ascending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeGreaterThan(0);
- expect(bIsUndefined).toBeLessThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeLessThan(0);
- expect(bIsBeforeA).toBeGreaterThan(0);
- });
-
- it('should compare date data in descending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('descending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeLessThan(0);
- expect(bIsUndefined).toBeGreaterThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeGreaterThan(0);
- expect(bIsBeforeA).toBeLessThan(0);
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareNumberData.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareNumberData.spec.ts
deleted file mode 100644
index fad9118fef9..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareNumberData.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { SortDirection } from '../../../composables/DataTable';
-import { compareNumberData } from '../compareNumberData';
-
-describe('compareNumberData', () => {
- const emptyContent = { type: 'number' as const, content: {} };
- const a = { ...emptyContent, content: { value: 1 } };
- const b = { ...emptyContent, content: { value: 2 } };
- const getComparisonResults = (direction: SortDirection) => [
- compareNumberData(emptyContent, emptyContent, direction),
- compareNumberData(emptyContent, b, direction),
- compareNumberData(a, emptyContent, direction),
- compareNumberData(a, a, direction),
- compareNumberData(a, b, direction),
- compareNumberData(b, a, direction),
- ];
-
- it('should compare date data in ascending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('ascending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeGreaterThan(0);
- expect(bIsUndefined).toBeLessThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeLessThan(0);
- expect(bIsBeforeA).toBeGreaterThan(0);
- });
-
- it('should compare date data in descending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('descending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeLessThan(0);
- expect(bIsUndefined).toBeGreaterThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeGreaterThan(0);
- expect(bIsBeforeA).toBeLessThan(0);
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareTextData.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareTextData.spec.ts
deleted file mode 100644
index 12b2ce7eda6..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/__tests__/compareTextData.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { SortDirection } from '../../../composables/DataTable';
-import { compareTextData } from '../compareTextData';
-
-describe('compareTextData', () => {
- const emptyContent = { type: 'text' as const, content: {} };
- const a = { ...emptyContent, content: { text: 'a' } };
- const b = { ...emptyContent, content: { text: 'b' } };
- const getComparisonResults = (direction: SortDirection) => [
- compareTextData(emptyContent, emptyContent, direction),
- compareTextData(emptyContent, b, direction),
- compareTextData(a, emptyContent, direction),
- compareTextData(a, a, direction),
- compareTextData(a, b, direction),
- compareTextData(b, a, direction),
- ];
-
- it('should compare date data in ascending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('ascending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeGreaterThan(0);
- expect(bIsUndefined).toBeLessThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeLessThan(0);
- expect(bIsBeforeA).toBeGreaterThan(0);
- });
-
- it('should compare date data in descending direction', () => {
- const [
- bothAreUndefined,
- aIsUndefined,
- bIsUndefined,
- aEqualsB,
- aIsBeforeB,
- bIsBeforeA,
- ] = getComparisonResults('descending');
- expect(bothAreUndefined).toBe(0);
- expect(aIsUndefined).toBeLessThan(0);
- expect(bIsUndefined).toBeGreaterThan(0);
- expect(aEqualsB).toBe(0);
- expect(aIsBeforeB).toBeGreaterThan(0);
- expect(bIsBeforeA).toBeLessThan(0);
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareButtonData.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareButtonData.ts
deleted file mode 100644
index dea2f40f363..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareButtonData.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {
- DataTableButtonDataCell,
- SortDirection,
-} from '../../composables/DataTable';
-
-const compareContent = (
- { label: a }: DataTableButtonDataCell['content'],
- { label: b }: DataTableButtonDataCell['content']
-): number => {
- if (a === undefined) {
- return b === undefined ? 0 : 1;
- }
- return b === undefined ? -1 : a.localeCompare(b);
-};
-
-export const compareButtonData = (
- a: DataTableButtonDataCell,
- b: DataTableButtonDataCell,
- direction: SortDirection
-): number =>
- direction === 'ascending'
- ? compareContent(a.content, b.content)
- : compareContent(b.content, a.content);
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareDateData.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareDateData.ts
deleted file mode 100644
index 0ba3e54a849..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareDateData.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {
- DataTableDateDataCell,
- SortDirection,
-} from '../../composables/DataTable';
-
-export const compareContent = (
- { date: a }: DataTableDateDataCell['content'],
- { date: b }: DataTableDateDataCell['content']
-): number => {
- if (a === undefined) {
- return b === undefined ? 0 : 1;
- }
- return b === undefined ? -1 : a.getTime() - b.getTime();
-};
-
-export const compareDateData = (
- a: DataTableDateDataCell,
- b: DataTableDateDataCell,
- direction: SortDirection
-): number =>
- direction === 'ascending'
- ? compareContent(a.content, b.content)
- : compareContent(b.content, a.content);
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareNumberData.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareNumberData.ts
deleted file mode 100644
index 75b9f8513c2..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareNumberData.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {
- DataTableNumberDataCell,
- SortDirection,
-} from '../../composables/DataTable';
-
-export const compareContent = (
- { value: a }: DataTableNumberDataCell['content'],
- { value: b }: DataTableNumberDataCell['content']
-): number => {
- if (a === undefined) {
- return b === undefined ? 0 : 1;
- }
- return b === undefined ? -1 : a - b;
-};
-
-export const compareNumberData = (
- a: DataTableNumberDataCell,
- b: DataTableNumberDataCell,
- direction: SortDirection
-): number =>
- direction === 'ascending'
- ? compareContent(a.content, b.content)
- : compareContent(b.content, a.content);
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareTextData.ts b/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareTextData.ts
deleted file mode 100644
index 8a57b7d95ff..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/controls/compareFunctions/compareTextData.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {
- DataTableTextDataCell,
- SortDirection,
-} from '../../composables/DataTable';
-
-export const compareContent = (
- { text: a }: DataTableTextDataCell['content'],
- { text: b }: DataTableTextDataCell['content']
-): number => {
- if (a === undefined) {
- return b === undefined ? 0 : 1;
- }
- return b === undefined ? -1 : a.localeCompare(b);
-};
-
-export const compareTextData = (
- a: DataTableTextDataCell,
- b: DataTableTextDataCell,
- direction: SortDirection
-): number =>
- direction === 'ascending'
- ? compareContent(a.content, b.content)
- : compareContent(b.content, a.content);
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/getNavigationItems.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/getNavigationItems.spec.ts
new file mode 100644
index 00000000000..6157d52f910
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/getNavigationItems.spec.ts
@@ -0,0 +1,74 @@
+import { LocationPermissions } from '../../../actions';
+import { getNavigationItems } from '../getNavigationItems';
+
+describe('getNavigationItems', () => {
+ const uuid = 'uuid';
+ const prefix = 'prefix';
+ const partA = 'part-a';
+ const partB = 'part-b';
+ const destinationParts = [prefix, partA, partB];
+ const location = {
+ bucket: 'bucket',
+ id: 'id',
+ permissions: ['delete', 'get', 'list', 'write'] as LocationPermissions,
+ prefix: `${prefix}/`,
+ type: 'PREFIX',
+ } as const;
+ const mockOnNavigate = jest.fn();
+ const mockRandomUUID = jest.fn();
+
+ beforeAll(() => {
+ Object.defineProperty(globalThis, 'crypto', {
+ value: { randomUUID: mockRandomUUID },
+ });
+ mockRandomUUID.mockReturnValue(uuid);
+ });
+
+ afterEach(() => {
+ mockOnNavigate.mockClear();
+ });
+
+ it('returns navigation items', () => {
+ expect(
+ getNavigationItems({
+ destinationParts,
+ location,
+ onNavigate: mockOnNavigate,
+ })
+ ).toStrictEqual([
+ { name: prefix, onNavigate: expect.any(Function) },
+ { name: partA, onNavigate: expect.any(Function) },
+ { isCurrent: true, name: partB, onNavigate: expect.any(Function) },
+ ]);
+ });
+
+ it('calls onNavigate', () => {
+ const [item1, item2, item3] = getNavigationItems({
+ destinationParts,
+ location,
+ onNavigate: mockOnNavigate,
+ });
+
+ item1.onNavigate?.();
+ item2.onNavigate?.();
+ item3.onNavigate?.();
+
+ expect(mockOnNavigate).toHaveBeenNthCalledWith(
+ 1,
+ { ...location, id: uuid },
+ ''
+ );
+
+ expect(mockOnNavigate).toHaveBeenNthCalledWith(
+ 2,
+ { ...location, id: uuid },
+ `${partA}/`
+ );
+
+ expect(mockOnNavigate).toHaveBeenNthCalledWith(
+ 3,
+ { ...location, id: uuid },
+ `${partA}/${partB}/`
+ );
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/getNavigationParts.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/getNavigationParts.spec.ts
new file mode 100644
index 00000000000..baa722c6575
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/getNavigationParts.spec.ts
@@ -0,0 +1,77 @@
+import { LocationPermissions } from '../../../actions';
+import { getNavigationParts } from '../getNavigationParts';
+
+describe('getNavigationParts', () => {
+ const bucket = 'bucket';
+ const prefix = 'prefix';
+ const partA = 'a';
+ const partB = 'b';
+ const path = `${partA}/${partB}/`;
+ const locationBase = {
+ bucket,
+ id: 'id',
+ permissions: ['delete', 'get', 'list', 'write'] as LocationPermissions,
+ prefix: `${prefix}/`,
+ };
+
+ describe('PREFIX type location', () => {
+ const location = { ...locationBase, type: 'PREFIX' } as const;
+
+ it('creates a part for the prefix and each subpath', () => {
+ // prefix > a > b
+ expect(getNavigationParts({ location, path })).toStrictEqual([
+ prefix,
+ partA,
+ partB,
+ ]);
+ });
+
+ it('does not split the prefix into separate items', () => {
+ const prefixWithSlashes = 'prefix/with/slashes';
+ // prefix/with/slashes > a > b
+ expect(
+ getNavigationParts({
+ location: {
+ ...location,
+ prefix: `${prefixWithSlashes}/`,
+ },
+ path,
+ })
+ ).toStrictEqual([prefixWithSlashes, partA, partB]);
+ });
+
+ it('can include the bucket as part of the prefix', () => {
+ // bucket/prefix > a > b
+ expect(
+ getNavigationParts({ location, path, includeBucketInPrefix: true })
+ ).toStrictEqual([`${bucket}/${prefix}`, partA, partB]);
+ });
+ });
+
+ describe('BUCKET type location', () => {
+ const location = { ...locationBase, type: 'BUCKET' } as const;
+ it('creates an item for the bucket, prefix and each subpath', () => {
+ // bucket > prefix > a > b
+ expect(getNavigationParts({ location, path })).toStrictEqual([
+ bucket,
+ prefix,
+ partA,
+ partB,
+ ]);
+ });
+
+ it('does not split the prefix into separate items', () => {
+ const prefixWithSlashes = 'prefix/with/slashes';
+ // Home > bucket > prefix/with/slashes > a > b
+ expect(
+ getNavigationParts({
+ location: {
+ ...location,
+ prefix: `${prefixWithSlashes}/`,
+ },
+ path,
+ })
+ ).toStrictEqual([bucket, prefixWithSlashes, partA, partB]);
+ });
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useActionDestination.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useActionDestination.spec.ts
new file mode 100644
index 00000000000..8758bc0c2de
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useActionDestination.spec.ts
@@ -0,0 +1,81 @@
+import { renderHook } from '@testing-library/react';
+import { useControlsContext } from '../../../controls/context';
+import { useActionDestination } from '../useActionDestination';
+import { LocationPermissions } from '../../../actions';
+import { getNavigationItems } from '../getNavigationItems';
+import { getNavigationParts } from '../getNavigationParts';
+
+jest.mock('../../../controls/context');
+jest.mock('../getNavigationItems');
+jest.mock('../getNavigationParts');
+
+describe('useActionDestination', () => {
+ const actionDestinationLabel = 'action-destination-label';
+ const bucket = 'bucket';
+ const data = {
+ actionDestinationLabel,
+ destination: {
+ current: {
+ bucket,
+ id: 'id',
+ permissions: ['delete', 'get', 'list', 'write'] as LocationPermissions,
+ prefix: 'prefix/',
+ type: 'PREFIX',
+ },
+ path: 'path/',
+ key: 'prefix/path/',
+ } as const,
+ };
+ const mockGetNavigationItems = jest.mocked(getNavigationItems);
+ const mockGetNavigationParts = jest.mocked(getNavigationParts);
+ const mockUseControlsContext = jest.mocked(useControlsContext);
+ const mockOnSelectDestination = jest.fn();
+
+ beforeEach(() => {
+ mockUseControlsContext.mockReturnValue({
+ data,
+ onSelectDestination: mockOnSelectDestination,
+ });
+ mockGetNavigationItems.mockReturnValue([
+ { name: bucket, onNavigate: mockOnSelectDestination, isCurrent: true },
+ ]);
+ });
+
+ afterEach(() => {
+ mockUseControlsContext.mockReset();
+ mockGetNavigationItems.mockClear();
+ mockGetNavigationParts.mockClear();
+ });
+
+ it('returns useActionDestination data', () => {
+ const { result } = renderHook(() => useActionDestination());
+
+ expect(result.current).toStrictEqual({
+ label: actionDestinationLabel,
+ items: [
+ { name: bucket, onNavigate: expect.any(Function), isCurrent: true },
+ ],
+ isNavigable: undefined,
+ });
+ });
+
+ it('returns empty items if current location is undefined', () => {
+ mockUseControlsContext.mockReturnValue({ data: {} });
+
+ const { result } = renderHook(() => useActionDestination());
+
+ expect(result.current).toStrictEqual({ items: [] });
+ });
+
+ it('calls onSelectDestination', () => {
+ mockGetNavigationItems.mockReturnValue([
+ { name: bucket, onNavigate: mockOnSelectDestination, isCurrent: true },
+ ]);
+ const { result } = renderHook(() => useActionDestination());
+ const [navigationItem] = result.current?.items ?? [];
+
+ navigationItem?.onNavigate?.();
+
+ expect(mockOnSelectDestination).toHaveBeenCalled();
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useNavigation.spec.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useNavigation.spec.ts
index 9b73515f02d..af8539907b5 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useNavigation.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/__tests__/useNavigation.spec.ts
@@ -2,38 +2,33 @@ import { renderHook } from '@testing-library/react';
import { useControlsContext } from '../../../controls/context';
import { useNavigation } from '../useNavigation';
import { LocationPermissions } from '../../../actions';
+import { getNavigationItems } from '../getNavigationItems';
+import { getNavigationParts } from '../getNavigationParts';
jest.mock('../../../controls/context');
+jest.mock('../getNavigationItems');
+jest.mock('../getNavigationParts');
describe('useNavigation', () => {
const bucket = 'bucket';
- const prefix = 'prefix';
- const path = 'path';
const data = {
location: {
current: {
bucket,
id: 'id',
permissions: ['delete', 'get', 'list', 'write'] as LocationPermissions,
- prefix: `${prefix}/`,
+ prefix: 'prefix/',
type: 'PREFIX',
},
- path: `${path}/`,
- key: `${prefix}/${path}/`,
+ path: 'path/',
+ key: 'prefix/path/',
} as const,
};
- // assert mocks
+ const mockGetNavigationItems = jest.mocked(getNavigationItems);
+ const mockGetNavigationParts = jest.mocked(getNavigationParts);
const mockUseControlsContext = jest.mocked(useControlsContext);
- // create mocks
const mockOnNavigate = jest.fn();
const mockOnNavigateHome = jest.fn();
- const mockRandomUUID = jest.fn();
-
- beforeAll(() => {
- Object.defineProperty(globalThis, 'crypto', {
- value: { randomUUID: mockRandomUUID },
- });
- });
beforeEach(() => {
mockUseControlsContext.mockReturnValue({
@@ -41,10 +36,15 @@ describe('useNavigation', () => {
onNavigateHome: mockOnNavigateHome,
onNavigate: mockOnNavigate,
});
+ mockGetNavigationItems.mockReturnValue([
+ { name: bucket, onNavigate: mockOnNavigate, isCurrent: true },
+ ]);
});
afterEach(() => {
mockUseControlsContext.mockReset();
+ mockGetNavigationItems.mockClear();
+ mockGetNavigationParts.mockClear();
mockOnNavigate.mockClear();
mockOnNavigateHome.mockClear();
});
@@ -55,8 +55,7 @@ describe('useNavigation', () => {
expect(result.current).toStrictEqual({
items: [
{ name: 'Home', onNavigate: expect.any(Function) },
- { name: `${bucket}/${prefix}`, onNavigate: expect.any(Function) },
- { name: path, onNavigate: expect.any(Function), isCurrent: true },
+ { name: bucket, onNavigate: expect.any(Function), isCurrent: true },
],
});
});
@@ -86,177 +85,4 @@ describe('useNavigation', () => {
expect(mockOnNavigate).toHaveBeenCalled();
});
-
- describe('PREFIX type location', () => {
- it('creates an item for the prefix and each subpath', () => {
- mockUseControlsContext.mockReturnValue({
- data: { location: { ...data.location, path: 'a/b/c/' } },
- });
-
- const { result } = renderHook(() => useNavigation());
-
- // Home > bucket/prefix > a > b > c
- expect(result.current?.items).toHaveLength(5);
- });
-
- it('does not split the prefix into separate items', () => {
- const prefixWithSlashes = 'prefix/with/slashes';
- mockUseControlsContext.mockReturnValue({
- data: {
- location: {
- ...data.location,
- current: { ...data.location.current, prefix: prefixWithSlashes },
- },
- },
- });
-
- const { result } = renderHook(() => useNavigation());
-
- // Home > bucket/prefix/with/slashes > path
- expect(result.current?.items).toHaveLength(3);
- expect(result.current?.items[1].name).toBe(
- `${bucket}/${prefixWithSlashes}`
- );
- });
-
- it('creates navigation items correctly', () => {
- const foo = 'foo';
- const bar = 'bar';
- const qux = 'qux';
- mockRandomUUID
- .mockReturnValueOnce(1)
- .mockReturnValueOnce(2)
- .mockReturnValueOnce(3);
- mockUseControlsContext.mockReturnValue({
- data: { location: { ...data.location, path: `${foo}/${bar}/${qux}/` } },
- onNavigate: mockOnNavigate,
- });
-
- const { result } = renderHook(() => useNavigation());
-
- // Home > bucket/prefix > foo > bar > qux
- const [, prefixItem, fooItem, barItem] = result.current?.items ?? [];
-
- prefixItem?.onNavigate?.();
- fooItem?.onNavigate?.();
- barItem?.onNavigate?.();
-
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 1,
- { ...data.location.current, id: 1 },
- ''
- );
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 2,
- { ...data.location.current, id: 2 },
- `${foo}/`
- );
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 3,
- { ...data.location.current, id: 3 },
- `${foo}/${bar}/`
- );
- });
- });
-
- describe('BUCKET type location', () => {
- it('creates an item for the bucket, prefix and each subpath', () => {
- mockUseControlsContext.mockReturnValue({
- data: {
- location: {
- ...data.location,
- current: {
- ...data.location.current,
- type: 'BUCKET',
- },
- path: 'a/b/c/',
- },
- },
- });
-
- const { result } = renderHook(() => useNavigation());
-
- // Home > bucket > prefix > a > b > c
- expect(result.current?.items).toHaveLength(6);
- });
-
- it('does not split the prefix into separate items', () => {
- const prefixWithSlashes = 'prefix/with/slashes';
- mockUseControlsContext.mockReturnValue({
- data: {
- location: {
- ...data.location,
- current: {
- ...data.location.current,
- prefix: prefixWithSlashes,
- type: 'BUCKET',
- },
- },
- },
- });
-
- const { result } = renderHook(() => useNavigation());
-
- // Home > bucket > prefix/with/slashes > path
- expect(result.current?.items).toHaveLength(4);
- expect(result.current?.items[1].name).toBe(bucket);
- expect(result.current?.items[2].name).toBe(prefixWithSlashes);
- });
-
- it('creates navigation items correctly', () => {
- const foo = 'foo';
- const bar = 'bar';
- const qux = 'qux';
- mockRandomUUID
- .mockReturnValueOnce(1)
- .mockReturnValueOnce(2)
- .mockReturnValueOnce(3)
- .mockReturnValueOnce(4);
- mockUseControlsContext.mockReturnValue({
- data: {
- location: {
- ...data.location,
- current: {
- ...data.location.current,
- type: 'BUCKET',
- },
- path: `${foo}/${bar}/${qux}/`,
- },
- },
- onNavigate: mockOnNavigate,
- });
-
- const { result } = renderHook(() => useNavigation());
-
- // Home > bucket > prefix > foo > bar > qux
- const [, bucketItem, prefixItem, fooItem, barItem] =
- result.current?.items ?? [];
-
- bucketItem?.onNavigate?.();
- prefixItem?.onNavigate?.();
- fooItem?.onNavigate?.();
- barItem?.onNavigate?.();
-
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 1,
- { ...data.location.current, type: 'BUCKET', id: 1 },
- ''
- );
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 2,
- { ...data.location.current, type: 'BUCKET', id: 2 },
- `${prefix}/`
- );
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 3,
- { ...data.location.current, type: 'BUCKET', id: 3 },
- `${prefix}/${foo}/`
- );
- expect(mockOnNavigate).toHaveBeenNthCalledWith(
- 4,
- { ...data.location.current, type: 'BUCKET', id: 4 },
- `${prefix}/${foo}/${bar}/`
- );
- });
- });
});
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/getNavigationItems.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/getNavigationItems.ts
new file mode 100644
index 00000000000..db9d0285f3d
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/getNavigationItems.ts
@@ -0,0 +1,43 @@
+import { LocationData } from '../../actions';
+import { NavigationProps } from '../../composables/Navigation';
+
+interface GetNavigationItemsInput {
+ destinationParts: string[];
+ location: LocationData;
+ onNavigate?: (location: LocationData, path?: string) => void;
+}
+
+export const getNavigationItems = ({
+ destinationParts,
+ location,
+ onNavigate,
+}: GetNavigationItemsInput): NavigationProps['items'] => {
+ const { bucket, permissions, prefix = '', type } = location;
+ const destinationSubpaths: string[] = [];
+
+ return destinationParts.map((part, index) => {
+ const isCurrent = index === destinationParts.length - 1;
+
+ if (index !== 0) {
+ destinationSubpaths.push(part);
+ }
+
+ const destinationPath = `${destinationSubpaths.concat('').join('/')}`;
+
+ const destination = {
+ id: crypto.randomUUID(),
+ type,
+ permissions,
+ bucket,
+ prefix,
+ };
+
+ return {
+ name: part,
+ ...(isCurrent && { isCurrent }),
+ onNavigate: () => {
+ onNavigate?.(destination, destinationPath);
+ },
+ };
+ });
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/getNavigationParts.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/getNavigationParts.ts
new file mode 100644
index 00000000000..f386a4038ff
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/getNavigationParts.ts
@@ -0,0 +1,41 @@
+import { LocationData } from '../../actions';
+
+interface GetNavigationPartsInput {
+ location: LocationData;
+ path: string;
+ includeBucketInPrefix?: boolean;
+}
+
+export const getNavigationParts = ({
+ location,
+ path,
+ includeBucketInPrefix,
+}: GetNavigationPartsInput): string[] => {
+ const { bucket, prefix = '', type } = location;
+
+ const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
+ const trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path;
+
+ const firstPrefixPart = [];
+ if (type !== 'BUCKET') {
+ if (includeBucketInPrefix) {
+ firstPrefixPart.push(bucket);
+ }
+ if (trimmedPrefix) {
+ if (includeBucketInPrefix) {
+ firstPrefixPart.push('/');
+ }
+ firstPrefixPart.push(trimmedPrefix);
+ }
+ }
+
+ const prefixParts = type === 'BUCKET' ? [bucket] : [firstPrefixPart.join('')];
+
+ if (type === 'BUCKET' && trimmedPrefix) {
+ prefixParts.push(trimmedPrefix);
+ }
+
+ const pathParts = trimmedPath ? trimmedPath.split('/') : [];
+
+ return prefixParts.concat(pathParts);
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/useActionDestination.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/useActionDestination.ts
new file mode 100644
index 00000000000..bd4ba1f4873
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/useActionDestination.ts
@@ -0,0 +1,41 @@
+// import { ActionDestinationProps } from '../../composables/ActionDestination';
+import React from 'react';
+
+import { useControlsContext } from '../../controls/context';
+import { getNavigationItems } from './getNavigationItems';
+import { getNavigationParts } from './getNavigationParts';
+import { ActionDestinationProps } from '../../composables/ActionDestination';
+
+export const useActionDestination = (): ActionDestinationProps => {
+ const { data, onSelectDestination } = useControlsContext();
+ const { actionDestinationLabel, isActionDestinationNavigable, destination } =
+ data;
+
+ return React.useMemo(() => {
+ if (!destination?.current) {
+ return { items: [] };
+ }
+
+ const { current, path } = destination;
+
+ const destinationParts = getNavigationParts({
+ location: current,
+ path,
+ });
+
+ return {
+ label: actionDestinationLabel,
+ items: getNavigationItems({
+ location: current,
+ destinationParts,
+ onNavigate: onSelectDestination,
+ }),
+ isNavigable: isActionDestinationNavigable,
+ };
+ }, [
+ actionDestinationLabel,
+ isActionDestinationNavigable,
+ destination,
+ onSelectDestination,
+ ]);
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/hooks/useNavigation.ts b/packages/react-storage/src/components/StorageBrowser/controls/hooks/useNavigation.ts
index 40f46381793..94322858fac 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/hooks/useNavigation.ts
+++ b/packages/react-storage/src/components/StorageBrowser/controls/hooks/useNavigation.ts
@@ -1,6 +1,8 @@
import React from 'react';
import { NavigationProps } from '../../composables/Navigation';
import { useControlsContext } from '../../controls/context';
+import { getNavigationItems } from './getNavigationItems';
+import { getNavigationParts } from './getNavigationParts';
export const useNavigation = (): NavigationProps => {
const { data, onNavigate, onNavigateHome } = useControlsContext();
@@ -12,52 +14,20 @@ export const useNavigation = (): NavigationProps => {
}
const { current, path } = location;
- const { bucket, permissions, prefix = '', type } = current;
- const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
- const trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path;
+ const destinationParts = getNavigationParts({
+ location: current,
+ path,
+ includeBucketInPrefix: true,
+ });
- const prefixParts =
- type === 'BUCKET'
- ? ['']
- : [`${bucket}${trimmedPrefix && `/${trimmedPrefix}`}`];
+ const homeItem: NavigationProps['items'] = [
+ { name: 'Home', onNavigate: onNavigateHome },
+ ];
- if (type === 'BUCKET' && trimmedPrefix) {
- prefixParts.push(trimmedPrefix);
- }
-
- const pathParts = trimmedPath ? trimmedPath.split('/') : [];
-
- const parts = prefixParts.concat(pathParts);
- const destinationParts: string[] = [];
return {
- items: [{ name: 'Home', onNavigate: onNavigateHome }].concat(
- parts.map((part, index) => {
- const isCurrent = index === parts.length - 1;
- const name = index === 0 && type === 'BUCKET' ? bucket : part;
-
- if (index !== 0) {
- destinationParts.push(part);
- }
-
- const destinationPath = `${destinationParts.concat('').join('/')}`;
-
- const destination = {
- id: crypto.randomUUID(),
- type,
- permissions,
- bucket,
- prefix,
- };
-
- return {
- name,
- ...(isCurrent && { isCurrent }),
- onNavigate: () => {
- onNavigate?.(destination, destinationPath);
- },
- };
- })
+ items: homeItem.concat(
+ getNavigationItems({ location: current, destinationParts, onNavigate })
),
};
}, [location, onNavigate, onNavigateHome]);
diff --git a/packages/react-storage/src/components/StorageBrowser/controls/types.ts b/packages/react-storage/src/components/StorageBrowser/controls/types.ts
index 02cf758b19d..768e7058fc1 100644
--- a/packages/react-storage/src/components/StorageBrowser/controls/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/controls/types.ts
@@ -6,10 +6,6 @@ import { Composables } from '../composables/types';
import { LocationState } from '../providers/store/location';
import { StatusCounts } from '../tasks';
-export interface ControlProps {
- className?: string;
-}
-
export interface Controls {
props: React.ComponentProps;
}
@@ -43,10 +39,12 @@ export interface ControlsContext {
data: {
actions?: ActionsListItem[];
actionCancelLabel?: string;
+ actionDestinationLabel?: string;
actionExitLabel?: string;
actionStartLabel?: string;
addFilesLabel?: string;
addFolderLabel?: string;
+ destination?: LocationState;
folderNameId?: string;
folderNameLabel?: string;
folderNamePlaceholder?: string;
@@ -57,6 +55,7 @@ export interface ControlsContext {
isActionsListDisabled?: boolean;
isAddFilesDisabled?: boolean;
isAddFolderDisabled?: boolean;
+ isActionDestinationNavigable?: boolean;
isOverwritingEnabled?: boolean;
isDataRefreshDisabled?: boolean;
isLoading?: boolean;
@@ -96,6 +95,7 @@ export interface ControlsContext {
onSearch?: () => void;
onSearchClear?: () => void;
onSearchQueryChange?: (value: string) => void;
+ onSelectDestination?: (location: LocationData, path?: string) => void;
onToggleOverwrite?: () => void;
onToggleSearchSubfolders?: () => void;
onValidateFolderName?: (value: string) => void;
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser.tsx b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser.tsx
index 0a51d9c901a..0c7300b592b 100644
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser.tsx
@@ -1,83 +1,36 @@
import React from 'react';
-import { MergeBaseElements } from '@aws-amplify/ui-react-core/elements';
-
-import {
- LocationActions,
- locationActionsDefault,
-} from './do-not-import-from-here/locationActions';
-import { createTempActionsProvider } from './do-not-import-from-here/createTempActionsProvider';
import { DEFAULT_COMPOSABLES } from './composables';
-import { StorageBrowserElements } from './context/elements';
-import { Components, ComponentsProvider } from './ComponentsProvider';
+import { elementsDefault } from './context/elements';
+import { ComponentsProvider } from './ComponentsProvider';
import { ErrorBoundary } from './ErrorBoundary';
-import {
- createConfigurationProvider,
- RegisterAuthListener,
- StoreProvider,
- StoreProviderProps,
-} from './providers';
+import { createConfigurationProvider, StoreProvider } from './providers';
import { StorageBrowserDefault } from './StorageBrowserDefault';
import { assertRegisterAuthListener } from './validators';
import {
- Views,
+ CopyView,
+ CreateFolderView,
+ DeleteView,
LocationActionView,
LocationDetailView,
LocationsView,
+ UploadView,
ViewsProvider,
} from './views';
-import { GetLocationCredentials } from './credentials/types';
import { defaultActionConfigs } from './actions';
-import { createUseView } from './views/createUseView';
-import { DisplayTextProvider } from './displayText';
-import { ListLocations } from './adapters/types';
-import { StorageBrowserDisplayText } from './displayText/types';
-
-export interface Config {
- accountId?: string;
- customEndpoint?: string;
- getLocationCredentials: GetLocationCredentials;
- listLocations: ListLocations;
- registerAuthListener: RegisterAuthListener;
- region: string;
-}
-
-export interface CreateStorageBrowserInput {
- actions?: LocationActions;
- config: Config;
- components?: Components;
- elements?: Partial;
-}
-
-export interface StorageBrowserProps {
- views?: Views;
- displayText?: StorageBrowserDisplayText;
-}
-export interface StorageBrowserComponent extends Views {
- (
- props: StorageBrowserProps & Exclude
- ): React.JSX.Element;
- displayName: string;
- Provider: (props: StorageBrowserProviderProps) => React.JSX.Element;
-}
-
-export interface ResolvedStorageBrowserElements<
- T extends Partial,
-> extends MergeBaseElements {}
-
-export type ActionViewName = Exclude<
- T,
- 'listLocationItems' | 'listLocations'
->;
+import { DisplayTextProvider } from './displayText';
+import { createUseView } from './views/createUseView';
-export interface StorageBrowserProviderProps extends StoreProviderProps {
- displayText?: StorageBrowserDisplayText;
-}
+import {
+ CreateStorageBrowserInput,
+ StorageBrowserProviderProps,
+ StorageBrowserType,
+} from './types';
export function createStorageBrowser(input: CreateStorageBrowserInput): {
- StorageBrowser: StorageBrowserComponent<
+ StorageBrowser: StorageBrowserType<
keyof Omit<
typeof defaultActionConfigs,
'listLocationItems' | 'listLocations'
@@ -95,15 +48,16 @@ export function createStorageBrowser(input: CreateStorageBrowserInput): {
region,
} = input.config;
- // will be replaced, contains the v0 actions API approach
- const TempActionsProvider = createTempActionsProvider({
- ...input,
- actions: locationActionsDefault,
- });
-
const ConfigurationProvider = createConfigurationProvider({
accountId,
- actions: defaultActionConfigs,
+ actions: {
+ ...defaultActionConfigs,
+ // @ts-expect-error To be addressed with line 40
+ listLocations: {
+ componentName: 'LocationsView',
+ handler: input.config.listLocations,
+ },
+ },
customEndpoint,
displayName: 'ConfigurationProvider',
getLocationCredentials,
@@ -112,6 +66,7 @@ export function createStorageBrowser(input: CreateStorageBrowserInput): {
});
const composables = { ...DEFAULT_COMPOSABLES, ...input.components };
+ const elements = { ...elementsDefault, ...input.elements };
/**
* Provides state, configuration and action values that are shared between
@@ -122,21 +77,16 @@ export function createStorageBrowser(input: CreateStorageBrowserInput): {
-
-
- {children}
-
-
+
+ {children}
+
);
}
- const StorageBrowser: StorageBrowserComponent = ({ views, displayText }) => (
+ const StorageBrowser: StorageBrowserType = ({ views, displayText }) => (
@@ -150,6 +100,11 @@ export function createStorageBrowser(input: CreateStorageBrowserInput): {
StorageBrowser.LocationDetailView = LocationDetailView;
StorageBrowser.LocationsView = LocationsView;
+ StorageBrowser.CopyView = CopyView;
+ StorageBrowser.CreateFolderView = CreateFolderView;
+ StorageBrowser.DeleteView = DeleteView;
+ StorageBrowser.UploadView = UploadView;
+
StorageBrowser.Provider = Provider;
StorageBrowser.displayName = 'StorageBrowser';
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowser.test-d.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowser.test-d.ts
deleted file mode 100644
index fd663f65c22..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowser.test-d.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-import { createStorageBrowser } from '..';
-import { DefaultActionConfigs } from '../../actions/configs';
-import {
- ListLocationItemsActionViewSubComponents,
- ListLocationsActionViewSubComponents,
- TaskActionViewComponent,
- ViewComponent,
-} from '../types';
-
-type Expect = T;
-type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y
- ? 1
- : 2
- ? true
- : false;
-
-describe('createStorageBrowser() type generation', () => {
- test('generate correct StorageBrowser type without any custom actions', () => {
- const { StorageBrowser } = createStorageBrowser({
- config: {} as any,
- });
-
- type _ = Expect<
- typeof StorageBrowser extends {
- (props: Record): React.JSX.Element;
- displayName: string;
- Provider: (props: { children?: React.ReactNode }) => React.JSX.Element;
- readonly LocationDetailView: ViewComponent<
- ListLocationItemsActionViewSubComponents,
- {}
- >;
- readonly LocationsView: ViewComponent<
- ListLocationsActionViewSubComponents,
- {}
- >;
- readonly CreateFolderView: TaskActionViewComponent<{}>;
- readonly UploadView: TaskActionViewComponent<{}>;
- }
- ? true
- : false
- >;
-
- expect('done').toBe('done');
- });
-
- test('generate correct StorageBrowser type with custom actions', () => {
- const { StorageBrowser } = createStorageBrowser({
- config: {} as any,
- actions: {
- Share: {
- componentName: 'MyShareView',
- handler: () => {},
- isCancelable: false,
- displayName: 'Share',
- includeProgress: false,
- },
- CropAll: {
- componentName: 'CropAllImagesView',
- handler: () => {},
- isCancelable: false,
- displayName: 'Crop All',
- includeProgress: false,
- },
- },
- });
-
- type _ = Expect<
- typeof StorageBrowser extends {
- (props: Record): React.JSX.Element;
- displayName: string;
- Provider: (props: { children?: React.ReactNode }) => React.JSX.Element;
- readonly MyShareView: TaskActionViewComponent<{}>;
- readonly CropAllImagesView: TaskActionViewComponent<{}>;
- readonly LocationDetailView: ViewComponent<
- ListLocationItemsActionViewSubComponents,
- {}
- >;
- readonly LocationsView: ViewComponent<
- ListLocationsActionViewSubComponents,
- {}
- >;
- readonly CreateFolderView: TaskActionViewComponent<{}>;
- readonly UploadView: TaskActionViewComponent<{}>;
- }
- ? true
- : false
- >;
-
- expect('done').toBe('done');
- });
-
- test('generate correct StorageBrowser type with custom actions and overriding actions', () => {
- const { StorageBrowser } = createStorageBrowser({
- config: {} as any,
- actions: {
- CreateFolder: {
- componentName: 'CreateFolderView',
- handler: () => {
- throw new Error('Not implemented for testing');
- },
- isCancelable: false,
- displayName: 'Create Folder',
- },
- Share: {
- componentName: 'MyShareView',
- handler: () => {},
- isCancelable: false,
- displayName: 'Share',
- },
- someOtherAction: {
- componentName: 'SomeOtherView',
- handler: () => {},
- isCancelable: false,
- displayName: 'Some Other Action',
- },
- },
- });
-
- type _ = Expect<
- typeof StorageBrowser extends {
- (props: Record): React.JSX.Element;
- displayName: string;
- Provider: (props: { children?: React.ReactNode }) => React.JSX.Element;
- readonly MyShareView: TaskActionViewComponent<{}>;
- readonly SomeOtherView: TaskActionViewComponent<{}>;
- readonly CreateFolderView: TaskActionViewComponent<{}>;
- readonly LocationDetailView: ViewComponent<
- ListLocationItemsActionViewSubComponents,
- {}
- >;
- readonly LocationsView: ViewComponent<
- ListLocationsActionViewSubComponents,
- {}
- >;
- readonly UploadView: TaskActionViewComponent<{}>;
- }
- ? true
- : false
- >;
-
- expect('done').toBe('done');
- });
-
- test('cannot override certain fields while specifying default action overrides', () => {
- createStorageBrowser({
- config: {} as any,
- actions: {
- CreateFolder: {
- // @ts-expect-error doesn't allow different componentName
- componentName: 'SomeOtherComponentName',
- handler: () => {
- throw new Error('Not implemented for testing');
- },
- displayName: 'Create Folder',
- },
- Upload: {
- // @ts-expect-error doesn't allow different componentName
- componentName: 'SomeOtherComponentName',
- handler: () => {
- throw new Error('Not implemented for testing');
- },
- displayName: 'Upload',
- },
- ListLocationItems: {
- // @ts-expect-error doesn't allow different componentName
- componentName: 'SomeOtherComponentName',
- handler: () => {
- throw new Error('Not implemented for testing');
- },
- // @ts-expect-error doesn't allow different displayName
- displayName: 'LocationItems',
- },
- ListLocations: {
- // @ts-expect-error doesn't allow different componentName
- componentName: 'SomeOtherComponentName',
- handler: () => {
- throw new Error('Not implemented for testing');
- },
- displayName: 'Locations',
- },
- },
- });
-
- expect('done').toBe('done');
- });
-
- test('should ignore custom actions with non-capitalized key', () => {
- const { StorageBrowser } = createStorageBrowser({
- config: {} as any,
- actions: {
- nonCapitalizedKey: {
- // @ts-expect-error cannot specify non-defined property
- randomValues: 'random',
- },
- },
- });
-
- type _ = Expect<
- typeof StorageBrowser extends {
- (props: Record): React.JSX.Element;
- displayName: string;
- Provider: (props: { children?: React.ReactNode }) => React.JSX.Element;
- readonly LocationDetailView: ViewComponent<
- ListLocationItemsActionViewSubComponents,
- {}
- >;
- readonly LocationsView: ViewComponent<
- ListLocationsActionViewSubComponents,
- {}
- >;
- readonly CreateFolderView: TaskActionViewComponent<{}>;
- readonly UploadView: TaskActionViewComponent<{}>;
- }
- ? true
- : false
- >;
-
- expect('done').toBe('done');
- });
-
- test('generate correct type for created `useAction`', () => {
- const { useAction } = createStorageBrowser({
- config: {} as any,
- actions: {
- Share: {
- componentName: 'MyShareView',
- handler: () => {},
- isCancelable: false,
- displayName: 'Share',
- includeProgress: false,
- },
- },
- });
-
- type ParamType = Parameters[0];
- type _ = Expect>;
-
- expect('done').toBe('done');
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowser.test.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowser.test.ts
deleted file mode 100644
index df15df33c5c..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowser.test.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { expectTypeTestsToPassAsync } from 'jest-tsd';
-
-// evaluates type defs in corresponding test-d.ts file
-it('should not produce static type errors', async () => {
- await expectTypeTestsToPassAsync(__filename);
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowserReact.test-d.tsx b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowserReact.test-d.tsx
deleted file mode 100644
index bb943b573df..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowserReact.test-d.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react';
-import { createStorageBrowser } from '../createStorageBrowser';
-import {
- ListLocationItemsHandlerOutput,
- TaskHandler,
- TaskHandlerInput,
- TaskHandlerOutput,
- UploadHandler,
- createFolderHandler,
- listLocationItemsHandler,
-} from '../../actions';
-
-type MyHandler = TaskHandler;
-const myHandler = null as unknown as MyHandler;
-
-describe('createStorageBrowser() created React components type generation', () => {
- const { StorageBrowser } = createStorageBrowser({
- actions: {
- MyAction: {
- displayName: 'Custom Name',
- isCancelable: true,
- handler: myHandler,
- componentName: 'MyComponentView',
- },
- SingleTaskAction: {
- handler: createFolderHandler,
- displayName: 'Create Folder',
- componentName: 'SingleTaskActionView',
- },
- ListLocationItems: {
- handler: (_input) =>
- null as unknown as Promise,
- componentName: 'LocationDetailView',
- displayName: () => '',
- },
- Upload: {
- handler: null as unknown as UploadHandler,
- componentName: 'UploadView',
- displayName: 'Upload',
- isCancelable: true,
- },
- whatever: {
- // `ListHandler` actions should be disallowed unless
- // they match the default componentName, that they are
- // directly related to, e.g. "ListLocationItems" -> listLocationItemsHandler
- handler: listLocationItemsHandler,
- componentName: 'SillyView',
- displayName: 'Silly',
- isCancelable: true,
- },
- },
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- config: {} as any,
- });
-
- it('can render default action views', () => {
- function _Container() {
- return (
- <>
-
-
-
-
-
-
-
- >
- );
- }
-
- expect('done').toBe('done');
- });
-
- it('can render custom action views', () => {
- function _Container() {
- return (
- <>
-
-
-
-
-
- >
- );
- }
-
- expect('done').toBe('done');
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowserReact.test.tsx b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowserReact.test.tsx
deleted file mode 100644
index df15df33c5c..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/__tests__/createStorageBrowserReact.test.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { expectTypeTestsToPassAsync } from 'jest-tsd';
-
-// evaluates type defs in corresponding test-d.ts file
-it('should not produce static type errors', async () => {
- await expectTypeTestsToPassAsync(__filename);
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.ts
deleted file mode 100644
index f6f8a1b585b..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createStorageBrowser.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { CreateStorageBrowser } from './types';
-
-export const createStorageBrowser: CreateStorageBrowser = (_) => {
- // TODO: implement this function
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return {} as any;
-};
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/index.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/index.ts
deleted file mode 100644
index 217378eec17..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { createStorageBrowser } from './createStorageBrowser';
diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts
deleted file mode 100644
index 41cd97ef872..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import {
- ActionConfigs,
- ComponentName,
- DefaultActionConfigs,
- DefaultActionKey,
-} from '../actions/configs';
-import { createUseAction } from '../actions/createUseAction';
-import { StorageBrowserElements } from '../context/elements';
-import { GetLocationCredentials } from '../credentials/types';
-import { RegisterAuthListener } from '../providers';
-import { ListLocations } from '../storage-internal';
-
-interface Config {
- accountId?: string;
- getLocationCredentials: GetLocationCredentials;
- listLocations: ListLocations;
- registerAuthListener: RegisterAuthListener;
- region: string;
-}
-
-interface CreateStorageBrowserInput {
- config: Config;
- elements?: Partial;
- actions?: T;
-}
-
-interface CreateStorageBrowserOutput {
- StorageBrowser: {
- (props: Record): React.JSX.Element;
- displayName: string;
- Provider: (props: { children?: React.ReactNode }) => React.JSX.Element;
- } & DerivedViews; // & the action derived views components
- useAction: ReturnType>;
-}
-
-export interface CreateStorageBrowser {
- <
- T extends ActionConfigs &
- Partial = Partial,
- >(
- input: CreateStorageBrowserInput
- ): CreateStorageBrowserOutput>;
-}
-
-interface LocationActionViewProps {
- type?: T;
-}
-
-type LocationActionViewComponent = (
- props: LocationActionViewProps
-) => React.JSX.Element;
-
-// Custom actions derived views
-type CustomActionViews = {
- readonly [K in keyof T as K extends DefaultActionKey
- ? never
- : T[K] extends { componentName: ComponentName }
- ? T[K]['componentName']
- : never]: TaskActionViewComponent;
-};
-
-export type ViewComponent = {
- (props: K): React.JSX.Element;
- displayName: string;
- Provider: (props: { children?: React.ReactNode }) => React.JSX.Element;
-} & T;
-
-/**
- * task action view component & sub-components interface
- */
-export type TaskActionViewComponent = ViewComponent<
- TaskActionViewSubComponents & T
->;
-
-interface DefaultActionViews {
- ListLocationItems: {
- componentName: 'LocationDetailView';
- subComponents: ViewComponent;
- };
- ListLocations: {
- componentName: 'LocationsView';
- subComponents: ViewComponent;
- };
- Upload: {
- componentName: 'UploadView';
- // TODO: pass in the generic parameter
- subComponents: TaskActionViewComponent;
- };
- // temp: needs full subcomp defintions
- CreateFolder: {
- componentName: 'CreateFolderView';
- subComponents: TaskActionViewComponent;
- };
-}
-
-/**
- * Create derived views from both custom actions and default actions.
- *
- * One can override default actions, but the view interface of the default actions
- * remain the same.
- */
-type DerivedViews = CustomActionViews & {
- readonly [K in keyof T as K extends keyof DefaultActionViews
- ? DefaultActionViews[K]['componentName']
- : never]: K extends keyof DefaultActionViews
- ? DefaultActionViews[K]['subComponents']
- : never;
-} & {
- readonly LocationActionView: LocationActionViewComponent<
- // exclude list view actions
- Exclude
- >;
-};
-
-interface DefaultViewSubComponentProps {
- className?: string;
-}
-
-export interface TaskActionViewSubComponents {
- Title: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Trigger: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Cancel: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Table: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- StatusDisplay: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Destination: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Exit: (props: DefaultViewSubComponentProps) => React.JSX.Element;
-}
-
-export interface ListLocationsActionViewSubComponents {
- Title: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Table: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Search: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Refresh: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Paginate: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Message: (props: DefaultViewSubComponentProps) => React.JSX.Element;
-}
-
-export interface ListLocationItemsActionViewSubComponents
- extends ListLocationsActionViewSubComponents {
- ActionList: (props: DefaultViewSubComponentProps) => React.JSX.Element;
- Navigate: (props: DefaultViewSubComponentProps) => React.JSX.Element;
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/copyView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/copyView.spec.ts.snap
index e1808ddba16..95b05aff3e5 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/copyView.spec.ts.snap
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/copyView.spec.ts.snap
@@ -82,11 +82,10 @@ exports[`CopyView display text values \`getListFoldersResultsMessage\` returns t
exports[`CopyView display text values should match snapshot values 1`] = `
{
"actionCancelLabel": "Cancel",
- "actionDestinationLabel": "Copy destination:",
+ "actionDestinationLabel": "Copy destination",
"actionExitLabel": "Exit",
"actionStartLabel": "Copy",
"getActionCompleteMessage": [Function],
- "getFolderSelectedMessage": [Function],
"getListFoldersResultsMessage": [Function],
"loadingIndicatorLabel": "Loading",
"overwriteWarningMessage": "Copied files will overwrite existing files at selected destination.",
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationDetailView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationDetailView.spec.ts.snap
index ea0dd403eeb..03dda25d017 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationDetailView.spec.ts.snap
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationDetailView.spec.ts.snap
@@ -21,6 +21,8 @@ exports[`LocationDetailView display text \`getListResultsMessage\` returns the e
}
`;
+exports[`LocationDetailView display text \`getListResultsMessage\` returns the expected values in the loading scenario 1`] = `undefined`;
+
exports[`LocationDetailView display text \`getListResultsMessage\` returns the expected values in the search exhausted scenario 1`] = `
{
"content": "Showing results for up to the first 10,000 items.",
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationsView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationsView.spec.ts.snap
index d9f8f66e235..adceaffcfa3 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationsView.spec.ts.snap
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/locationsView.spec.ts.snap
@@ -21,6 +21,8 @@ exports[`LocationsView display text \`getListLocationsResultMessage\` returns th
}
`;
+exports[`LocationsView display text \`getListLocationsResultMessage\` returns the expected values in the loading scenario 1`] = `undefined`;
+
exports[`LocationsView display text \`getListLocationsResultMessage\` returns the expected values in the search failed scenario 1`] = `
{
"content": "Network got confused",
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap
index fc7ba3645aa..272f67f6885 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/__snapshots__/uploadView.spec.ts.snap
@@ -1,55 +1,122 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the all failed scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the all failed scenario 1`] = `
{
"content": "All files failed to upload.",
"type": "error",
}
`;
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the all prevented overwrites scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the all overwrite prevented or failed scenario 1`] = `
+{
+ "content": "Overwrite prevented for 6 files, 5 files failed to upload.",
+ "type": "error",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the all overwrite prevented scenario 1`] = `
+{
+ "content": "Overwrite prevented for all files.",
+ "type": "warning",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the all prevented overwrites scenario 1`] = `
{
"content": "Overwrite prevented for all files.",
"type": "warning",
}
`;
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the all success scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the all success scenario 1`] = `
{
"content": "All files uploaded.",
"type": "success",
}
`;
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the no failures, some prevented overwrites, some success scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the multiple overwrite prevented scenario 1`] = `
+{
+ "content": "Overwrite prevented for 3 files, 8 files uploaded.",
+ "type": "warning",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the multiple overwrite prevented with a failure scenario 1`] = `
+{
+ "content": "Overwrite prevented for 3 files, 1 file failed to upload, 7 files uploaded.",
+ "type": "error",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the multiple overwrite prevented with failures scenario 1`] = `
+{
+ "content": "Overwrite prevented for 3 files, 3 files failed to upload, 5 files uploaded.",
+ "type": "error",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the no failures, some prevented overwrites, some success scenario 1`] = `
{
"content": "Overwrite prevented for 3 files, 2 files uploaded.",
"type": "warning",
}
`;
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the some failed scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the single overwrite prevented scenario 1`] = `
+{
+ "content": "Overwrite prevented for 1 file, 10 files uploaded.",
+ "type": "warning",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the single overwrite prevented with a failure scenario 1`] = `
+{
+ "content": "Overwrite prevented for 1 file, 1 file failed to upload, 9 files uploaded.",
+ "type": "error",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the single overwrite prevented with failures scenario 1`] = `
+{
+ "content": "Overwrite prevented for 1 file, 3 files failed to upload, 7 files uploaded.",
+ "type": "error",
+}
+`;
+
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the some failed scenario 1`] = `
{
"content": "3 files failed to upload, 8 files uploaded.",
"type": "error",
}
`;
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, no success scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, no success scenario 1`] = `
{
"content": "Overwrite prevented for 3 files, 2 files failed to upload.",
"type": "error",
}
`;
-exports[`CopyView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, some success scenario 1`] = `
+exports[`UploadView display text values \`getActionCompleteMessage\` returns the expected values in the some failures, some prevented overwrites, some success scenario 1`] = `
{
"content": "Overwrite prevented for 3 files, 2 files failed to upload, 8 files uploaded.",
"type": "error",
}
`;
-exports[`CopyView display text values should match snapshot values 1`] = `
+exports[`UploadView display text values \`getFilesValidationMessage\` returns expected values in the empty file items scenario 1`] = `undefined`;
+
+exports[`UploadView display text values \`getFilesValidationMessage\` returns expected values in the no files scenario 1`] = `undefined`;
+
+exports[`UploadView display text values \`getFilesValidationMessage\` returns expected values in the too large file scenario 1`] = `
+{
+ "content": "Files larger than 160GB cannot be added to the upload queue: file1",
+ "type": "warning",
+}
+`;
+
+exports[`UploadView display text values should match snapshot values 1`] = `
{
"actionCancelLabel": "Cancel",
"actionDestinationLabel": "Destination",
@@ -58,6 +125,7 @@ exports[`CopyView display text values should match snapshot values 1`] = `
"addFilesLabel": "Add files",
"addFolderLabel": "Add folder",
"getActionCompleteMessage": [Function],
+ "getFilesValidationMessage": [Function],
"loadingIndicatorLabel": "Loading",
"overwriteToggleLabel": "Overwrite existing files",
"statusDisplayCanceledLabel": "Canceled",
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/scenarios.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/scenarios.ts
index 89567e9cb04..9d0c8869794 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/scenarios.ts
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/scenarios.ts
@@ -3,7 +3,9 @@ import {
LocationData,
LocationItemData,
} from '../../../../actions';
+import { FileItems } from '../../../../providers';
import { INITIAL_STATUS_COUNTS, StatusCounts } from '../../../../tasks';
+import { UPLOAD_FILE_SIZE_LIMIT } from '../../../../validators/isFileTooBig';
export const ACTION_SCENARIOS: [string, StatusCounts][] = [
['all failed', { ...INITIAL_STATUS_COUNTS, FAILED: 11, TOTAL: 11 }],
@@ -60,6 +62,31 @@ export const CREATE_FOLDER_ACTION_SCENARIOS: [string, StatusCounts][] = [
['success', { ...INITIAL_STATUS_COUNTS, COMPLETE: 1, TOTAL: 1 }],
];
+export const UPLOAD_FILES_VALIDATION_SCENARIOS: [
+ string,
+ FileItems | undefined,
+][] = [
+ ['no files', undefined],
+ ['empty file items', []],
+ [
+ 'too large file',
+ [
+ {
+ // @ts-expect-error: mock file
+ file: { name: 'file1', size: UPLOAD_FILE_SIZE_LIMIT + 1 },
+ key: 'file1',
+ id: 'file1-id',
+ },
+ {
+ // @ts-expect-error: mock file
+ file: { name: 'file2', size: UPLOAD_FILE_SIZE_LIMIT },
+ key: 'file2',
+ id: 'file2-id',
+ },
+ ],
+ ],
+];
+
export const UPLOAD_ACTION_SCENARIOS: [string, StatusCounts][] = [
...ACTION_SCENARIOS,
[
@@ -183,6 +210,8 @@ export const LIST_LOCATIONS_SCENARIOS: [
query?: string;
hasError?: boolean;
message?: string;
+ isLoading?: boolean;
+ hasExhaustedSearch?: boolean;
},
][] = [
['empty results', { locations: [] }],
@@ -215,6 +244,14 @@ export const LIST_LOCATIONS_SCENARIOS: [
hasExhaustedSearch: true,
},
],
+ [
+ 'loading',
+ {
+ locations: [],
+ isLoading: true,
+ hasExhaustedSearch: false,
+ },
+ ],
];
export const LIST_ITEMS_SCENARIOS: [
@@ -224,6 +261,7 @@ export const LIST_ITEMS_SCENARIOS: [
query?: string;
hasError?: boolean;
message?: string;
+ isLoading?: boolean;
hasExhaustedSearch?: boolean;
},
][] = [
@@ -265,4 +303,12 @@ export const LIST_ITEMS_SCENARIOS: [
hasExhaustedSearch: true,
},
],
+ [
+ 'loading',
+ {
+ items: [],
+ isLoading: true,
+ hasExhaustedSearch: false,
+ },
+ ],
];
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/uploadView.spec.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/uploadView.spec.ts
index c85699946ba..52cc1e394f8 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/uploadView.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/__tests__/uploadView.spec.ts
@@ -1,12 +1,15 @@
-import { ACTION_SCENARIOS } from './scenarios';
+import {
+ UPLOAD_ACTION_SCENARIOS,
+ UPLOAD_FILES_VALIDATION_SCENARIOS,
+} from './scenarios';
import { DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT } from '../uploadView';
-describe('CopyView display text values', () => {
+describe('UploadView display text values', () => {
it('should match snapshot values', () => {
expect(DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT).toMatchSnapshot();
});
- it.each(ACTION_SCENARIOS)(
+ it.each(UPLOAD_ACTION_SCENARIOS)(
'`getActionCompleteMessage` returns the expected values in the %s scenario',
(_, counts) => {
const { getActionCompleteMessage } = DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT;
@@ -14,4 +17,13 @@ describe('CopyView display text values', () => {
expect(getActionCompleteMessage({ counts })).toMatchSnapshot();
}
);
+
+ it.each(UPLOAD_FILES_VALIDATION_SCENARIOS)(
+ '`getFilesValidationMessage` returns expected values in the %s scenario',
+ (_, invalidFiles) => {
+ const { getFilesValidationMessage } = DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT;
+
+ expect(getFilesValidationMessage({ invalidFiles })).toMatchSnapshot();
+ }
+ );
});
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/copyView.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/copyView.ts
index 71f85006e34..69ec21e5c00 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/copyView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/copyView.ts
@@ -5,7 +5,7 @@ export const DEFAULT_COPY_VIEW_DISPLAY_TEXT: DefaultCopyViewDisplayText = {
...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
title: 'Copy',
actionStartLabel: 'Copy',
- actionDestinationLabel: 'Copy destination:',
+ actionDestinationLabel: 'Copy destination',
getListFoldersResultsMessage: ({ folders, query, message, hasError }) => {
if (!folders?.length) {
return {
@@ -48,9 +48,6 @@ export const DEFAULT_COPY_VIEW_DISPLAY_TEXT: DefaultCopyViewDisplayText = {
type: 'error',
};
},
- getFolderSelectedMessage: (key: string) => {
- return `Current folder selected: ${key}. There are no additional folders under this path.`;
- },
searchSubmitLabel: 'Submit',
searchClearLabel: 'Clear search',
};
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 5352eff1156..e760476c471 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
@@ -12,8 +12,13 @@ export const DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT: DefaultLocationDetailVie
hasExhaustedSearch,
hasError = false,
message,
+ isLoading,
} = data ?? {};
+ if (isLoading) {
+ return undefined;
+ }
+
if (hasError) {
return {
type: 'error',
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationsView.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationsView.ts
index 7901170c105..57d8ba3068d 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationsView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/locationsView.ts
@@ -11,6 +11,7 @@ export const DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT: DefaultLocationsViewDisplayTex
searchPlaceholder: 'Filter folders and files',
getListLocationsResultMessage: (data) => {
const {
+ isLoading,
locations,
query,
hasExhaustedSearch,
@@ -18,6 +19,10 @@ export const DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT: DefaultLocationsViewDisplayTex
message,
} = data ?? {};
+ if (isLoading) {
+ return undefined;
+ }
+
if (hasError) {
return {
type: 'error',
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts
index 639e22003c3..54051daa1b1 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/libraries/en/uploadView.ts
@@ -1,5 +1,6 @@
import { DEFAULT_ACTION_VIEW_DISPLAY_TEXT } from './shared';
import { DefaultUploadViewDisplayText } from '../../types';
+import { isFileTooBig } from '../../../validators';
export const DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT: DefaultUploadViewDisplayText = {
...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
@@ -79,6 +80,22 @@ export const DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT: DefaultUploadViewDisplayText = {
return { content: 'All files uploaded.', type };
},
+ getFilesValidationMessage: (data) => {
+ if (!data?.invalidFiles) {
+ return undefined;
+ }
+ const tooBigFileNames = data.invalidFiles
+ .filter(({ file }) => isFileTooBig(file))
+ .map(({ file }) => file.name)
+ .join(', ');
+ if (tooBigFileNames) {
+ return {
+ content: `Files larger than 160GB cannot be added to the upload queue: ${tooBigFileNames}`,
+ type: 'warning',
+ };
+ }
+ return undefined;
+ },
statusDisplayOverwritePreventedLabel: 'Overwrite prevented',
overwriteToggleLabel: 'Overwrite existing files',
};
diff --git a/packages/react-storage/src/components/StorageBrowser/displayText/types.ts b/packages/react-storage/src/components/StorageBrowser/displayText/types.ts
index 2edd9de2abf..5fafa744150 100644
--- a/packages/react-storage/src/components/StorageBrowser/displayText/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/displayText/types.ts
@@ -1,6 +1,7 @@
import { StatusCounts, Tasks } from '../tasks';
import {
CopyHandlerData,
+ CreateFolderHandlerData,
DeleteHandlerData,
FolderData,
LocationData,
@@ -11,7 +12,7 @@ import {
} from '../actions';
import { LocationState } from '../providers/store/location';
import { MessageType } from '../composables/Message';
-import { CreateFolderHandlerData } from '../actions';
+import { FileItems } from '../providers';
/**
* Common list view display text values
@@ -28,6 +29,7 @@ interface ListMessageData {
message?: string;
hasExhaustedSearch?: boolean;
query?: string;
+ isLoading?: boolean;
}
interface ListLocationsMessageData extends ListMessageData {
@@ -120,7 +122,6 @@ export interface DefaultCopyViewDisplayText
getListFoldersResultsMessage: (
data: ListFoldersMessageData
) => { content?: string; type?: MessageType } | undefined;
- getFolderSelectedMessage: (path: string) => string;
loadingIndicatorLabel: 'Loading';
overwriteWarningMessage: string;
searchPlaceholder: string;
@@ -137,6 +138,9 @@ export interface DefaultUploadViewDisplayText
addFolderLabel: string;
statusDisplayOverwritePreventedLabel: string;
overwriteToggleLabel: string;
+ getFilesValidationMessage: (data?: {
+ invalidFiles?: FileItems;
+ }) => { content?: string; type?: MessageType } | undefined;
}
export interface DefaultStorageBrowserDisplayText {
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/createUseActionStateContext.spec.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/createUseActionStateContext.spec.tsx
deleted file mode 100644
index 6b025b2976a..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/createUseActionStateContext.spec.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React from 'react';
-import { act, renderHook } from '@testing-library/react-hooks';
-
-import { createActionStateContext } from '../createActionStateContext';
-import { AsyncDataAction } from '@aws-amplify/ui-react-core';
-
-const actionOne: AsyncDataAction = jest.fn(
- (
- _: { value: string | undefined },
- { inputValue }: { inputValue: string }
- ): Promise<{ value: string | undefined }> =>
- Promise.resolve({ value: inputValue })
-);
-
-const actionTwo: AsyncDataAction = jest.fn(
- (
- _: { anotherValue: boolean },
- { inputValue }: { inputValue: boolean }
- ): Promise<{ anotherValue: boolean }> =>
- Promise.resolve({ anotherValue: inputValue })
-);
-
-const actions = { actionOne, actionTwo };
-const initialValue = {
- actionOne: { value: undefined },
- actionTwo: { anotherValue: false },
-};
-
-describe('createActionStateContext', () => {
- it('creates a composed `Provider`', async () => {
- const [Provider, useAction] = createActionStateContext(
- actions,
- 'context error'
- );
- const Wrapper = (props: { children?: React.ReactNode }) => (
-
- );
-
- const { result, waitForNextUpdate } = renderHook(
- () => useAction({ type: 'actionOne' }),
- {
- wrapper: Wrapper,
- }
- );
-
- const [initialState, handleAction] = result.current;
- expect(initialState.isLoading).toBe(false);
- expect(initialState.hasError).toBe(false);
- expect(initialState.message).toBeUndefined();
- expect(initialState.data).toBe(initialValue['actionOne']);
-
- act(() => {
- handleAction({ inputValue: 'new value' });
- });
-
- const [loadingState] = result.current;
- expect(loadingState.isLoading).toBe(true);
- expect(loadingState.hasError).toBe(false);
- expect(loadingState.message).toBeUndefined();
- expect(loadingState.data).toBe(initialValue['actionOne']);
-
- await waitForNextUpdate();
-
- const [doneState] = result.current;
- expect(doneState.isLoading).toBe(false);
- expect(doneState.hasError).toBe(false);
- expect(doneState.message).toBeUndefined();
- expect(doneState.data).toStrictEqual({ value: 'new value' });
- });
-
- it('`useAction` throws when used outside a `Provider`', () => {
- const [, useAction] = createActionStateContext(actions, 'context error');
-
- const { result } = renderHook(() => useAction({ type: 'actionOne' }));
-
- expect(result.error?.message).toBe('context error');
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/downloadAction.spec.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/downloadAction.spec.tsx
deleted file mode 100644
index ab3d76cada0..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/downloadAction.spec.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as StorageModule from '../../../storage-internal';
-import { downloadAction } from '../downloadAction';
-
-const getUrlSpy = jest.spyOn(StorageModule, 'getUrl');
-const config = {
- accountId: '012345678901',
- bucket: 'bucket',
- credentialsProvider: jest.fn(),
- region: 'region',
-};
-const initialValue = { signedUrl: '' };
-
-describe('downloadAction', () => {
- beforeEach(() => {
- getUrlSpy.mockClear();
- });
-
- it('returns the expected output in the happy path', async () => {
- getUrlSpy.mockResolvedValueOnce({
- url: new URL('https://docs.amplify.aws/'),
- expiresAt: new Date(),
- });
-
- const { signedUrl } = await downloadAction(initialValue, {
- config,
- key: 'a_prefix',
- });
-
- expect(signedUrl).toEqual('https://docs.amplify.aws/');
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/listLocationItemsAction.spec.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/listLocationItemsAction.spec.ts
deleted file mode 100644
index 8d4243cbb4f..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/listLocationItemsAction.spec.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import * as StorageModule from '../../../storage-internal';
-import {
- listLocationItemsAction,
- parseResult,
-} from '../listLocationItemsAction';
-
-let uuid = 0;
-Object.defineProperty(globalThis, 'crypto', {
- value: {
- randomUUID: () => {
- uuid++;
- return uuid.toString();
- },
- },
-});
-
-const listSpy = jest.spyOn(StorageModule, 'list');
-const config = {
- bucket: 'bucket',
- credentialsProvider: jest.fn(),
- region: 'region',
-};
-
-const options = { delimiter: '/' };
-const prefix = 'a_prefix/';
-const initialValue = { nextToken: undefined, result: [] };
-
-const generateMockItems = (size: number): StorageModule.ListOutput['items'] => {
- return Array.apply(0, new Array(size)).map((_, index) => ({
- path: `${prefix}key${index}`,
- lastModified: new Date(),
- size: 1,
- }));
-};
-
-const generateMockSubpaths = (
- size: number
-): StorageModule.ListOutput['excludedSubpaths'] =>
- Array.apply(0, new Array(size)).map((_, index) => {
- return `subpath${index}`;
- });
-
-describe('listLocationItemsAction', () => {
- beforeEach(() => {
- listSpy.mockClear();
- });
-
- it('returns the expected output shape in the happy path', async () => {
- listSpy.mockResolvedValueOnce({ items: [], nextToken: 'tokeno' });
-
- const { result, nextToken } = await listLocationItemsAction(initialValue, {
- config,
- prefix,
- });
-
- expect(result).toHaveLength(0);
- expect(nextToken).toBeDefined();
- });
-
- it('merges the current action result with the previous action result', async () => {
- listSpy
- .mockResolvedValueOnce({
- items: generateMockItems(100),
- excludedSubpaths: generateMockSubpaths(10),
- nextToken: 'first',
- })
- .mockResolvedValueOnce({
- items: generateMockItems(100),
- excludedSubpaths: generateMockSubpaths(10),
- nextToken: 'second',
- });
-
- const { result, nextToken } = await listLocationItemsAction(initialValue, {
- config,
- options,
- prefix: 'a_prefix',
- });
-
- expect(result).toHaveLength(110);
- expect(nextToken).toBeDefined();
-
- const { result: nextResult, nextToken: nextNextToken } =
- await listLocationItemsAction(
- { nextToken, result },
- { config, options, prefix: 'a_prefix' }
- );
-
- expect(nextResult).toHaveLength(220);
- expect(nextNextToken).not.toBe(nextToken);
- expect(nextToken).toBeDefined();
- });
-
- it('provides expected `pageSize` to `list` on `refresh`', async () => {
- listSpy.mockResolvedValueOnce({ items: [] });
-
- const input = {
- config,
- options: { refresh: true, pageSize: 10 },
- prefix: 'a_prefix',
- };
-
- await listLocationItemsAction(initialValue, input);
-
- expect(listSpy).toHaveBeenCalledTimes(1);
- expect(listSpy).toHaveBeenCalledWith({
- path: input.prefix,
- options: {
- bucket: {
- bucketName: input.config.bucket,
- region: input.config.region,
- },
- locationCredentialsProvider: input.config.credentialsProvider,
- nextToken: undefined,
- pageSize: input.options.pageSize + 1,
- subpathStrategy: { delimiter: undefined, strategy: 'include' },
- },
- });
- });
-
- it('provides expected `pageSize` to `list` on initial load', async () => {
- listSpy.mockResolvedValueOnce({ items: [] });
-
- const input = { config, options: { pageSize: 10 }, prefix: 'a_prefix' };
-
- await listLocationItemsAction(initialValue, input);
-
- expect(listSpy).toHaveBeenCalledTimes(1);
- expect(listSpy).toHaveBeenCalledWith({
- path: input.prefix,
- options: {
- bucket: {
- bucketName: input.config.bucket,
- region: input.config.region,
- },
- locationCredentialsProvider: input.config.credentialsProvider,
- nextToken: undefined,
- pageSize: input.options.pageSize + 1,
- subpathStrategy: { delimiter: undefined, strategy: 'include' },
- },
- });
- });
-
- it.todo('handles a search action as expected');
- it.todo('handles a paginate action as expected');
-});
-
-describe('parseResult', () => {
- it('outputs correct list with items: prefix, zero byte folder, object and excludedSubpaths', () => {
- const output = {
- items: [
- // Current prefix
- { path: prefix, lastModified: new Date(), size: 0 },
- // Zero byte subfolder:
- { path: `${prefix}Banana/`, lastModified: new Date(), size: 0 },
- // Image file:
- { path: `${prefix}Orange.jpg`, lastModified: new Date(), size: 56984 },
- ],
- // subfolder with objects in it
- excludedSubpaths: [`${prefix}Cloudberry/`],
- };
- const result = parseResult(output, prefix);
- expect(result).toHaveLength(3); // excludes prefix
- const subFolderWithObject = result[0];
- expect(subFolderWithObject.key).toBe(`${prefix}Cloudberry/`);
- expect(subFolderWithObject.type).toBe('FOLDER');
- const zeroByteSubFolder = result[1];
- expect(zeroByteSubFolder.key).toBe(`${prefix}Banana/`);
- expect(zeroByteSubFolder.type).toBe('FOLDER');
- const file = result[2];
- expect(file.key).toBe(`${prefix}Orange.jpg`);
- expect(file.type).toBe('FILE');
- });
-
- it('should return empty array for empty zero byte folder', () => {
- // empty folders will just show the current prefix as the path
- const output = {
- items: [{ path: prefix, lastModified: new Date(), size: 0 }],
- };
- const result = parseResult(output, prefix);
- expect(result).toHaveLength(0);
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/listLocationsAction.spec.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/listLocationsAction.spec.ts
deleted file mode 100644
index 6829991daf7..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/__tests__/listLocationsAction.spec.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import {
- createListLocationsAction,
- parseAccessGrantLocation,
-} from '../listLocationsAction';
-import { ListLocations, LocationAccess } from '../../../adapters/types';
-import { generateCombinations } from '../../../actions/__testUtils__/permissions';
-import { LocationPermissions } from '../../../actions';
-
-Object.defineProperty(globalThis, 'crypto', {
- value: {
- randomUUID: () => 'identifier!',
- },
-});
-
-const fakeLocation: LocationAccess = {
- scope: 's3://some-bucket/*',
- permissions: ['list'],
- type: 'BUCKET',
-};
-
-const getFakeLocation = (
- permissions: LocationAccess['permissions'] = ['list'],
- type: LocationAccess['type'] = 'BUCKET'
-) => {
- if (type === 'PREFIX') {
- return {
- ...fakeLocation,
- scope: 's3://some-bucket/prefix1/*',
- permissions,
- type,
- };
- } else if (type === 'OBJECT') {
- return {
- ...fakeLocation,
- scope: 's3://some-bucket/my.pdf',
- permissions,
- type,
- };
- }
-
- return { ...fakeLocation, permissions, type };
-};
-
-const generateMockLocations = (size: number) =>
- Array(size).fill(fakeLocation);
-
-const listLocations: ListLocations = ({ pageSize } = {}) => {
- return Promise.resolve({
- locations: generateMockLocations(pageSize!),
- nextToken: undefined,
- });
-};
-
-const mockListLocations = jest.fn(listLocations);
-
-describe('createListLocationsAction', () => {
- beforeEach(() => {
- mockListLocations.mockClear();
- });
- it('returns the expected output shape in the happy path', async () => {
- mockListLocations.mockResolvedValueOnce({
- locations: generateMockLocations(100),
- nextToken: 'next',
- });
- const listLocationsAction = createListLocationsAction(mockListLocations);
-
- const output = await listLocationsAction(
- { nextToken: undefined, result: [] },
- { options: { pageSize: 100 } }
- );
-
- expect(output.result).toHaveLength(100);
- expect(output.nextToken).toBe('next');
- });
-
- it('merges the current action result with the previous action result', async () => {
- mockListLocations
- .mockResolvedValueOnce({
- locations: generateMockLocations(100),
- nextToken: 'next',
- })
- .mockResolvedValueOnce({
- locations: generateMockLocations(100),
- nextToken: 'next-oooo',
- });
-
- const listLocationsAction = createListLocationsAction(mockListLocations);
- const { result, nextToken } = await listLocationsAction(
- { nextToken: undefined, result: [] },
- { options: { pageSize: 100 } }
- );
-
- expect(result).toHaveLength(100);
- expect(nextToken).toBe('next');
-
- const { result: nextResult, nextToken: nextNextToken } =
- await listLocationsAction(
- { result, nextToken },
- { options: { pageSize: 100 } }
- );
-
- expect(nextResult).toHaveLength(200);
- expect(nextNextToken).not.toBe(nextToken);
- expect(nextNextToken).toBe('next-oooo');
- });
-
- it('should paginate with default page limit and provide next token', async () => {
- // assume, total items: 1500; default page limit: 1000
- mockListLocations.mockResolvedValueOnce({
- locations: generateMockLocations(600),
- nextToken: 'next-1',
- });
- mockListLocations.mockResolvedValueOnce({
- locations: generateMockLocations(200),
- nextToken: 'next-2',
- });
- mockListLocations.mockResolvedValueOnce({
- locations: generateMockLocations(200),
- nextToken: 'next-3',
- });
-
- const listLocationsAction = createListLocationsAction(mockListLocations);
-
- const output = await listLocationsAction(
- { nextToken: undefined, result: [] },
- {}
- );
-
- expect(mockListLocations).toHaveBeenCalledTimes(3);
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 1000,
- nextToken: undefined,
- });
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 400,
- nextToken: 'next-1',
- });
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 200,
- nextToken: 'next-2',
- });
-
- expect(output.result).toHaveLength(1000);
- expect(output.nextToken).toBe('next-3');
- });
-
- it('should paginate with input page limit and conclude', async () => {
- // assume, total items: 70; requested page limit: 100
- mockListLocations.mockResolvedValueOnce({
- locations: generateMockLocations(50),
- nextToken: 'next',
- });
- mockListLocations.mockResolvedValueOnce({
- locations: generateMockLocations(20),
- nextToken: undefined,
- });
-
- const listLocationsAction = createListLocationsAction(mockListLocations);
-
- const output = await listLocationsAction(
- { nextToken: undefined, result: [] },
- { options: { pageSize: 100 } }
- );
-
- expect(mockListLocations).toHaveBeenCalledTimes(2);
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 100,
- nextToken: undefined,
- });
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 50,
- nextToken: 'next',
- });
-
- expect(output.result).toHaveLength(70);
- expect(output.nextToken).toBeUndefined();
- });
-
- it('should filter out all WRITE permission grants and invalid prefix grants (prefix*)', async () => {
- const invalidPrefixLocation: LocationAccess = {
- permissions: ['list', 'write'],
- scope: 's3://some-bucket/invalid-prefix*',
- type: 'PREFIX',
- };
- // Expect 9 total combinations and 0 will be filtered out.
- // 3 different combinations of ['get', 'list'] permissions * 3 different location types.
- const fakeReadLocations = generateCombinations([
- 'get',
- 'list',
- ] as LocationPermissions)
- .map((permissions) => [
- getFakeLocation(permissions, 'OBJECT'),
- getFakeLocation(permissions, 'BUCKET'),
- getFakeLocation(permissions, 'PREFIX'),
- ])
- .flat();
- // Expect 9 total combinations and 9 will be filtered out.
- // 3 different combinations of ['write', 'delete'] permissions * 3 different location types.
- const fakeWriteLocations = generateCombinations([
- 'write',
- 'delete',
- ] as LocationPermissions)
- .map((permissions) => [
- getFakeLocation(permissions, 'OBJECT'),
- getFakeLocation(permissions, 'BUCKET'),
- getFakeLocation(permissions, 'PREFIX'),
- ])
- .flat();
- // expect 27 total combinations and 0 will be filtered out.
- // 3 different combinations of ['get', 'list'] permissions * 3 different combinations of
- // ['write', 'delete'] permissions * 3 different location types.
- const fakeReadWriteLocations: LocationAccess[] = [];
- for (const fakeReadLocation of fakeReadLocations) {
- for (const fakeWriteLocation of fakeWriteLocations) {
- if (fakeWriteLocation.type === fakeReadLocation.type) {
- fakeReadWriteLocations.push({
- permissions: [
- ...fakeReadLocation.permissions,
- ...fakeWriteLocation.permissions,
- ],
- type: fakeReadLocation.type,
- scope: fakeWriteLocation.scope,
- });
- }
- }
- }
-
- mockListLocations.mockResolvedValueOnce({
- locations: [...fakeWriteLocations, ...fakeReadLocations],
- nextToken: 'next',
- });
- mockListLocations.mockResolvedValueOnce({
- locations: [invalidPrefixLocation, ...fakeReadWriteLocations],
- nextToken: undefined,
- });
-
- const listLocationsAction = createListLocationsAction(mockListLocations);
- const output = await listLocationsAction(
- { nextToken: undefined, result: [] },
- { options: { pageSize: 36, exclude: 'WRITE' } }
- );
-
- expect(mockListLocations).toHaveBeenCalledTimes(2);
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 36,
- nextToken: undefined,
- });
- expect(mockListLocations).toHaveBeenCalledWith({
- pageSize: 27,
- nextToken: 'next',
- });
-
- expect(output.result).toStrictEqual(
- [...fakeReadLocations, ...fakeReadWriteLocations].map(
- parseAccessGrantLocation
- )
- );
- expect(output.nextToken).toBeUndefined();
- });
-
- it.todo('handles a search action as expected');
- it.todo('handles a refresh action as expected');
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/actions.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/actions.tsx
deleted file mode 100644
index 69dc7832295..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/actions.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-
-import { AsyncDataAction, DataAction } from '@aws-amplify/ui-react-core';
-
-import {
- ActionState,
- InitialValue,
- createActionStateContext,
-} from './createActionStateContext';
-
-import { createFolderAction } from './createFolderAction';
-import { listLocationItemsAction } from './listLocationItemsAction';
-import { ListLocationsAction } from './listLocationsAction';
-import { LocationsDataProvider } from './locationsData';
-import { useGetActionInput } from '../../providers/configuration';
-
-export type ActionsWithConfig = {
- [K in keyof DefaultActions]: WithLocationConfig;
-};
-
-export type DefaultActions = typeof DEFAULT_ACTIONS;
-export type WithLocationConfig = T extends AsyncDataAction
- ? AsyncDataAction>
- : T extends DataAction
- ? DataAction>
- : never;
-
-export type UseActionState = T extends
- | AsyncDataAction
- | DataAction
- ? ActionState
- : never;
-
-export const ERROR_MESSAGE =
- '`useAction` must be called from within `StorageBrowser.Provider`';
-
-export const DEFAULT_ACTIONS = {
- CREATE_FOLDER: createFolderAction,
- LIST_LOCATION_ITEMS: listLocationItemsAction,
-};
-
-export const INITIAL_VALUE: InitialValue = {
- CREATE_FOLDER: { result: undefined },
- LIST_LOCATION_ITEMS: { result: [], nextToken: undefined },
-};
-
-const [ActionStateProvider, useActionState] = createActionStateContext(
- DEFAULT_ACTIONS,
- ERROR_MESSAGE
-);
-
-export const useAction = (
- type: T
-): UseActionState => {
- const [state, handle] = useActionState({ type });
-
- const getConfig = useGetActionInput();
-
- const handleAction = React.useCallback(
- (input: Parameters[1]>) => {
- const { credentials: credentialsProvider, ...config } = getConfig();
- return handle({ ...input, config: { ...config, credentialsProvider } });
- },
- [getConfig, handle]
- );
-
- return [state, handleAction] as UseActionState;
-};
-
-export function ActionProvider({
- children,
- listLocationsAction,
-}: {
- children?: React.ReactNode;
- listLocationsAction: ListLocationsAction;
-}): React.JSX.Element {
- return (
-
-
- {children}
-
-
- );
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/createActionStateContext.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/createActionStateContext.tsx
deleted file mode 100644
index a3f19373fcd..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/createActionStateContext.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React from 'react';
-
-import {
- AsyncDataAction,
- DataAction,
- DataState,
- useDataState,
-} from '@aws-amplify/ui-react-core';
-
-type SyncOrAsyncAction =
- | DataAction
- | AsyncDataAction;
-
-type ContextProvider = (props: {
- children?: React.ReactNode;
-}) => React.JSX.Element;
-
-export type ActionState = [
- state: DataState,
- handleAction: (...input: K[]) => void,
-];
-
-interface ActionContext {
- Context: React.Context | undefined>;
- Provider: ContextProvider;
-}
-
-type DataActions = { [key: string]: AsyncDataAction | DataAction };
-
-type ActionContexts = {
- [K in keyof T]: T[K] extends SyncOrAsyncAction
- ? ActionContext
- : never;
-};
-
-export type InitialValue = {
- [K in keyof T]: T[K] extends SyncOrAsyncAction ? X : never;
-};
-
-type ActionsState = {
- [K in keyof T]: T[K] extends SyncOrAsyncAction
- ? ActionState
- : never;
-};
-
-export type UseAction = (input: {
- type: U;
-}) => ActionsState[U];
-
-export interface ActionProviderProps {
- children?: React.ReactNode;
- initialValue: T;
-}
-
-type ActionProvider = (props: ActionProviderProps) => React.JSX.Element;
-
-const InitialValue = React.createContext | undefined>(
- undefined
-);
-function InitialValueProvider>({
- children,
- initialValue,
-}: ActionProviderProps) {
- return (
-
- {children}
-
- );
-}
-
-function createActionContext(
- action: AsyncDataAction | DataAction,
- type: string
-) {
- const ActionContext = React.createContext | undefined>(
- undefined
- );
-
- function Provider(props: { children?: React.ReactNode }) {
- const initialValue = React.useContext(InitialValue);
- const value = useDataState(action, initialValue?.[type]);
- return ;
- }
-
- return { Provider, Context: ActionContext };
-}
-
-export function createActionProvider(
- contexts: ActionContexts
-): ActionProvider> {
- const ComposedActionProvider = Object.values(contexts).reduce(
- (Wrapper, { Provider }) =>
- function ActionProvider({
- children,
- }: {
- children?: React.ReactNode;
- }): React.JSX.Element {
- return (
-
- {children}
-
- );
- },
- ({ children }: { children?: React.ReactNode }): React.JSX.Element => (
- <>{children}>
- )
- );
-
- return function ActionProvider({
- children,
- ...props
- }: ActionProviderProps>): React.JSX.Element {
- return (
-
- {children}
-
- );
- };
-}
-
-export function createUseAction(
- contexts: ActionContexts,
- errorMessage: string
-): UseAction {
- return function useAction({ type }: { type: K }) {
- const context = React.useContext(contexts[type].Context);
- if (!context) {
- throw new Error(errorMessage);
- }
- return context as ActionsState[K];
- };
-}
-
-const createContexts = (actions: T) =>
- Object.entries(actions).reduce(
- (acc, [type, action]) => ({
- ...acc,
- [type]: createActionContext(action, type),
- }),
- {} as ActionContexts
- );
-
-export type ActionStateContext = [
- Provider: ActionProvider>,
- useAction: UseAction,
-];
-
-export function createActionStateContext(
- actions: T,
- errorMessage: string
-): ActionStateContext {
- const contexts = createContexts(actions);
-
- return [
- createActionProvider(contexts),
- createUseAction(contexts, errorMessage),
- ];
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/createFolderAction.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/createFolderAction.ts
deleted file mode 100644
index 5f2f0f59429..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/createFolderAction.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { uploadData } from '../../storage-internal';
-
-import { TaskAction, TaskActionInput, TaskActionOutput } from '../types';
-
-export interface CreateFolderActionInput
- extends Omit<
- TaskActionInput<{ reset?: boolean; preventOverwrite?: boolean }>,
- 'data'
- > {}
-
-export interface CreateFolderActionInputV2
- extends Omit<
- TaskActionInput<{ reset?: boolean; preventOverwrite?: boolean }>,
- 'data'
- > {}
-
-export interface CreateFolderActionOutput extends TaskActionOutput {}
-
-export interface CreateFolderAction
- extends TaskAction {}
-
-export const createFolderAction = async (
- _: CreateFolderActionOutput,
- input: CreateFolderActionInput
-): Promise => {
- const { prefix, config, options } = input;
-
- if (options?.reset) {
- return { result: undefined };
- }
-
- const {
- accountId: expectedBucketOwner,
- bucket: bucketName,
- credentialsProvider: locationCredentialsProvider,
- region,
- customEndpoint,
- } = typeof config === 'object' ? config : config();
-
- let result: CreateFolderActionOutput['result'] | undefined;
-
- try {
- await uploadData({
- path: prefix,
- data: '',
- options: {
- bucket: { bucketName, region },
- expectedBucketOwner,
- locationCredentialsProvider,
- customEndpoint,
- },
- }).result;
- result = { key: prefix, status: 'COMPLETE', message: undefined };
- } catch (e) {
- result = { key: prefix, status: 'FAILED', message: (e as Error).message };
- }
-
- return { result };
-};
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/downloadAction.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/downloadAction.ts
deleted file mode 100644
index e0232421426..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/downloadAction.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { getUrl } from '../../storage-internal';
-
-import { DownloadActionInput, DownloadActionOutput } from '../types';
-
-export async function downloadAction(
- _: DownloadActionOutput,
- input: DownloadActionInput
-): Promise {
- const { config, key: path } = input ?? {};
- const {
- accountId,
- bucket: bucketName,
- credentialsProvider,
- region,
- customEndpoint,
- } = (typeof config === 'function' ? config() : config) ?? {};
-
- const bucket = bucketName && region ? { bucketName, region } : undefined;
-
- try {
- const signedUrl = await getUrl({
- path,
- options: {
- bucket,
- expectedBucketOwner: accountId,
- locationCredentialsProvider: credentialsProvider,
- validateObjectExistence: true,
- contentDisposition: 'attachment',
- customEndpoint,
- },
- });
-
- return { signedUrl: signedUrl.url.toString() };
- } catch (e) {
- // @TODO: update UI to let user know that the file no longer exists?
- return Promise.reject(e);
- }
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/index.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/index.ts
deleted file mode 100644
index d0bd1842070..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { createListLocationsAction } from './listLocationsAction';
-export { downloadAction } from './downloadAction';
-export { ActionProvider, useAction } from './actions';
-export { useLocationsData, LocationsDataState } from './locationsData';
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/listLocationItemsAction.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/listLocationItemsAction.ts
deleted file mode 100644
index dc1c53ec7be..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/listLocationItemsAction.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import {
- StorageSubpathStrategy,
- list,
- ListPaginateInput,
- ListOutput,
-} from '../../storage-internal';
-
-import {
- ListActionInput,
- ListActionOptions,
- ListActionOutput,
- LocationItem,
-} from '../types';
-
-export interface ListLocationItemsActionInput
- extends ListActionInput {}
-
-export interface ListLocationItemsActionOutput
- extends ListActionOutput {}
-
-type ListOutputItem = ListOutput['items'][number];
-
-const parseItems = (
- items: ListOutputItem[],
- excludedPath: string
-): LocationItem[] =>
- items
- .filter(({ path }) => path !== excludedPath)
- .map(({ path: key, lastModified, size }) => {
- const id = crypto.randomUUID();
- // Mark zero byte files as Folders
- if (size === 0 && key.endsWith('/')) {
- return { key, id, type: 'FOLDER' };
- }
-
- return {
- key,
- id,
- lastModified: lastModified!,
- size: size!,
- type: 'FILE',
- };
- });
-
-const parseExcludedPaths = (paths: string[] | undefined): LocationItem[] =>
- paths?.map((key) => ({ key, id: crypto.randomUUID(), type: 'FOLDER' })) ?? [];
-
-export const parseResult = (
- output: ListOutput,
- path: string
-): LocationItem[] => [
- ...parseExcludedPaths(output.excludedSubpaths),
- ...parseItems(output.items, path),
-];
-
-export async function listLocationItemsAction(
- prevState: ListLocationItemsActionOutput,
- input: ListLocationItemsActionInput
-): Promise {
- const { config, options, prefix: path } = input ?? {};
- const { delimiter, nextToken, pageSize, refresh, reset } = options ?? {};
-
- if (reset) {
- return { result: [], nextToken: undefined };
- }
-
- const {
- accountId,
- bucket: bucketName,
- credentialsProvider,
- region,
- customEndpoint,
- } = (typeof config === 'function' ? config() : config) ?? {};
-
- const bucket = { bucketName, region };
- const subpathStrategy: StorageSubpathStrategy = {
- delimiter,
- strategy: delimiter ? 'exclude' : 'include',
- };
-
- // `ListObjectsV2` returns the root `key` on initial request, which is from
- // filtered from `results` by `parseResult`, creatimg a scenario where the
- // return count of `results` to be one item less than provided the `pageSize`.
- // To mitigate, if a `pageSize` is provided and there are no previous `results`
- // or `refresh` is `true` increment the provided `pageSize` by `1`
- const hasPrevResults = !!prevState.result.length;
- const resolvedPageSize =
- pageSize && (!hasPrevResults || refresh) ? pageSize + 1 : pageSize;
-
- const listInput: ListPaginateInput = {
- path,
- options: {
- bucket,
- expectedBucketOwner: accountId,
- locationCredentialsProvider: credentialsProvider,
- // ignore provided `nextToken` on `refresh`
- nextToken: refresh ? undefined : nextToken,
- pageSize: resolvedPageSize,
- subpathStrategy,
- customEndpoint,
- },
- };
-
- const output = await list(listInput);
-
- const result = [
- ...(refresh ? [] : prevState.result),
- ...parseResult(output, path),
- ];
-
- return { result, nextToken: output.nextToken };
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/listLocationsAction.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/listLocationsAction.ts
deleted file mode 100644
index b6b38035356..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/listLocationsAction.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { LocationData } from '../../actions';
-import {
- ListLocations,
- ListLocationsOutput,
- LocationAccess,
-} from '../../adapters/types';
-import { Permission } from '../../storage-internal';
-
-import { ListActionInput, ListActionOptions, ListActionOutput } from '../types';
-import { toAccessGrantPermission } from '../../adapters/permissionParsers';
-import { parseAccessGrantLocationScope } from '../../actions/handlers';
-
-const PAGE_SIZE = 1000;
-
-export interface ListLocationsActionOptions
- extends Omit, 'delimiter'> {}
-
-export interface ListLocationsActionInput
- extends Omit<
- ListActionInput>,
- 'prefix' | 'config'
- > {}
-
-export interface ListLocationsActionOutput
- extends ListActionOutput {}
-
-export type ListLocationsAction = (
- prevState: ListLocationsActionOutput,
- input: ListLocationsActionInput
-) => Promise;
-
-const shouldExclude = (
- permission: T,
- exclude?: T | T[]
-) =>
- !exclude
- ? false
- : typeof exclude === 'string'
- ? exclude === permission
- : exclude.includes(permission);
-
-// FIXME: temporary fix until we use the list action in actions folder
-export const parseAccessGrantLocation = (
- location: LocationAccess
-): LocationData => {
- const { scope, type } = location;
- if (!scope.startsWith('s3://')) {
- throw new Error(`Invalid scope: ${scope}`);
- }
- const id = crypto.randomUUID();
- const { bucket, prefix } = parseAccessGrantLocationScope(scope, type);
- return { id, ...location, bucket, prefix };
-};
-
-export const createListLocationsAction = (
- listLocations: ListLocations
-): ListLocationsAction =>
- async function listLocationsAction(prevState, input) {
- const { options } = input ?? {};
- const {
- exclude,
- nextToken,
- pageSize = PAGE_SIZE,
- refresh,
- reset,
- } = options ?? {};
-
- if (reset) {
- return { result: [], nextToken: undefined };
- }
-
- let locationsResult: ListLocationsOutput['locations'] = [];
- let nextNextToken: ListLocationsOutput['nextToken'] = refresh
- ? undefined
- : nextToken;
-
- do {
- const remainingPageSize = pageSize - locationsResult.length;
-
- const output = await listLocations({
- nextToken: nextNextToken,
- pageSize: remainingPageSize,
- });
-
- nextNextToken = output.nextToken;
- locationsResult = [
- ...locationsResult,
- ...output.locations.filter(
- ({ permissions, type, scope }) =>
- !(
- shouldExclude(toAccessGrantPermission(permissions), exclude) ||
- // filter out PREFIX/BUCKET types with scopes that don't end with /*, e.g. /prefix*
- (type !== 'OBJECT' && !scope.endsWith('/*'))
- )
- ),
- ];
- } while (nextNextToken && locationsResult.length < pageSize);
-
- const nextLocations = locationsResult.map(parseAccessGrantLocation);
-
- const result = refresh
- ? nextLocations
- : [...(prevState.result ?? []), ...nextLocations];
-
- return { result, nextToken: nextNextToken };
- };
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/locationsData.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/locationsData.tsx
deleted file mode 100644
index 7efac1819ab..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/actions/locationsData.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react';
-
-import { useDataState } from '@aws-amplify/ui-react-core';
-
-import { ActionState } from './createActionStateContext';
-import {
- ListLocationsAction,
- ListLocationsActionInput,
- ListLocationsActionOutput,
-} from './listLocationsAction';
-
-export type LocationsDataState = ActionState<
- ListLocationsActionOutput,
- ListLocationsActionInput
->;
-
-const INITIAL_VALUE = { result: [], nextToken: undefined };
-const ERROR_MESSAGE =
- '`useLocationsData` must be called from with `LocationsDataProvider';
-
-const LocationsDataContext = React.createContext<
- LocationsDataState | undefined
->(undefined);
-
-export function LocationsDataProvider({
- children,
- listLocationsAction,
-}: {
- children?: React.ReactNode;
- listLocationsAction: ListLocationsAction;
-}): React.JSX.Element {
- const value = useDataState(listLocationsAction, INITIAL_VALUE);
-
- return (
-
- {children}
-
- );
-}
-
-export function useLocationsData(): LocationsDataState {
- const context = React.useContext(LocationsDataContext);
- if (!context) {
- throw new Error(ERROR_MESSAGE);
- }
-
- return context;
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/createTempActionsProvider.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/createTempActionsProvider.tsx
deleted file mode 100644
index d876d4eed86..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/createTempActionsProvider.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react';
-import { createContextUtilities } from '@aws-amplify/ui-react-core';
-
-import { ActionProvider, createListLocationsAction } from './actions';
-import { LocationActions } from './locationActions';
-import { ListLocations } from '../adapters/types';
-
-export const { useTempActions, TempActionsContext } = createContextUtilities<
- LocationActions,
- 'TempActions'
->({
- contextName: 'TempActions',
- errorMessage: 'Call to useTempActions must be wrapped in TempActionsProvider',
-});
-
-function TempActionsProvider({
- actions,
- children,
-}: {
- actions: LocationActions;
- children?: React.ReactNode;
-}): React.JSX.Element {
- return (
-
- {children}
-
- );
-}
-
-interface Config {
- accountId?: string;
- listLocations: ListLocations;
- region: string;
-}
-
-interface CreateTempActionsProviderInput {
- actions: LocationActions;
- config: Config;
-}
-
-export function createTempActionsProvider({
- actions,
- config,
-}: CreateTempActionsProviderInput): (props: {
- children?: React.ReactNode;
-}) => React.JSX.Element {
- const listLocationsAction = createListLocationsAction(config.listLocations);
-
- function Provider({
- children,
- }: {
- children?: React.ReactNode;
- }): React.JSX.Element {
- return (
-
-
- {children}
-
-
- );
- }
-
- return Provider;
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/__tests__/__snapshots__/defaults.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/__tests__/__snapshots__/defaults.spec.ts.snap
deleted file mode 100644
index 8fe03d3e083..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/__tests__/__snapshots__/defaults.spec.ts.snap
+++ /dev/null
@@ -1,58 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`default actions contains the expected properties 1`] = `
-{
- "COPY_FILES": {
- "options": {
- "disable": [Function],
- "displayName": "Copy files",
- "hide": [Function],
- "icon": ,
- },
- },
- "CREATE_FOLDER": {
- "options": {
- "disable": [Function],
- "displayName": "Create folder",
- "hide": [Function],
- "icon": ,
- },
- },
- "DELETE_FILES": {
- "options": {
- "disable": [Function],
- "displayName": "Delete files",
- "hide": [Function],
- "icon": ,
- },
- },
- "UPLOAD_FILES": {
- "options": {
- "disable": [Function],
- "displayName": "Upload files",
- "hide": [Function],
- "icon": ,
- "selectionData": "file",
- },
- },
- "UPLOAD_FOLDER": {
- "options": {
- "disable": [Function],
- "displayName": "Upload folder",
- "hide": [Function],
- "icon": ,
- "selectionData": "folder",
- },
- },
-}
-`;
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/__tests__/defaults.spec.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/__tests__/defaults.spec.ts
deleted file mode 100644
index 2c65c806773..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/__tests__/defaults.spec.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { locationActionsDefault, OPTIONS_DEFAULT } from '../defaults';
-import { LocationActionOptions } from '../types';
-
-describe('default actions', () => {
- it('contains the expected properties', () => {
- expect(locationActionsDefault).toMatchSnapshot();
- });
-
- it('has the expected behavior for the values of default options', () => {
- const disable = OPTIONS_DEFAULT?.disable as Exclude<
- LocationActionOptions['disable'],
- undefined | boolean
- >;
-
- expect(typeof disable).toBe('function');
- expect(disable([])).toBe(false);
- expect(disable([{ type: 'FOLDER', key: 'something', id: 'an-id' }])).toBe(
- true
- );
-
- const hide = OPTIONS_DEFAULT?.hide as Exclude<
- LocationActionOptions['hide'],
- undefined | boolean
- >;
-
- expect(typeof hide).toBe('function');
- expect(hide('READWRITE')).toBe(false);
- expect(hide('WRITE')).toBe(false);
- expect(hide('READ')).toBe(true);
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/defaults.tsx b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/defaults.tsx
deleted file mode 100644
index 401494f45a4..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/defaults.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-import { IconElement } from '../../context/elements/IconElement';
-
-import { LocationAction } from './types';
-import { displayText } from '../../displayText/en';
-const {
- createFolderTitle,
- deleteFilesTitle,
- uploadFilesTitle,
- uploadFolderTitle,
-} = displayText;
-
-export const OPTIONS_DEFAULT: LocationAction['options'] = {
- disable: (items) => !!items.length,
- hide: (permission) => permission === 'READ',
-};
-
-const COPY_FILES: LocationAction = {
- options: {
- ...OPTIONS_DEFAULT,
- disable: (selectedItems) => selectedItems.length < 1,
- displayName: 'Copy files',
- icon: ,
- },
-};
-
-const CREATE_FOLDER: LocationAction = {
- options: {
- ...OPTIONS_DEFAULT,
- displayName: createFolderTitle,
- icon: ,
- },
-};
-
-const DELETE_FILES: LocationAction = {
- options: {
- ...OPTIONS_DEFAULT,
- disable: (selectedItems) => selectedItems.length < 1,
- displayName: deleteFilesTitle,
- icon: ,
- },
-};
-
-const UPLOAD_FOLDER: LocationAction = {
- options: {
- ...OPTIONS_DEFAULT,
- displayName: uploadFolderTitle,
- icon: ,
- selectionData: 'folder',
- },
-};
-
-const UPLOAD_FILES: LocationAction = {
- options: {
- ...OPTIONS_DEFAULT,
- displayName: uploadFilesTitle,
- icon: ,
- selectionData: 'file',
- },
-};
-
-export const locationActionsDefault = {
- COPY_FILES,
- CREATE_FOLDER,
- DELETE_FILES,
- UPLOAD_FILES,
- UPLOAD_FOLDER,
-};
-
-export type LocationActionsDefault = typeof locationActionsDefault;
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/index.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/index.ts
deleted file mode 100644
index 7a09fcbd040..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { LocationActionsDefault, locationActionsDefault } from './defaults';
-export { LocationAction, LocationActions, LocationActionsState } from './types';
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/types.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/types.ts
deleted file mode 100644
index f37128b391e..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/locationActions/types.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-
-import { Permission } from '../../storage-internal';
-import { PrefixTaskAction, LocationItem } from '../types';
-
-/**
- * open native OS file picker with associated selection type on action select
- */
-export type SelectionType =
- | ('file' | 'folder')
- | ['file' | 'folder', ...string[]];
-
-export interface LocationActionOptions {
- /**
- * disable menu
- */
- disable?: boolean | ((selectedItems: LocationItem[]) => boolean);
- displayName?: string;
- hide?: boolean | ((permission: T) => boolean);
- icon?: React.ReactNode | string;
- selectionData?: SelectionType;
-}
-
-interface _LocationAction {
- readonly handler: PrefixTaskAction;
- options?: LocationActionOptions;
-}
-export interface LocationAction
- extends Omit<_LocationAction, 'handler'> {}
-
-export interface LocationActions {
- [key: string]: Omit, 'handler'>;
-}
-
-export type LocationActionsAction =
- | { type: 'CLEAR' }
- | { type: 'SET_ACTION'; actionType: T }
- | { type: 'TOGGLE_SELECTED_ITEM'; item: LocationItem }
- | { type: 'TOGGLE_SELECTED_ITEMS'; items?: LocationItem[] };
-
-export interface LocationActionsState {
- actions: LocationActions;
- selected: {
- type: T | undefined;
- items: LocationItem[] | undefined;
- };
-}
-
-export type LocationActionsStateContext = [
- state: LocationActionsState,
- handleUpdateState: (action: LocationActionsAction) => void,
-];
-
-export interface LocationActionsProviderProps {
- actions?: LocationActions;
- actionType?: string;
- children?: React.ReactNode;
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/types.ts b/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/types.ts
deleted file mode 100644
index 1e456e2893f..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/do-not-import-from-here/types.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { LocationCredentialsProvider } from '../storage-internal';
-
-interface FolderItem {
- key: string;
- id: string;
- type: 'FOLDER';
-}
-
-interface FileItem {
- key: string;
- id: string;
- lastModified: Date;
- size: number;
- type: 'FILE';
-}
-
-export type LocationItem = FileItem | FolderItem;
-
-export interface LocationConfig {
- accountId?: string;
- bucket: string;
- credentialsProvider: LocationCredentialsProvider;
- customEndpoint?: string;
- region: string;
-}
-
-export type TaskStatus =
- | 'INITIAL'
- | 'QUEUED'
- | 'PENDING'
- | 'FAILED'
- | 'COMPLETE';
-
-export interface TaskResult {
- key: string;
- message: string | undefined;
- status: T;
-}
-
-export interface TaskActionInput {
- prefix: string;
- config: LocationConfig | (() => LocationConfig);
- data: File;
- options?: T;
-}
-
-export interface TaskActionOutput {
- result: T | undefined;
-}
-
-export type PrefixTaskAction = (
- input: T
-) => Promise;
-
-export type TaskAction = (
- input: T
-) => Promise;
-
-export interface ListActionOptions {
- delimiter?: string;
- exclude?: T | T[];
- nextToken?: string;
- pageSize?: number;
- refresh?: boolean;
- reset?: boolean;
-}
-
-export interface ListActionInput {
- prefix: string;
- config: (() => LocationConfig) | LocationConfig;
- options?: K;
-}
-
-export interface ListActionOutput {
- result: T[];
- nextToken: string | undefined;
-}
-
-export interface DownloadActionInput {
- key: string;
- config?: (() => LocationConfig) | LocationConfig;
-}
-
-export interface DownloadActionOutput {
- signedUrl: string;
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/index.ts b/packages/react-storage/src/components/StorageBrowser/index.ts
index 825a2c1fb4e..ce7bae3a708 100644
--- a/packages/react-storage/src/components/StorageBrowser/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/index.ts
@@ -1,13 +1,9 @@
export { StorageBrowserElements, elementsDefault } from './context/elements';
-export {
- createStorageBrowser,
- CreateStorageBrowserInput,
- StorageBrowserComponent,
- ResolvedStorageBrowserElements,
-} from './createStorageBrowser';
+export { createStorageBrowser } from './createStorageBrowser';
export {
createAmplifyAuthAdapter,
createManagedAuthAdapter,
CreateManagedAuthAdapterInput,
StorageBrowserAuthAdapter,
} from './adapters';
+export { CreateStorageBrowserInput, StorageBrowserType } from './types';
diff --git a/packages/react-storage/src/components/StorageBrowser/types.ts b/packages/react-storage/src/components/StorageBrowser/types.ts
new file mode 100644
index 00000000000..57746bfdf4b
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/types.ts
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import { ListLocations } from './actions';
+
+import { StorageBrowserElements } from './context/elements';
+import { Components } from './ComponentsProvider';
+
+import { RegisterAuthListener, StoreProviderProps } from './providers';
+
+import {
+ CopyViewType,
+ CreateFolderViewType,
+ DeleteViewType,
+ UploadViewType,
+ Views,
+} from './views';
+
+import { GetLocationCredentials } from './credentials/types';
+import { StorageBrowserDisplayText } from './displayText';
+
+export interface Config {
+ accountId?: string;
+ customEndpoint?: string;
+ getLocationCredentials: GetLocationCredentials;
+ listLocations: ListLocations;
+ registerAuthListener: RegisterAuthListener;
+ region: string;
+}
+
+export interface CreateStorageBrowserInput {
+ // to be updated
+ actions?: never;
+ config: Config;
+ components?: Components;
+ elements?: Partial;
+}
+
+export interface StorageBrowserProps {
+ views?: Views;
+ displayText?: StorageBrowserDisplayText;
+}
+
+export interface StorageBrowserType extends Views {
+ (
+ props: StorageBrowserProps & Exclude
+ ): React.JSX.Element;
+ displayName: string;
+ Provider: (props: StorageBrowserProviderProps) => React.JSX.Element;
+ CopyView: CopyViewType;
+ CreateFolderView: CreateFolderViewType;
+ DeleteView: DeleteViewType;
+ UploadView: UploadViewType;
+}
+
+export type ActionViewName = Exclude<
+ T,
+ 'listLocationItems' | 'listLocations'
+>;
+
+export interface StorageBrowserProviderProps extends StoreProviderProps {
+ displayText?: StorageBrowserDisplayText;
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/validators/index.ts b/packages/react-storage/src/components/StorageBrowser/validators/index.ts
index 1c58c4de74d..669e5f32a15 100644
--- a/packages/react-storage/src/components/StorageBrowser/validators/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/validators/index.ts
@@ -1,3 +1,4 @@
export { assertLocationData } from './assertLocationData';
export { assertRegisterAuthListener } from './assertRegisterAuthListener';
export { assertAccountId } from './assertAccountId';
+export { isFileTooBig } from './isFileTooBig';
diff --git a/packages/react-storage/src/components/StorageBrowser/validators/isFileTooBig.ts b/packages/react-storage/src/components/StorageBrowser/validators/isFileTooBig.ts
new file mode 100644
index 00000000000..1a7869ca34e
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/validators/isFileTooBig.ts
@@ -0,0 +1,4 @@
+export const UPLOAD_FILE_SIZE_LIMIT = 160 * 1000 * 1000 * 1000;
+
+export const isFileTooBig = (file: File): boolean =>
+ file.size > UPLOAD_FILE_SIZE_LIMIT;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx b/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx
index 2d70cca498b..33d132b22d5 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/Controls/Controls.tsx
@@ -1,12 +1,9 @@
-import { EmptyMessageControl } from './EmptyMessage';
import { MessageControl } from './Message';
export interface Controls {
- EmptyMessage: typeof EmptyMessageControl;
Message: typeof MessageControl;
}
export const Controls: Controls = {
- EmptyMessage: EmptyMessageControl,
Message: MessageControl,
};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/EmptyMessage.tsx b/packages/react-storage/src/components/StorageBrowser/views/Controls/EmptyMessage.tsx
deleted file mode 100644
index 50dafddb588..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/Controls/EmptyMessage.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-import { ViewElement } from '../../context/elements/definitions';
-import { STORAGE_BROWSER_BLOCK_TO_BE_UPDATED } from '../../constants';
-
-interface EmptyMessageControlProps {
- children?: React.ReactNode;
-}
-
-export const EmptyMessageControl = ({
- children,
-}: EmptyMessageControlProps): React.JSX.Element => (
-
- {children}
-
-);
diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/__tests__/EmptyMessage.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/Controls/__tests__/EmptyMessage.spec.tsx
deleted file mode 100644
index 9d02cffedf1..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/Controls/__tests__/EmptyMessage.spec.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { EmptyMessageControl } from '../EmptyMessage';
-
-describe('EmptyMessageControl', () => {
- it('renders the EmptyMessageControl', () => {
- const message = 'No items to show.';
- render({message});
-
- const title = screen.getByText(message);
- expect(title).toBeInTheDocument();
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts b/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts
index 1085fc603f8..fee01746c71 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/Controls/index.ts
@@ -1,3 +1,2 @@
-export { EmptyMessageControl } from './EmptyMessage';
export { MessageControl } from './Message';
export { Controls } from './Controls';
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.tsx
index 80c799ec890..df2f28c7b14 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.tsx
@@ -17,20 +17,17 @@ import { SearchControl } from '../../../controls/SearchControl';
import { StatusDisplayControl } from '../../../controls/StatusDisplayControl';
import { TitleControl } from '../../../controls/TitleControl';
-import { resolveClassName } from '../../utils';
-
import { CopyViewProvider } from './CopyViewProvider';
-import { DestinationControl } from './DestinationControl';
import { FoldersMessageControl } from './FoldersMessageControl';
import { FoldersPaginationControl } from './FoldersPaginationControl';
import { FoldersTableControl } from './FoldersTableControl';
-import { CopyViewProps } from './types';
+import { ActionDestinationControl } from '../../../controls/ActionDestinationControl';
+
+import { CopyViewType } from './types';
import { useCopyView } from './useCopyView';
+import { classNames } from '@aws-amplify/ui';
-export function CopyView({
- className,
- ...props
-}: CopyViewProps): React.JSX.Element {
+export const CopyView: CopyViewType = ({ className, ...props }) => {
const state = useCopyView(props);
const {
isProcessing,
@@ -43,7 +40,7 @@ export function CopyView({
}, [onInitialize]);
return (
-
+
@@ -55,7 +52,7 @@ export function CopyView({
{isProcessing || isProcessingComplete ? null : (
<>
)}
-
+
{!(isProcessing || isProcessingComplete) ? null : (
)}
@@ -87,24 +84,24 @@ export function CopyView({
-
+
);
-}
+};
CopyView.displayName = 'CopyView';
CopyView.Provider = CopyViewProvider;
CopyView.Cancel = ActionCancelControl;
-CopyView.Destination = DestinationControl;
+CopyView.Destination = ActionDestinationControl;
CopyView.Exit = ActionExitControl;
-CopyView.Folders = FoldersTableControl;
-CopyView.FoldersLoading = LoadingIndicatorControl;
+CopyView.FoldersLoadingIndicator = LoadingIndicatorControl;
CopyView.FoldersMessage = FoldersMessageControl;
CopyView.FoldersPagination = FoldersPaginationControl;
CopyView.FoldersSearch = SearchControl;
+CopyView.FoldersTable = FoldersTableControl;
CopyView.Message = MessageControl;
CopyView.Start = ActionStartControl;
CopyView.Statuses = StatusDisplayControl;
-CopyView.Tasks = DataTableControl;
+CopyView.TasksTable = DataTableControl;
CopyView.Title = TitleControl;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.tsx
index 442b07ba1fb..c9982001f58 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.tsx
@@ -5,7 +5,6 @@ import { useDisplayText } from '../../../displayText';
import { getActionViewTableData } from '../getActionViewTableData';
-import { DestinationProvider } from './DestinationControl';
import { FoldersMessageProvider } from './FoldersMessageControl';
import { FoldersPaginationProvider } from './FoldersPaginationControl';
import { FoldersTableProvider } from './FoldersTableControl';
@@ -23,7 +22,6 @@ export function CopyViewProvider({
actionExitLabel,
actionStartLabel,
getActionCompleteMessage,
- getListFoldersResultsMessage,
overwriteWarningMessage,
searchPlaceholder,
searchSubmitLabel,
@@ -35,18 +33,18 @@ export function CopyViewProvider({
} = displayText;
const {
- destinationList,
+ destination,
folders,
isProcessing,
isProcessingComplete,
location,
+ statusCounts,
+ tasks,
onActionCancel,
onActionExit,
onActionStart,
- onDestinationChange,
+ onSelectDestination,
onTaskRemove,
- statusCounts,
- tasks,
} = props;
const {
@@ -55,19 +53,17 @@ export function CopyViewProvider({
hasError: hasFoldersError,
message: foldersErrorMessage,
query,
- hasInitialized: hasFoldersInitialized,
- onQuery,
- onSearchClear,
- onSearch,
- onSelect,
- onPaginate,
isLoading,
page,
pageItems,
+ onPaginate,
+ onQuery,
+ onSearchClear,
+ onSearch,
+ onSelectFolder,
} = folders;
- const { current, key: locationKey } = location ?? {};
- const { bucket } = current ?? {};
+ const { key: locationKey } = location ?? {};
const tableData = getActionViewTableData({
tasks,
@@ -78,7 +74,7 @@ export function CopyViewProvider({
});
const isActionStartDisabled =
- isProcessing || isProcessingComplete || destinationList.length === 0;
+ isProcessing || isProcessingComplete || !destination?.current;
const isActionCancelDisabled = !isProcessing || isProcessingComplete;
@@ -89,22 +85,16 @@ export function CopyViewProvider({
}
: getActionCompleteMessage({ counts: statusCounts });
- const foldersMessage = !hasFoldersInitialized
- ? undefined
- : getListFoldersResultsMessage({
- hasError: hasFoldersError,
- message: foldersErrorMessage,
- folders: pageItems,
- query,
- });
-
return (
-
-
-
-
- {children}
-
-
-
-
+
+ {children}
+
+
+
);
}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/DestinationControl.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/DestinationControl.tsx
deleted file mode 100644
index f30308adb8e..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/DestinationControl.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react';
-import { createContextUtilities } from '@aws-amplify/ui-react-core';
-
-import { Breadcrumb } from '../../../components/BreadcrumbNavigation';
-import { DescriptionList } from '../../../components/DescriptionList';
-
-export interface DestinationProps {
- bucket?: string;
- label?: string;
- isDisabled?: boolean;
- destinationList?: string[];
- onDestinationChange?: (destination: string[]) => void;
-}
-
-const defaultValue: DestinationProps = {};
-export const { useDestination, DestinationProvider } = createContextUtilities({
- contextName: 'Destination',
- defaultValue,
-});
-
-/**
- * Temporary `Destination` for `CopyView` only
- */
-export function DestinationControl(): React.JSX.Element {
- const { bucket, destinationList, isDisabled, label, onDestinationChange } =
- useDestination();
-
- const handleNavigatePath = (index: number) => {
- const newPath = destinationList?.slice(0, index + 1);
- if (!newPath) return;
-
- onDestinationChange?.(newPath);
- };
-
- return (
-
- {destinationList.map((key, index) => (
- handleNavigatePath(index)
- }
- // If bucket level access, show bucket name as root breadcrumb
- name={key === '' ? bucket : key.replace('/', '')}
- />
- ))}
- >
- ) : (
- '-'
- ),
- },
- ]}
- />
- );
-}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.tsx
index 9290ac8204b..4fc5eb1f0da 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.tsx
@@ -2,22 +2,39 @@ import React from 'react';
import { createContextUtilities } from '@aws-amplify/ui-react-core';
import { ControlsContextProvider } from '../../../controls/context';
-import { MessageProps } from '../../../composables/Message';
+
import { MessageControl } from '../../../controls/MessageControl';
import { ViewElement } from '../../../context/elements';
import { STORAGE_BROWSER_BLOCK } from '../../../constants';
+import { FolderData } from '../../../actions';
+import { useDisplayText } from '../../../displayText';
-export interface FoldersMessageProps extends MessageProps {}
+export interface FoldersMessageProps {
+ hasError?: boolean;
+ message?: string;
+ folders?: FolderData[];
+ query?: string;
+}
const defaultValue: FoldersMessageProps = {};
export const { useFoldersMessage, FoldersMessageProvider } =
createContextUtilities({ contextName: 'FoldersMessage', defaultValue });
export const FoldersMessageControl = (): React.JSX.Element => {
- const message = useFoldersMessage();
+ const {
+ CopyView: { getListFoldersResultsMessage },
+ } = useDisplayText();
+ const { hasError, folders, message, query } = useFoldersMessage();
+
+ const messageContent = getListFoldersResultsMessage({
+ hasError,
+ folders,
+ message,
+ query,
+ });
return (
-
+
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersTableControl.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersTableControl.tsx
index 90cea25a3a2..02b6b17a66e 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersTableControl.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/FoldersTableControl.tsx
@@ -5,11 +5,13 @@ import { FolderData } from '../../../actions';
import { ControlsContextProvider } from '../../../controls/context';
import { DataTableControl } from '../../../controls/DataTableControl';
-import { getDestinationPickerTableData } from './utils';
+import { getDestinationPickerTableData } from './getDestinationPickerTableData';
+import { LocationState } from '../../../providers/store/location';
export interface FoldersTableProps {
+ destination?: LocationState;
folders?: FolderData[];
- onSelect?: (value: string) => void;
+ onSelectFolder?: (id: string, folderLocationPath: string) => void;
}
const defaultValue: FoldersTableProps = {};
@@ -18,11 +20,15 @@ export const { useFoldersTable, FoldersTableProvider } = createContextUtilities(
);
export const FoldersTableControl = (): React.JSX.Element => {
- const { folders, onSelect } = useFoldersTable();
+ const { destination, folders, onSelectFolder } = useFoldersTable();
+
+ const { current, path = '' } = destination ?? {};
const tableData = getDestinationPickerTableData({
+ prefix: current?.prefix ?? '',
+ path,
folders,
- onSelect,
+ onSelectFolder,
});
return (
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyView.spec.tsx
index 6b08b28d4b4..1bf813b2d4f 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyView.spec.tsx
@@ -15,6 +15,11 @@ jest.mock('../CopyViewProvider', () => ({
jest.mock('../../../../controls/ActionCancelControl', () => ({
ActionCancelControl: () => ,
}));
+jest.mock('../../../../controls/ActionDestinationControl', () => ({
+ ActionDestinationControl: () => (
+
+ ),
+}));
jest.mock('../../../../controls/ActionExitControl', () => ({
ActionExitControl: () => ,
}));
@@ -39,10 +44,6 @@ jest.mock('../../../../controls/StatusDisplayControl', () => ({
jest.mock('../../../../controls/TitleControl', () => ({
TitleControl: () => ,
}));
-
-jest.mock('../DestinationControl', () => ({
- DestinationControl: () => ,
-}));
jest.mock('../FoldersMessageControl', () => ({
FoldersMessageControl: () => ,
}));
@@ -75,15 +76,15 @@ describe('CopyView', () => {
expect(CopyView.Cancel).toBeDefined();
expect(CopyView.Destination).toBeDefined();
expect(CopyView.Exit).toBeDefined();
- expect(CopyView.Folders).toBeDefined();
- expect(CopyView.FoldersLoading).toBeDefined();
+ expect(CopyView.FoldersLoadingIndicator).toBeDefined();
expect(CopyView.FoldersMessage).toBeDefined();
expect(CopyView.FoldersPagination).toBeDefined();
expect(CopyView.FoldersSearch).toBeDefined();
+ expect(CopyView.FoldersTable).toBeDefined();
expect(CopyView.Message).toBeDefined();
expect(CopyView.Start).toBeDefined();
expect(CopyView.Statuses).toBeDefined();
- expect(CopyView.Tasks).toBeDefined();
+ expect(CopyView.TasksTable).toBeDefined();
expect(CopyView.Title).toBeDefined();
});
@@ -95,7 +96,9 @@ describe('CopyView', () => {
expect(screen.queryByTestId('ActionExitControl')).toBeInTheDocument();
expect(screen.queryByTestId('ActionStartControl')).toBeInTheDocument();
expect(screen.queryByTestId('DataTableControl')).toBeInTheDocument();
- expect(screen.queryByTestId('DestinationControl')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('ActionDestinationControl')
+ ).toBeInTheDocument();
expect(screen.queryByTestId('FoldersMessageControl')).toBeInTheDocument();
expect(
screen.queryByTestId('FoldersPaginationControl')
@@ -126,7 +129,9 @@ describe('CopyView', () => {
expect(screen.queryByTestId('ActionExitControl')).toBeInTheDocument();
expect(screen.queryByTestId('ActionStartControl')).toBeInTheDocument();
expect(screen.queryByTestId('DataTableControl')).toBeInTheDocument();
- expect(screen.queryByTestId('DestinationControl')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('ActionDestinationControl')
+ ).toBeInTheDocument();
expect(screen.queryByTestId('MessageControl')).toBeInTheDocument();
expect(screen.queryByTestId('StatusDisplayControl')).toBeInTheDocument();
@@ -160,7 +165,9 @@ describe('CopyView', () => {
expect(screen.queryByTestId('ActionExitControl')).toBeInTheDocument();
expect(screen.queryByTestId('ActionStartControl')).toBeInTheDocument();
expect(screen.queryByTestId('DataTableControl')).toBeInTheDocument();
- expect(screen.queryByTestId('DestinationControl')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('ActionDestinationControl')
+ ).toBeInTheDocument();
expect(screen.queryByTestId('MessageControl')).toBeInTheDocument();
expect(screen.queryByTestId('StatusDisplayControl')).toBeInTheDocument();
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyViewProvider.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyViewProvider.spec.tsx
index 028971cbd2b..060c71eba30 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyViewProvider.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/CopyViewProvider.spec.tsx
@@ -73,8 +73,7 @@ const location: LocationData = {
const onActionCancel = jest.fn();
const onActionExit = jest.fn();
const onActionStart = jest.fn();
-
-const onDestinationChange = jest.fn();
+const onSelectDestination = jest.fn();
const onTaskRemove = jest.fn();
const actionCallbacks = {
@@ -85,11 +84,9 @@ const actionCallbacks = {
const defaultViewState: CopyViewState = {
...actionCallbacks,
- onTaskRemove,
- destinationList: [],
+ destination: { current: undefined, path: '', key: '' },
folders: {
hasError: false,
- hasInitialized: false,
hasNextPage: false,
highestPageVisited: 1,
page: 1,
@@ -102,14 +99,15 @@ const defaultViewState: CopyViewState = {
onSearch: jest.fn(),
onInitialize: jest.fn(),
onSearchClear: jest.fn(),
- onSelect: jest.fn(),
+ onSelectFolder: jest.fn(),
},
isProcessingComplete: false,
isProcessing: false,
location: { current: location, path: '', key: `itsa-prefix/` },
- onDestinationChange,
statusCounts: { ...INITIAL_STATUS_COUNTS, QUEUED: 1, TOTAL: 1 },
tasks: [taskOne],
+ onTaskRemove,
+ onSelectDestination,
};
describe('CopyViewProvider', () => {
@@ -125,6 +123,7 @@ describe('CopyViewProvider', () => {
isActionCancelDisabled: true,
isActionStartDisabled: true,
isActionExitDisabled: false,
+ isActionDestinationNavigable: true,
statusCounts: defaultViewState.statusCounts,
},
...actionCallbacks,
@@ -134,7 +133,7 @@ describe('CopyViewProvider', () => {
it('provides the expected values to `ControlsContextProvider` on destination change', () => {
const preprocessingViewState: CopyViewState = {
...defaultViewState,
- destinationList: ['some-prefix'],
+ destination: { current: location, path: '', key: `itsa-prefix/` },
};
render();
@@ -146,6 +145,7 @@ describe('CopyViewProvider', () => {
isActionCancelDisabled: true,
isActionStartDisabled: false,
isActionExitDisabled: false,
+ isActionDestinationNavigable: true,
statusCounts: defaultViewState.statusCounts,
},
...actionCallbacks,
@@ -155,7 +155,7 @@ describe('CopyViewProvider', () => {
it('provides the expected values to `ControlsContextProvider` while processing', () => {
const processingViewState: CopyViewState = {
...defaultViewState,
- destinationList: ['some-prefix'],
+ destination: { current: location, path: '', key: `itsa-prefix/` },
isProcessing: true,
tasks: [{ ...taskOne, status: 'PENDING' }],
statusCounts: { ...defaultViewState.statusCounts, PENDING: 1, QUEUED: 0 },
@@ -170,6 +170,7 @@ describe('CopyViewProvider', () => {
isActionCancelDisabled: false,
isActionStartDisabled: true,
isActionExitDisabled: true,
+ isActionDestinationNavigable: false,
statusCounts: processingViewState.statusCounts,
},
...actionCallbacks,
@@ -179,7 +180,7 @@ describe('CopyViewProvider', () => {
it('provides the expected values to `ControlsContextProvider` post processing in the happy path', () => {
const postProcessingViewState: CopyViewState = {
...defaultViewState,
- destinationList: ['some-prefix'],
+ destination: { current: location, path: '', key: `itsa-prefix/` },
isProcessingComplete: true,
tasks: [{ ...taskOne, status: 'COMPLETE' }],
statusCounts: {
@@ -198,6 +199,7 @@ describe('CopyViewProvider', () => {
isActionCancelDisabled: true,
isActionStartDisabled: true,
isActionExitDisabled: false,
+ isActionDestinationNavigable: false,
statusCounts: postProcessingViewState.statusCounts,
},
...actionCallbacks,
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/__snapshots__/useFolders.spec.ts.snap b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/__snapshots__/useFolders.spec.ts.snap
index 69e1301b5d9..c22f4564572 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/__snapshots__/useFolders.spec.ts.snap
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/__snapshots__/useFolders.spec.ts.snap
@@ -3,7 +3,6 @@
exports[`useFolders should return the correct initial state 1`] = `
{
"hasError": false,
- "hasInitialized": false,
"hasNextPage": true,
"highestPageVisited": 1,
"isLoading": false,
@@ -13,7 +12,7 @@ exports[`useFolders should return the correct initial state 1`] = `
"onQuery": [Function],
"onSearch": [Function],
"onSearchClear": [Function],
- "onSelect": [Function],
+ "onSelectFolder": [Function],
"page": 1,
"pageItems": [
{
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/getDestinationPickerTableData.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/getDestinationPickerTableData.spec.ts
new file mode 100644
index 00000000000..f6c98a41652
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/getDestinationPickerTableData.spec.ts
@@ -0,0 +1,38 @@
+import { getDestinationPickerTableData } from '../getDestinationPickerTableData';
+
+describe('getDestinationPickerTableData', () => {
+ const prefix = 'test/prefix/';
+ const path = 'path/';
+ const key = `${prefix}${path}folder/`;
+ it('returns the expected values', () => {
+ const output = getDestinationPickerTableData({
+ prefix,
+ path,
+ folders: [{ key, id: 'id' }],
+ onSelectFolder: jest.fn(),
+ });
+
+ expect(output).toStrictEqual({
+ headers: [
+ { content: { label: 'Folder name' }, key: 'name', type: 'sort' },
+ ],
+ rows: [
+ {
+ content: [
+ {
+ content: {
+ icon: 'folder',
+ ariaLabel: 'folder/',
+ label: 'folder/',
+ onClick: expect.any(Function),
+ },
+ key: 'name-id',
+ type: 'button',
+ },
+ ],
+ key: 'id',
+ },
+ ],
+ });
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useCopyView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useCopyView.spec.ts
index bf130b5766c..d6c902850f6 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useCopyView.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useCopyView.spec.ts
@@ -12,22 +12,24 @@ describe('useCopyView', () => {
const mockDispatchStoreAction = jest.fn();
const mockCancel = jest.fn();
+ const location = {
+ current: {
+ prefix: 'test-prefix/',
+ bucket: 'bucket',
+ id: 'id',
+ permissions: ['delete', 'get', 'list', 'write'],
+ type: 'PREFIX',
+ } as LocationData,
+ path: '',
+ key: 'test-prefix/',
+ };
+
beforeEach(() => {
jest.spyOn(Store, 'useStore').mockReturnValue([
{
actionType: 'COPY',
files: [],
- location: {
- current: {
- prefix: 'test-prefix/',
- bucket: 'bucket',
- id: 'id',
- permissions: ['delete', 'get', 'list', 'write'],
- type: 'PREFIX',
- } as LocationData,
- path: '',
- key: 'test-prefix/',
- },
+ location,
locationItems: {
fileDataItems: [
{
@@ -96,7 +98,6 @@ describe('useCopyView', () => {
expect(result.current).toEqual(
expect.objectContaining({
- destinationList: ['test-prefix'],
isProcessing: false,
isProcessingComplete: false,
onActionCancel: expect.any(Function),
@@ -180,4 +181,26 @@ describe('useCopyView', () => {
type: 'RESET_ACTION_TYPE',
});
});
+
+ it('should set a destination', () => {
+ const { result } = renderHook(() => useCopyView());
+
+ expect(result.current.destination).toStrictEqual(location);
+
+ const destinationLocation = {
+ ...location.current,
+ id: 'id-2',
+ };
+ const destinationPath = 'test-path/';
+
+ act(() => {
+ result.current.onSelectDestination(destinationLocation, destinationPath);
+ });
+
+ expect(result.current.destination).toStrictEqual({
+ current: destinationLocation,
+ path: destinationPath,
+ key: `${location.current.prefix}${destinationPath}`,
+ });
+ });
});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useFolders.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useFolders.spec.ts
index e91697dc6de..435f4bec355 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useFolders.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/useFolders.spec.ts
@@ -6,6 +6,7 @@ import { LocationData } from '../../../../actions';
import * as Store from '../../../../providers/store';
import * as Config from '../../../../providers/configuration';
import { DEFAULT_LIST_OPTIONS, useFolders } from '../useFolders';
+import { LocationState } from '../../../../providers/store/location';
const mockDispatchStoreAction = jest.fn();
const mockHandleList = jest.fn();
@@ -37,6 +38,20 @@ const mockItems = [
];
describe('useFolders', () => {
+ const location = {
+ current: {
+ prefix: 'prefix1/',
+ bucket: 'bucket',
+ id: 'id',
+ permissions: ['get', 'list'],
+ type: 'PREFIX',
+ } as LocationData,
+ path: '',
+ key: 'prefix1/',
+ };
+
+ const mockSetDestination = jest.fn();
+
beforeEach(() => {
jest.clearAllMocks();
@@ -44,17 +59,7 @@ describe('useFolders', () => {
{
actionType: 'COPY',
files: [],
- location: {
- current: {
- prefix: 'test-prefix/',
- bucket: 'bucket',
- id: 'id',
- permissions: ['get', 'list'],
- type: 'PREFIX',
- } as LocationData,
- path: '',
- key: 'test-prefix/',
- },
+ location,
locationItems: {
fileDataItems: [
{
@@ -89,9 +94,7 @@ describe('useFolders', () => {
]);
const { result } = renderHook(() =>
- useFolders({
- destinationList: ['prefix1'],
- })
+ useFolders({ destination: location, setDestination: mockSetDestination })
);
await waitFor(() => {
@@ -99,7 +102,7 @@ describe('useFolders', () => {
});
});
- it('should update the reference of onInitialize on destinationList change', () => {
+ it('should update the reference of onInitialize on destination change', () => {
jest.spyOn(AmplifyReactCore, 'useDataState').mockReturnValue([
{
data: {
@@ -115,15 +118,22 @@ describe('useFolders', () => {
const { rerender, result } = renderHook(
(
- props: { destinationList: string[] } = {
- destinationList: ['prefix1'],
+ props: { destination: LocationState; setDestination: () => void } = {
+ destination: location,
+ setDestination: mockSetDestination,
}
) => useFolders(props)
);
const initial = result.current.onInitialize;
rerender({
- destinationList: ['prefix1', 'subfolder1'],
+ destination: {
+ ...location,
+ current: { ...location.current },
+ path: 'subfolder1/',
+ key: `${location.current.prefix}subfolder1/`,
+ },
+ setDestination: mockSetDestination,
});
const next = result.current.onInitialize;
@@ -145,9 +155,7 @@ describe('useFolders', () => {
mockHandleList,
]);
const { result } = renderHook(() =>
- useFolders({
- destinationList: ['prefix1'],
- })
+ useFolders({ destination: location, setDestination: mockSetDestination })
);
act(() => {
@@ -198,7 +206,7 @@ describe('useFolders', () => {
]);
const { result } = renderHook(() =>
- useFolders({ destinationList: ['prefix1'] })
+ useFolders({ destination: location, setDestination: mockSetDestination })
);
act(() => {
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/utils.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/utils.spec.ts
deleted file mode 100644
index ac72f4c4543..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/__tests__/utils.spec.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import {
- getDestinationListFullPrefix,
- getDestinationPickerTableData,
-} from '../utils';
-
-describe('getDestinationListFullPrefix', () => {
- it('returns an empty string when provided an empty array', () => {
- const output = getDestinationListFullPrefix([]);
-
- expect(output).toBe('');
- });
-
- it('returns an empty string when provided an single length array with an empty string value', () => {
- const output = getDestinationListFullPrefix(['']);
-
- expect(output).toBe('');
- });
-
- it('filters empty string values', () => {
- const output = getDestinationListFullPrefix(['', 'prefix', 'nested/']);
-
- expect(output).toBe('prefix/nested/');
- });
-
- it('postfixes a / character when missing from the last value', () => {
- const output = getDestinationListFullPrefix(['', 'prefix', 'nested']);
-
- expect(output).toBe('prefix/nested/');
- });
-});
-
-describe('getDestinationPickerTableData', () => {
- it('returns the expected values', () => {
- const onSelect = jest.fn();
- const output = getDestinationPickerTableData({
- onSelect,
- folders: [{ key: 'folder1/key', id: 'id' }],
- });
-
- expect(output).toStrictEqual({
- headers: [
- { content: { label: 'Folder name' }, key: 'key', type: 'sort' },
- ],
- rows: [
- {
- content: [
- {
- content: {
- icon: 'folder',
- ariaLabel: 'folder1',
- label: 'folder1',
- onClick: expect.any(Function),
- },
- key: 'id',
- type: 'button',
- },
- ],
- key: 'id',
- },
- ],
- });
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/getDestinationPickerTableData.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/getDestinationPickerTableData.ts
new file mode 100644
index 00000000000..dd23804d410
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/getDestinationPickerTableData.ts
@@ -0,0 +1,49 @@
+import { WithKey } from '../../../components/types';
+import { DataTableProps } from '../../../composables/DataTable';
+import { DataTableRow } from '../../../composables/DataTable/DataTable';
+
+const DESTINATION_PICKER_COLUMNS: DataTableProps['headers'] = [
+ { key: 'name', type: 'sort', content: { label: 'Folder name' } },
+];
+
+export const getDestinationPickerTableData = ({
+ prefix,
+ path,
+ folders,
+ onSelectFolder,
+}: {
+ prefix: string;
+ path: string;
+ folders?: { key: string; id: string }[];
+ onSelectFolder?: (id: string, folderLocationPath: string) => void;
+}): DataTableProps => {
+ const rows: DataTableProps['rows'] = !folders
+ ? []
+ : folders.map(({ id, key }) => {
+ const folderSubPath = key.slice(`${prefix ?? ''}${path}`.length);
+ const folderLocationPath = key.slice(prefix.length);
+ const row: WithKey = {
+ key: id,
+ content: [
+ {
+ key: `${DESTINATION_PICKER_COLUMNS[0].key}-${id}`,
+ type: 'button',
+ content: {
+ icon: 'folder',
+ ariaLabel: folderSubPath,
+ label: folderSubPath,
+ onClick: () => {
+ onSelectFolder?.(id, folderLocationPath);
+ },
+ },
+ },
+ ],
+ };
+ return row;
+ });
+
+ return {
+ headers: DESTINATION_PICKER_COLUMNS,
+ rows,
+ };
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/types.ts
index 33c16a234ec..33598c7a9ce 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/types.ts
@@ -1,14 +1,11 @@
import { FolderData, CopyHandlerData, LocationData } from '../../../actions';
-import {
- ActionViewComponent,
- ActionViewProps,
- ActionViewState,
-} from '../types';
+import { LocationState } from '../../../providers/store/location';
+import { ActionViewType, ActionViewProps, ActionViewState } from '../types';
export interface CopyViewState extends ActionViewState {
folders: FoldersState;
- destinationList: string[];
- onDestinationChange: (destination: string[]) => void;
+ destination: LocationState;
+ onSelectDestination: (location: LocationData, path?: string) => void;
}
export interface CopyViewProviderProps extends CopyViewState {
@@ -17,8 +14,23 @@ export interface CopyViewProviderProps extends CopyViewState {
export interface CopyViewProps extends ActionViewProps {}
-export interface CopyViewComponent
- extends ActionViewComponent {}
+export interface CopyViewType
+ extends ActionViewType {
+ Provider: (props: CopyViewProviderProps) => React.JSX.Element;
+ Cancel: () => React.JSX.Element | null;
+ Destination: () => React.JSX.Element | null;
+ Exit: () => React.JSX.Element | null;
+ FoldersLoadingIndicator: () => React.JSX.Element | null;
+ FoldersMessage: () => React.JSX.Element | null;
+ FoldersPagination: () => React.JSX.Element | null;
+ FoldersSearch: () => React.JSX.Element | null;
+ FoldersTable: () => 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;
+}
export interface UseCopyViewOptions {
onExit?: (location?: LocationData) => void;
@@ -26,18 +38,17 @@ export interface UseCopyViewOptions {
export interface FoldersState {
hasError: boolean;
- hasInitialized: boolean;
hasNextPage: boolean;
highestPageVisited: number;
isLoading: boolean;
message: string | undefined;
page: number;
- onInitialize: () => void;
pageItems: FolderData[];
query: string;
- onSelect: (name: string) => void;
+ onInitialize: () => void;
onPaginate: (page: number) => void;
onQuery: (value: string) => void;
onSearch: () => void;
onSearchClear: () => void;
+ onSelectFolder: (id: string, folderLocationPath: string) => void;
}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.ts
index 2ce434530da..0d72948d32c 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.ts
@@ -2,26 +2,13 @@ import React, { useState } from 'react';
import { isFunction } from '@aws-amplify/ui';
-import { copyHandler } from '../../../actions/handlers';
+import { copyHandler, LocationData } from '../../../actions/handlers';
import { Task, useProcessTasks } from '../../../tasks';
import { useGetActionInput } from '../../../providers/configuration';
import { useStore } from '../../../providers/store';
import { CopyViewState, UseCopyViewOptions } from './types';
import { useFolders } from './useFolders';
-import { getDestinationListFullPrefix } from './utils';
-
-const getInitialDestinationList = (key: string, prefix?: string) =>
- // handle root bucket access grant
- key === ''
- ? ['']
- : // handle subfolder inside root access grant
- key && prefix == ''
- ? ['', ...key.split('/').slice(0, -1)]
- : // regular access that starts at prefix (not root bucket)
- key.includes('/')
- ? key.split('/').slice(0, -1)
- : [];
export const useCopyView = (options?: UseCopyViewOptions): CopyViewState => {
const { onExit } = options ?? {};
@@ -32,7 +19,7 @@ export const useCopyView = (options?: UseCopyViewOptions): CopyViewState => {
},
dispatchStoreAction,
] = useStore();
- const { key, current } = location;
+ const { current } = location;
const getInput = useGetActionInput();
@@ -45,14 +32,12 @@ export const useCopyView = (options?: UseCopyViewOptions): CopyViewState => {
const { isProcessing, isProcessingComplete, statusCounts, tasks } =
processState;
- const [destinationList, onDestinationChange] = useState(() =>
- getInitialDestinationList(key, current?.prefix)
- );
+ const [destination, setDestination] = useState(location);
const onActionStart = () => {
handleProcess({
config: getInput(),
- destinationPrefix: getDestinationListFullPrefix(destinationList),
+ destinationPrefix: destination.key,
});
};
@@ -77,10 +62,21 @@ export const useCopyView = (options?: UseCopyViewOptions): CopyViewState => {
[dispatchStoreAction]
);
- const folders = useFolders({ destinationList, onDestinationChange });
+ const folders = useFolders({ destination, setDestination });
+
+ const onSelectDestination = (
+ selectedDestination: LocationData,
+ path?: string
+ ) => {
+ setDestination({
+ current: selectedDestination,
+ path: path ?? '',
+ key: `${selectedDestination.prefix ?? ''}${path}`,
+ });
+ };
return {
- destinationList,
+ destination,
isProcessing,
isProcessingComplete,
folders,
@@ -89,8 +85,8 @@ export const useCopyView = (options?: UseCopyViewOptions): CopyViewState => {
tasks,
onActionCancel,
onActionStart,
- onDestinationChange,
onActionExit,
+ onSelectDestination,
onTaskRemove,
};
};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.ts
index 3ccb814b11b..9bd34c12696 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.ts
@@ -1,19 +1,19 @@
import React from 'react';
-import { useDataState, useHasValueUpdated } from '@aws-amplify/ui-react-core';
+import { useDataState } from '@aws-amplify/ui-react-core';
import { usePaginate } from '../../hooks/usePaginate';
import { listLocationItemsHandler, FolderData } from '../../../actions';
import { useGetActionInput } from '../../../providers/configuration';
-import { createEnhancedListHandler } from '../../../actions/createEnhancedListHandler';
+import { createEnhancedListHandler } from '../../../actions/useAction/createEnhancedListHandler';
import { useSearch } from '../../hooks/useSearch';
-import { getDestinationListFullPrefix } from './utils';
import {
ListLocationItemsHandlerInput,
ListHandlerOutput,
} from '../../../actions';
import { FoldersState } from './types';
+import { LocationState } from '../../../providers/store/location';
const DEFAULT_PAGE_SIZE = 100;
export const DEFAULT_LIST_OPTIONS = {
@@ -28,20 +28,20 @@ export type ListFoldersAction = (
input: ListLocationItemsHandlerInput
) => Promise>;
+interface UseFoldersInput {
+ destination: LocationState;
+ setDestination: (destination: LocationState) => void;
+}
+
const listLocationItemsAction = createEnhancedListHandler(
listLocationItemsHandler as ListFoldersAction
);
export const useFolders = ({
- destinationList,
- onDestinationChange,
-}: {
- destinationList?: string[];
- onDestinationChange?: (destinationList: string[]) => void;
-}): FoldersState => {
- const prefix = !destinationList
- ? ''
- : getDestinationListFullPrefix(destinationList);
+ destination,
+ setDestination,
+}: UseFoldersInput): FoldersState => {
+ const { current, key } = destination;
const [{ data, hasError, isLoading, message }, handleList] = useDataState(
listLocationItemsAction,
@@ -52,19 +52,13 @@ export const useFolders = ({
const { items, nextToken } = data;
- const hasInitializedRef = React.useRef(false);
- const hasItemsChanged = useHasValueUpdated(items, true);
- if (hasItemsChanged) {
- hasInitializedRef.current = true;
- }
-
const onInitialize = React.useCallback(() => {
handleList({
config: getInput(),
- prefix,
+ prefix: key,
options: { ...DEFAULT_REFRESH_OPTIONS },
});
- }, [getInput, handleList, prefix]);
+ }, [getInput, handleList, key]);
const hasNextToken = !!nextToken;
@@ -73,7 +67,7 @@ export const useFolders = ({
handleList({
config: getInput(),
- prefix,
+ prefix: key,
options: { ...DEFAULT_LIST_OPTIONS, nextToken },
});
};
@@ -95,7 +89,7 @@ export const useFolders = ({
handleReset();
handleList({
config: getInput(),
- prefix,
+ prefix: key,
options: {
...DEFAULT_LIST_OPTIONS,
search: { query, filterBy: 'key' },
@@ -103,14 +97,16 @@ export const useFolders = ({
});
};
- const onSelect = (name: string) => {
- const newPath = !destinationList
- ? undefined
- : [...destinationList, name.replace('/', '')];
+ const onSelectFolder = (id: string, folderLocationPath: string) => {
+ if (!current) {
+ return;
+ }
- if (!newPath) return;
-
- onDestinationChange?.(newPath);
+ setDestination({
+ current: { ...current, id },
+ path: folderLocationPath,
+ key: `${current.prefix ?? ''}${folderLocationPath}`,
+ });
};
const {
@@ -122,7 +118,6 @@ export const useFolders = ({
return {
hasError,
- hasInitialized: hasInitializedRef.current,
hasNextPage: hasNextToken,
highestPageVisited,
isLoading,
@@ -139,10 +134,10 @@ export const useFolders = ({
resetSearch();
handleList({
config: getInput(),
- prefix,
+ prefix: key,
options: { ...DEFAULT_REFRESH_OPTIONS },
});
},
- onSelect,
+ onSelectFolder,
};
};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/utils.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/utils.ts
deleted file mode 100644
index b0fb17e9400..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CopyView/utils.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { WithKey } from '../../../components/types';
-import { DataTableProps } from '../../../composables/DataTable';
-import { DataTableRow } from '../../../composables/DataTable/DataTable';
-
-const DESTINATION_PICKER_COLUMNS: DataTableProps['headers'] = [
- { key: 'key', type: 'sort', content: { label: 'Folder name' } },
-];
-
-const getFolderNameFromKey = (key: string): string => {
- if (key === '') return 'root';
- const lastFolder = key.split('/').at(-2);
- return lastFolder ? lastFolder : '';
-};
-
-export const getDestinationListFullPrefix = (
- destinationList: string[]
-): string => {
- if (
- destinationList.length < 1 ||
- (destinationList.length === 1 && destinationList[0] === '')
- ) {
- return '';
- }
- // filter out root bucket ""
- const destination = destinationList.filter((item) => item !== '').join('/');
- return destination.endsWith('/') ? destination : `${destination}/`;
-};
-
-export const getDestinationPickerTableData = ({
- folders,
- onSelect,
-}: {
- folders?: { key: string; id: string }[];
- onSelect?: (name: string) => void;
-}): DataTableProps => {
- const rows: DataTableProps['rows'] = !folders
- ? []
- : folders.map((item) => {
- const name = getFolderNameFromKey(item.key);
- const row: WithKey = {
- key: item.id,
- content: [
- {
- key: item.id,
- type: 'button',
- content: {
- ariaLabel: name,
- label: name,
- icon: 'folder',
- onClick: () => {
- onSelect?.(name);
- },
- },
- },
- ],
- };
- return row;
- });
-
- const tableData: DataTableProps = {
- headers: DESTINATION_PICKER_COLUMNS,
- rows,
- };
- return tableData;
-};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.tsx
index 504e2ae9f80..b3cecd9492b 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.tsx
@@ -1,93 +1,27 @@
import React from 'react';
+import { STORAGE_BROWSER_BLOCK } from '../../../constants';
+import { ViewElement } from '../../../context/elements';
import { ActionStartControl } from '../../../controls/ActionStartControl';
import { ActionExitControl } from '../../../controls/ActionExitControl';
import { FolderNameFieldControl } from '../../../controls/FolderNameFieldControl';
import { MessageControl } from '../../../controls/MessageControl';
import { TitleControl } from '../../../controls/TitleControl';
-import { ControlsContextProvider } from '../../../controls/context';
-import { useDisplayText } from '../../../displayText';
-import { resolveClassName } from '../../utils';
-import { CreateFolderViewProps } from './types';
+
+import { CreateFolderViewProvider } from './CreateFolderViewProvider';
+import { CreateFolderViewType } from './types';
import { useCreateFolderView } from './useCreateFolderView';
-import { isValidFolderName } from './utils';
-import { STORAGE_BROWSER_BLOCK } from '../../../constants';
-import { ViewElement } from '../../../context/elements';
-import { LoadingIndicator } from '../../../composables/LoadingIndicator';
+import { classNames } from '@aws-amplify/ui';
-export function CreateFolderView({
+export const CreateFolderView: CreateFolderViewType = ({
className,
...props
-}: CreateFolderViewProps): React.JSX.Element {
- const {
- CreateFolderView: {
- actionExitLabel,
- actionStartLabel,
- folderNameLabel,
- folderNamePlaceholder,
- getActionCompleteMessage,
- getValidationMessage,
- loadingIndicatorLabel,
- title,
- },
- } = useDisplayText();
-
- const {
- folderName,
- folderNameId,
- isProcessing,
- isProcessingComplete,
- onActionStart,
- onActionExit,
- onFolderNameChange,
- statusCounts,
- } = useCreateFolderView(props);
-
- const loadingIndicator = (
-
- );
-
- const [validationMessage, setValidationMessage] = React.useState<
- string | undefined
- >();
-
- const message = isProcessingComplete
- ? getActionCompleteMessage({ counts: statusCounts })
- : undefined;
-
- const onValidateFolderName = (value: string) => {
- setValidationMessage(() =>
- isValidFolderName(value) ? undefined : getValidationMessage(value)
- );
- };
-
- const isActionStartDisabled =
- !folderName.length ||
- !!validationMessage ||
- isProcessing ||
- isProcessingComplete;
+}) => {
+ const state = useCreateFolderView(props);
return (
-
-
+
+
@@ -99,7 +33,17 @@ export function CreateFolderView({
-
-
+
+
);
-}
+};
+
+CreateFolderView.displayName = 'CreateFolderView';
+
+CreateFolderView.Provider = CreateFolderViewProvider;
+
+CreateFolderView.Exit = ActionExitControl;
+CreateFolderView.NameField = FolderNameFieldControl;
+CreateFolderView.Message = MessageControl;
+CreateFolderView.Start = ActionStartControl;
+CreateFolderView.Title = TitleControl;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderViewProvider.tsx
new file mode 100644
index 00000000000..0a911a50849
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderViewProvider.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+
+import { ControlsContextProvider } from '../../../controls/context';
+import { useDisplayText } from '../../../displayText';
+
+import { CreateFolderViewProviderProps } from './types';
+import { isValidFolderName } from './utils';
+
+export function CreateFolderViewProvider({
+ children,
+ ...props
+}: CreateFolderViewProviderProps): React.JSX.Element {
+ const {
+ CreateFolderView: {
+ actionExitLabel,
+ actionStartLabel,
+ folderNameLabel,
+ folderNamePlaceholder,
+ getActionCompleteMessage,
+ getValidationMessage,
+ title,
+ },
+ } = useDisplayText();
+
+ const {
+ folderName,
+ folderNameId,
+ isProcessing,
+ isProcessingComplete,
+ onActionStart,
+ onActionExit,
+ onFolderNameChange,
+ statusCounts,
+ } = props;
+
+ const [validationMessage, setValidationMessage] = React.useState<
+ string | undefined
+ >();
+
+ const message = isProcessingComplete
+ ? getActionCompleteMessage({ counts: statusCounts })
+ : undefined;
+
+ const onValidateFolderName = (value: string) => {
+ setValidationMessage(() =>
+ isValidFolderName(value) ? undefined : getValidationMessage(value)
+ );
+ };
+
+ const isActionStartDisabled =
+ !folderName.length ||
+ !!validationMessage ||
+ isProcessing ||
+ isProcessingComplete;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/__tests__/CreateFolderView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/__tests__/CreateFolderView.spec.tsx
index dcac1040ab7..407be56779a 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/__tests__/CreateFolderView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/__tests__/CreateFolderView.spec.tsx
@@ -100,6 +100,14 @@ const useCreateFolderViewSpy = jest
describe('CreateFolderView', () => {
afterEach(jest.clearAllMocks);
+ it('has the expected composable components', () => {
+ expect(CreateFolderView.Exit).toBeDefined();
+ expect(CreateFolderView.NameField).toBeDefined();
+ expect(CreateFolderView.Message).toBeDefined();
+ expect(CreateFolderView.Start).toBeDefined();
+ expect(CreateFolderView.Title).toBeDefined();
+ });
+
it('provides the expected values to `ControlsContextProvider` on initial render', () => {
render();
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/types.ts
index de54a3d1a77..47ed8650782 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/types.ts
@@ -1,10 +1,10 @@
-import { CopyHandlerData, CreateFolderHandlerData } from '../../../actions';
-
import {
- ActionViewComponent,
- ActionViewState,
- ActionViewProps,
-} from '../types';
+ CopyHandlerData,
+ CreateFolderHandlerData,
+ LocationData,
+} from '../../../actions';
+
+import { ActionViewType, ActionViewState, ActionViewProps } from '../types';
export interface CreateFolderViewState
extends Omit, 'onActionCancel'> {
@@ -17,5 +17,20 @@ export interface CreateFolderViewProps
extends ActionViewProps,
Partial {}
-export interface CreateFolderViewComponent
- extends ActionViewComponent {}
+export interface CreateFolderViewProviderProps extends CreateFolderViewState {
+ children?: React.ReactNode;
+}
+
+export interface CreateFolderViewType
+ extends ActionViewType {
+ Provider: (props: CreateFolderViewProviderProps) => React.JSX.Element;
+ Exit: () => React.JSX.Element | null;
+ NameField: () => React.JSX.Element | null;
+ Message: () => React.JSX.Element | null;
+ Start: () => React.JSX.Element | null;
+ Title: () => React.JSX.Element | null;
+}
+
+export interface UseCreateFolderViewOptions {
+ onExit?: (location?: LocationData) => void;
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.ts
index f8de233aad6..3c0bb3f66b8 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.ts
@@ -1,19 +1,17 @@
import React from 'react';
import { isFunction } from '@aws-amplify/ui';
-import { LocationData } from '../../../actions';
-
-import { useStore } from '../../../providers/store';
-
-import { useProcessTasks } from '../../../tasks';
import { createFolderHandler } from '../../../actions';
import { useGetActionInput } from '../../../providers/configuration';
-import { CreateFolderViewState } from './types';
+import { useStore } from '../../../providers/store';
+import { useProcessTasks } from '../../../tasks';
+
+import { CreateFolderViewState, UseCreateFolderViewOptions } from './types';
-export const useCreateFolderView = (params?: {
- onExit?: (location: LocationData) => void;
-}): CreateFolderViewState => {
- const { onExit } = params ?? {};
+export const useCreateFolderView = (
+ options?: UseCreateFolderViewOptions
+): CreateFolderViewState => {
+ const { onExit } = options ?? {};
const [folderName, setFolderName] = React.useState('');
const folderNameId = React.useRef(crypto.randomUUID()).current;
@@ -41,7 +39,7 @@ export const useCreateFolderView = (params?: {
});
},
onActionExit: () => {
- if (isFunction(onExit)) onExit(current!);
+ if (isFunction(onExit)) onExit(current);
dipatchStoreAction({ type: 'RESET_ACTION_TYPE' });
},
onFolderNameChange: (value) => {
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.tsx
index 179588e62cb..8e6f2d8a44c 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.tsx
@@ -1,99 +1,30 @@
import React from 'react';
-import { ControlsContextProvider } from '../../../controls/context';
import { ViewElement } from '../../../context/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 { useDisplayText } from '../../../displayText';
import { STORAGE_BROWSER_BLOCK } from '../../../constants';
-import { resolveClassName } from '../../utils';
-import { getActionViewTableData } from '../getActionViewTableData';
+import { DeleteViewProvider } from './DeleteViewProvider';
import { useDeleteView } from './useDeleteView';
-import { DeleteViewProps } from './types';
-import { LoadingIndicator } from '../../../composables/LoadingIndicator';
-import { MessageControl } from '../../Controls';
+import { DeleteViewType } from './types';
+import { classNames } from '@aws-amplify/ui';
-export function DeleteView({
- className,
- ...props
-}: DeleteViewProps): React.JSX.Element {
- const { DeleteView: displayText } = useDisplayText();
- const {
- actionCancelLabel,
- actionExitLabel,
- actionStartLabel,
- loadingIndicatorLabel,
- title,
- statusDisplayCanceledLabel,
- statusDisplayCompletedLabel,
- statusDisplayFailedLabel,
- statusDisplayQueuedLabel,
- getActionCompleteMessage,
- } = displayText;
-
- const {
- isProcessing,
- isProcessingComplete,
- location,
- statusCounts,
- tasks,
- onActionCancel,
- onActionStart,
- onActionExit,
- onTaskRemove,
- } = useDeleteView(props);
-
- const message = isProcessingComplete
- ? getActionCompleteMessage({ counts: statusCounts })
- : undefined;
-
- const tableData = getActionViewTableData({
- tasks,
- locationKey: location.key,
- isProcessing,
- displayText,
- onTaskRemove,
- });
-
- const loadingIndicator = (
-
- );
+export const DeleteView: DeleteViewType = ({ className, ...props }) => {
+ const state = useDeleteView(props);
return (
-
-
+
+
-
-
@@ -106,7 +37,19 @@ export function DeleteView({
-
-
+
+
);
-}
+};
+
+DeleteView.displayName = 'DeleteView';
+
+DeleteView.Provider = DeleteViewProvider;
+
+DeleteView.Cancel = ActionCancelControl;
+DeleteView.Exit = ActionExitControl;
+DeleteView.Message = MessageControl;
+DeleteView.Start = ActionStartControl;
+DeleteView.Statuses = StatusDisplayControl;
+DeleteView.TasksTable = DataTableControl;
+DeleteView.Title = TitleControl;
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
new file mode 100644
index 00000000000..f17f595e0b6
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+
+import { ControlsContextProvider } from '../../../controls/context';
+import { useDisplayText } from '../../../displayText';
+import { getActionViewTableData } from '../getActionViewTableData';
+import { DeleteViewProviderProps } from './types';
+
+export function DeleteViewProvider({
+ children,
+ ...props
+}: DeleteViewProviderProps): React.JSX.Element {
+ const { DeleteView: displayText } = useDisplayText();
+ const {
+ actionCancelLabel,
+ actionExitLabel,
+ actionStartLabel,
+ title,
+ statusDisplayCanceledLabel,
+ statusDisplayCompletedLabel,
+ statusDisplayFailedLabel,
+ statusDisplayQueuedLabel,
+ getActionCompleteMessage,
+ } = displayText;
+
+ const {
+ isProcessing,
+ isProcessingComplete,
+ location,
+ statusCounts,
+ tasks,
+ onActionCancel,
+ onActionStart,
+ onActionExit,
+ onTaskRemove,
+ } = props;
+
+ const message = isProcessingComplete
+ ? getActionCompleteMessage({ counts: statusCounts })
+ : undefined;
+
+ const tableData = getActionViewTableData({
+ tasks,
+ locationKey: location.key,
+ isProcessing,
+ displayText,
+ onTaskRemove,
+ });
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/__tests__/DeleteView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/__tests__/DeleteView.spec.tsx
index c947c0ce33a..c7cf74a1553 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/__tests__/DeleteView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/__tests__/DeleteView.spec.tsx
@@ -118,6 +118,16 @@ const useDeleteViewSpy = jest
describe('DeleteView', () => {
afterEach(jest.clearAllMocks);
+ it('has the expected composable components', () => {
+ expect(DeleteView.Cancel).toBeDefined();
+ expect(DeleteView.Exit).toBeDefined();
+ expect(DeleteView.Message).toBeDefined();
+ expect(DeleteView.Start).toBeDefined();
+ expect(DeleteView.Statuses).toBeDefined();
+ expect(DeleteView.TasksTable).toBeDefined();
+ expect(DeleteView.Title).toBeDefined();
+ });
+
it('provides the expected values to `ControlsContextProvider` on initial render', () => {
render();
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/types.ts
index f29406dd667..afc8d0d52df 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/DeleteView/types.ts
@@ -1,16 +1,25 @@
import { DeleteHandlerData, LocationData } from '../../../actions';
-import {
- ActionViewComponent,
- ActionViewProps,
- ActionViewState,
-} from '../types';
+import { ActionViewType, ActionViewProps, ActionViewState } from '../types';
export interface DeleteViewState extends ActionViewState {}
export interface DeleteViewProps extends ActionViewProps {}
-export interface DeleteViewComponent
- extends ActionViewComponent {}
+export interface DeleteViewProviderProps extends DeleteViewState {
+ children?: React.ReactNode;
+}
+
+export interface DeleteViewType
+ extends ActionViewType {
+ Provider: (props: DeleteViewProviderProps) => 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;
+}
export interface UseDeleteViewOptions {
onExit?: (location?: LocationData) => void;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/LocationActionView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/LocationActionView.tsx
index 9fff594fba0..631b6002403 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/LocationActionView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/LocationActionView.tsx
@@ -1,45 +1,36 @@
import React from 'react';
+import { isDefaultActionViewType } from '../../actions';
+import { useStore } from '../../providers/store';
+
import { CreateFolderView } from './CreateFolderView';
import { CopyView } from './CopyView';
import { DeleteView } from './DeleteView';
import { UploadView } from './UploadView';
-import { useStore } from '../../providers/store';
export interface LocationActionViewProps {
onExit?: () => void;
type?: T;
}
-const ACTION_VIEW_TYPES = [
- 'COPY_FILES',
- 'CREATE_FOLDER',
- 'DELETE_FILES',
- 'UPLOAD_FILES',
- 'UPLOAD_FOLDER',
-];
-
-const isActionViewType = (value?: string) =>
- ACTION_VIEW_TYPES.some((type) => type === value);
-
export const LocationActionView = ({
- onExit,
type,
+ ...props
}: LocationActionViewProps): React.JSX.Element | null => {
const [{ actionType = type }] = useStore();
- if (!isActionViewType(actionType)) return null;
+ if (!isDefaultActionViewType(actionType)) return null;
return (
<>
- {actionType === 'CREATE_FOLDER' ? (
-
- ) : actionType === 'DELETE_FILES' ? (
-
- ) : actionType === 'COPY_FILES' ? (
-
+ {actionType === 'createFolder' ? (
+
+ ) : actionType === 'delete' ? (
+
+ ) : actionType === 'copy' ? (
+
) : (
-
+
)}
>
);
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx
index daae84c0e4b..951d0999776 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.tsx
@@ -1,14 +1,13 @@
import React from 'react';
-import { DescriptionList } from '../../../components/DescriptionList';
+import { STORAGE_BROWSER_BLOCK } from '../../../constants';
import { ViewElement } from '../../../context/elements';
-
import { ActionCancelControl } from '../../../controls/ActionCancelControl';
+import { ActionDestinationControl } from '../../../controls/ActionDestinationControl';
import { ActionExitControl } from '../../../controls/ActionExitControl';
import { ActionStartControl } from '../../../controls/ActionStartControl';
import { AddFilesControl } from '../../../controls/AddFilesControl';
import { AddFolderControl } from '../../../controls/AddFolderControl';
-import { ControlsContextProvider } from '../../../controls/context';
import { DataTableControl } from '../../../controls/DataTableControl';
import { DropZoneControl } from '../../../controls/DropZoneControl';
import { OverwriteToggleControl } from '../../../controls/OverwriteToggleControl';
@@ -16,116 +15,17 @@ import { MessageControl } from '../../../controls/MessageControl';
import { StatusDisplayControl } from '../../../controls/StatusDisplayControl';
import { TitleControl } from '../../../controls/TitleControl';
-import { useDisplayText } from '../../../displayText';
-import { STORAGE_BROWSER_BLOCK } from '../../../constants';
-import { resolveClassName } from '../../utils';
-import { getActionViewTableData } from '../getActionViewTableData';
+import { UploadViewProvider } from './UploadViewProvider';
+import { UploadViewType } from './types';
import { useUploadView } from './useUploadView';
-import { UploadViewProps } from './types';
-import { Breadcrumb } from '../../../components/BreadcrumbNavigation';
-import { LoadingIndicator } from '../../../composables/LoadingIndicator';
+import { classNames } from '@aws-amplify/ui';
-export function UploadView({
- className,
- ...props
-}: UploadViewProps): React.JSX.Element {
- const { UploadView: displayText } = useDisplayText();
- const {
- actionCancelLabel,
- actionDestinationLabel,
- actionExitLabel,
- actionStartLabel,
- addFilesLabel,
- addFolderLabel,
- loadingIndicatorLabel,
- statusDisplayCanceledLabel,
- statusDisplayCompletedLabel,
- statusDisplayFailedLabel,
- statusDisplayQueuedLabel,
- overwriteToggleLabel,
- title,
- getActionCompleteMessage,
- } = displayText;
-
- const {
- isOverwritingEnabled,
- isProcessing,
- isProcessingComplete,
- location,
- tasks,
- statusCounts,
- onActionStart,
- onActionCancel,
- onDropFiles,
- onActionExit,
- onTaskRemove,
- onSelectFiles,
- onToggleOverwrite,
- } = useUploadView(props);
-
- const loadingIndicator = (
-
- );
-
- const isActionStartDisabled =
- isProcessing || isProcessingComplete || statusCounts.TOTAL === 0;
- const isActionCancelDisabled = !isProcessing || isProcessingComplete;
- const isAddFilesDisabled = isProcessing || isProcessingComplete;
- const isAddFolderDisabled = isProcessing || isProcessingComplete;
- const isActionExitDisabled = isProcessing;
- const destinationList = (location.key || '/').split('/');
-
- const message = isProcessingComplete
- ? getActionCompleteMessage({
- counts: statusCounts,
- })
- : undefined;
+export const UploadView: UploadViewType = ({ className, ...props }) => {
+ const state = useUploadView(props);
return (
-
-
{
- onSelectFiles('FILE');
- }}
- onAddFolder={() => {
- onSelectFiles('FOLDER');
- }}
- onDropFiles={onDropFiles}
- onToggleOverwrite={onToggleOverwrite}
- >
+
+
@@ -139,25 +39,7 @@ export function UploadView({
-
- {destinationList.map((key, index) => (
-
- ))}
- >
- ),
- },
- ]}
- />
+
@@ -169,7 +51,24 @@ export function UploadView({
-
-
+
+
);
-}
+};
+
+UploadView.displayName = 'UploadView';
+
+UploadView.Provider = UploadViewProvider;
+
+UploadView.AddFiles = AddFilesControl;
+UploadView.AddFolder = AddFolderControl;
+UploadView.Cancel = ActionCancelControl;
+UploadView.Destination = ActionDestinationControl;
+UploadView.DropZone = DropZoneControl;
+UploadView.Exit = ActionExitControl;
+UploadView.Message = MessageControl;
+UploadView.OverwriteToggle = OverwriteToggleControl;
+UploadView.Start = ActionStartControl;
+UploadView.Statuses = StatusDisplayControl;
+UploadView.TasksTable = DataTableControl;
+UploadView.Title = TitleControl;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.tsx
new file mode 100644
index 00000000000..2a4cb52b15f
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+
+import { ControlsContextProvider } from '../../../controls/context';
+import { useDisplayText } from '../../../displayText';
+
+import { getActionViewTableData } from '../getActionViewTableData';
+
+import { UploadViewProviderProps } from './types';
+
+export function UploadViewProvider({
+ children,
+ ...props
+}: UploadViewProviderProps): React.JSX.Element {
+ const { UploadView: displayText } = useDisplayText();
+ const {
+ actionCancelLabel,
+ actionDestinationLabel,
+ actionExitLabel,
+ actionStartLabel,
+ addFilesLabel,
+ addFolderLabel,
+ statusDisplayCanceledLabel,
+ statusDisplayCompletedLabel,
+ statusDisplayFailedLabel,
+ statusDisplayQueuedLabel,
+ overwriteToggleLabel,
+ title,
+ getActionCompleteMessage,
+ getFilesValidationMessage,
+ } = displayText;
+
+ const {
+ isOverwritingEnabled,
+ isProcessing,
+ isProcessingComplete,
+ location,
+ tasks,
+ statusCounts,
+ invalidFiles,
+ onActionStart,
+ onActionCancel,
+ onDropFiles,
+ onActionExit,
+ onTaskRemove,
+ onSelectFiles,
+ onToggleOverwrite,
+ } = props;
+
+ const isActionStartDisabled =
+ isProcessing || isProcessingComplete || statusCounts.TOTAL === 0;
+ const isActionCancelDisabled = !isProcessing || isProcessingComplete;
+ const isAddFilesDisabled = isProcessing || isProcessingComplete;
+ const isAddFolderDisabled = isProcessing || isProcessingComplete;
+ const isActionExitDisabled = isProcessing;
+
+ const actionCompleteMessage = isProcessingComplete
+ ? getActionCompleteMessage({
+ counts: statusCounts,
+ })
+ : undefined;
+ const filesValidationMessage =
+ invalidFiles && !isProcessing
+ ? getFilesValidationMessage({ invalidFiles })
+ : undefined;
+
+ return (
+ {
+ onSelectFiles('FILE');
+ }}
+ onAddFolder={() => {
+ onSelectFiles('FOLDER');
+ }}
+ onDropFiles={onDropFiles}
+ onToggleOverwrite={onToggleOverwrite}
+ >
+ {children}
+
+ );
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx
index 099a8ed67da..c5e81daef5a 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/UploadView.spec.tsx
@@ -10,7 +10,10 @@ import { UploadView } from '../UploadView';
jest.mock('../../../../displayText', () => ({
useDisplayText: () => ({
- UploadView: { getActionCompleteMessage: jest.fn() },
+ UploadView: {
+ getActionCompleteMessage: jest.fn(),
+ getFilesValidationMessage: jest.fn(),
+ },
}),
}));
@@ -42,6 +45,11 @@ const statusCounts = { ...INITIAL_STATUS_COUNTS };
const testFile = new File([], 'test-ooo');
const data = { id: 'some-uuid', file: testFile, key: testFile.name };
+const invalidFileData = {
+ file: new File([], 'very-big-file'),
+ id: 'uuid',
+ key: 'very-big-file',
+};
const taskOne = {
data,
@@ -67,6 +75,7 @@ const initialViewState: UploadViewState = {
isProcessingComplete: false,
isProcessing: false,
tasks: [],
+ invalidFiles: undefined,
statusCounts,
};
@@ -74,6 +83,7 @@ const preprocessingViewState: UploadViewState = {
...initialViewState,
tasks: [taskOne],
statusCounts: { ...statusCounts, QUEUED: 1, TOTAL: 1 },
+ invalidFiles: [invalidFileData],
};
const processingViewState: UploadViewState = {
@@ -97,6 +107,21 @@ const useUploadViewSpy = jest
describe('UploadView', () => {
afterEach(jest.clearAllMocks);
+ it('has the expected composable components', () => {
+ expect(UploadView.AddFiles).toBeDefined();
+ expect(UploadView.AddFolder).toBeDefined();
+ expect(UploadView.Cancel).toBeDefined();
+ expect(UploadView.Destination).toBeDefined();
+ expect(UploadView.DropZone).toBeDefined();
+ expect(UploadView.Exit).toBeDefined();
+ expect(UploadView.Message).toBeDefined();
+ expect(UploadView.OverwriteToggle).toBeDefined();
+ expect(UploadView.Start).toBeDefined();
+ expect(UploadView.Statuses).toBeDefined();
+ expect(UploadView.TasksTable).toBeDefined();
+ expect(UploadView.Title).toBeDefined();
+ });
+
it('provides the expected boolean flags to `ControlsContextProvider` prior to processing when tasks is empty', () => {
render();
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts
index 63883f432a0..cbb2f680723 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts
@@ -4,6 +4,7 @@ import { LocationData } from '../../../../actions';
import * as ConfigModule from '../../../../providers/configuration';
import * as StoreModule from '../../../../providers/store';
import * as TasksModule from '../../../../tasks';
+import { UPLOAD_FILE_SIZE_LIMIT } from '../../../../validators/isFileTooBig';
const useStoreSpy = jest.spyOn(StoreModule, 'useStore');
@@ -16,13 +17,12 @@ const rootLocation: LocationData = {
type: 'BUCKET',
};
+const mockUserStoreState = {
+ location: { current: rootLocation, path: '', key: '' },
+ files: undefined,
+} as StoreModule.UseStoreState;
const dispatchStoreAction = jest.fn();
-useStoreSpy.mockReturnValue([
- {
- location: { current: rootLocation, path: '', key: '' },
- } as StoreModule.UseStoreState,
- dispatchStoreAction,
-]);
+useStoreSpy.mockReturnValue([mockUserStoreState, dispatchStoreAction]);
const credentials = jest.fn();
const config: ConfigModule.GetActionInput = jest.fn(() => ({
@@ -43,6 +43,15 @@ const fileItemTwo = {
file: testFileTwo,
key: testFileTwo.name,
};
+const invalidFile = {
+ ...new File([], 'invalid-file'),
+ size: UPLOAD_FILE_SIZE_LIMIT + 1,
+};
+const invalidFileItem = {
+ id: 'invalid-file-uuid',
+ file: invalidFile,
+ key: invalidFile.name,
+};
jest.spyOn(ConfigModule, 'useGetActionInput').mockReturnValue(config);
const handleProcessTasks = jest.fn();
@@ -77,6 +86,7 @@ const useProcessTasksSpy = jest
describe('useUploadView', () => {
afterEach(() => {
+ mockUserStoreState.files = undefined;
jest.clearAllMocks();
});
@@ -94,6 +104,13 @@ describe('useUploadView', () => {
});
});
+ it('should show invalid files if exists', () => {
+ mockUserStoreState.files = [invalidFileItem];
+ const { result } = renderHook(() => useUploadView());
+
+ expect(result.current.invalidFiles).toEqual([invalidFileItem]);
+ });
+
it('should dispatchStoreAction when onSelectFiles is invoked with different types', () => {
const { result } = renderHook(() => useUploadView());
@@ -119,6 +136,19 @@ describe('useUploadView', () => {
});
it('should call handleProcessTasks with the expected values', () => {
+ mockUserStoreState.files = [invalidFileItem];
+ const { result } = renderHook(() => useUploadView());
+ act(() => {
+ result.current.onActionStart();
+ });
+ expect(dispatchStoreAction).toHaveBeenCalledTimes(1);
+ expect(dispatchStoreAction).toHaveBeenCalledWith({
+ type: 'REMOVE_FILE_ITEM',
+ id: invalidFileItem.id,
+ });
+ });
+
+ it('should remove any invalid files action is started', () => {
const { result } = renderHook(() => useUploadView());
act(() => {
result.current.onActionStart();
@@ -134,6 +164,7 @@ describe('useUploadView', () => {
destinationPrefix: '',
});
});
+
it('should call cancel on each pending task when onCancel is invoked', () => {
const tasks: TasksModule.Task[] = [
{ ...taskOne, status: 'PENDING' },
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts
index 167c9669153..c6a08fa7c35 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/types.ts
@@ -1,24 +1,39 @@
import { LocationData, UploadHandlerData } from '../../../actions';
-import { FileItem } from '../../../providers';
-import {
- ActionViewComponent,
- ActionViewProps,
- ActionViewState,
-} from '../types';
+import { FileItem, FileItems } from '../../../providers';
+import { ActionViewType, ActionViewProps, ActionViewState } from '../types';
export interface UploadViewState extends ActionViewState {
isOverwritingEnabled: boolean;
onDropFiles: (files: File[]) => void;
onSelectFiles: (type: 'FILE' | 'FOLDER') => void;
onToggleOverwrite: () => void;
+ invalidFiles: FileItems | undefined;
}
export interface UploadViewProps
extends ActionViewProps,
Partial {}
-export interface UploadViewComponent
- extends ActionViewComponent {}
+export interface UploadViewProviderProps extends UploadViewState {
+ children?: React.ReactNode;
+}
+
+export interface UploadViewType
+ extends ActionViewType {
+ Provider: (props: UploadViewProviderProps) => React.JSX.Element;
+ AddFiles: () => React.JSX.Element | null;
+ AddFolder: () => React.JSX.Element | null;
+ Cancel: () => React.JSX.Element | null;
+ DropZone: (props: { children: React.ReactNode }) => React.JSX.Element | null;
+ Destination: () => React.JSX.Element | null;
+ Exit: () => React.JSX.Element | null;
+ Message: () => React.JSX.Element | null;
+ OverwriteToggle: () => React.JSX.Element | null;
+ Start: () => React.JSX.Element | null;
+ Statuses: () => React.JSX.Element | null;
+ TasksTable: () => React.JSX.Element | null;
+ Title: () => React.JSX.Element | null;
+}
export interface UseUploadViewOptions {
onExit?: (location?: LocationData) => void;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts
index 674fa9cb4a3..1d7e6313b0c 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.ts
@@ -3,12 +3,14 @@ import React from 'react';
import { uploadHandler } from '../../../actions';
import { useGetActionInput } from '../../../providers/configuration';
-import { useStore } from '../../../providers/store';
+import { FileItems, useStore } from '../../../providers/store';
import { Task, useProcessTasks } from '../../../tasks';
import { DEFAULT_ACTION_CONCURRENCY } from '../constants';
import { UploadViewState, UseUploadViewOptions } from './types';
import { DEFAULT_OVERWRITE_ENABLED } from './constants';
+import { isUndefined } from '@aws-amplify/ui';
+import { isFileTooBig } from '../../../validators';
export const useUploadView = (
options?: UseUploadViewOptions
@@ -18,6 +20,30 @@ export const useUploadView = (
const [{ files, location }, dispatchStoreAction] = useStore();
const { current, key } = location;
+ const { invalidFiles, validFiles } = React.useMemo(
+ () =>
+ (files ?? [])?.reduce(
+ (curr, file) => {
+ if (isFileTooBig(file.file)) {
+ curr.invalidFiles = isUndefined(curr.invalidFiles)
+ ? [file]
+ : curr.invalidFiles.concat(file);
+ } else {
+ curr.validFiles = isUndefined(curr.validFiles)
+ ? [file]
+ : curr.validFiles.concat(file);
+ }
+
+ return curr;
+ },
+ {} as {
+ invalidFiles: FileItems | undefined;
+ validFiles: FileItems | undefined;
+ }
+ ),
+ [files]
+ );
+
const [isOverwritingEnabled, setIsOverwritingEnabled] = React.useState(
DEFAULT_OVERWRITE_ENABLED
);
@@ -25,7 +51,7 @@ export const useUploadView = (
const [
{ isProcessing, isProcessingComplete, statusCounts, tasks },
handleProcess,
- ] = useProcessTasks(uploadHandler, files, {
+ ] = useProcessTasks(uploadHandler, validFiles, {
concurrency: DEFAULT_ACTION_CONCURRENCY,
});
@@ -46,12 +72,23 @@ export const useUploadView = (
);
const onActionStart = React.useCallback(() => {
+ invalidFiles?.forEach((file) => {
+ dispatchStoreAction({ type: 'REMOVE_FILE_ITEM', id: file.id });
+ });
+
handleProcess({
config: getInput(),
destinationPrefix: key,
options: { preventOverwrite: !isOverwritingEnabled },
});
- }, [isOverwritingEnabled, key, getInput, handleProcess]);
+ }, [
+ isOverwritingEnabled,
+ key,
+ getInput,
+ handleProcess,
+ invalidFiles,
+ dispatchStoreAction,
+ ]);
const onActionCancel = React.useCallback(() => {
tasks.forEach((task) => task.cancel?.());
@@ -81,6 +118,7 @@ export const useUploadView = (
isProcessingComplete,
isOverwritingEnabled,
location,
+ invalidFiles,
statusCounts,
tasks,
onActionCancel,
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/LocationActionView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/LocationActionView.spec.tsx
index 46e5e28b2a4..2b266e33106 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/LocationActionView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/LocationActionView.spec.tsx
@@ -42,27 +42,22 @@ describe('LocationActionView', () => {
it.each([
{
view: 'CreateFolderView',
- actionType: 'CREATE_FOLDER',
+ actionType: 'createFolder',
testId: 'create-folder-view',
},
{
view: 'CopyView',
- actionType: 'COPY_FILES',
+ actionType: 'copy',
testId: 'copy-view',
},
{
view: 'DeleteView',
- actionType: 'DELETE_FILES',
+ actionType: 'delete',
testId: 'delete-view',
},
{
view: 'UploadView',
- actionType: 'UPLOAD_FILES',
- testId: 'upload-view',
- },
- {
- view: 'UploadView',
- actionType: 'UPLOAD_FOLDER',
+ actionType: 'upload',
testId: 'upload-view',
},
])(
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/getPercentValue.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/getPercentValue.spec.ts
new file mode 100644
index 00000000000..72d27ac1f15
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/__tests__/getPercentValue.spec.ts
@@ -0,0 +1,9 @@
+import { getPercentValue } from '../getPercentValue';
+
+describe('getPercentValue', () => {
+ it('calculates the percentage of a number', () => {
+ expect(getPercentValue(0.01)).toBe(1);
+ expect(getPercentValue(0.5)).toBe(50);
+ expect(getPercentValue(1)).toBe(100);
+ });
+});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getActionViewTableData.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getActionViewTableData.ts
index 5d0970f85a9..7d727005b2d 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getActionViewTableData.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getActionViewTableData.ts
@@ -6,7 +6,7 @@ import { Task, TaskStatus } from '../../tasks';
import { isFileItem, isFileDataItem, TaskData } from '../../actions';
import { getActionIcon } from './getActionIcon';
import { getFileTypeDisplayValue } from './getFileTypeDisplayValue';
-import { getPercentValue } from '../utils';
+import { getPercentValue } from './getPercentValue';
import { getDefaultActionViewHeaders } from './getDefaultActionViewHeaders';
import { ActionViewHeaders } from './types';
import { DefaultActionViewDisplayText } from '../../displayText/types';
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getPercentValue.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getPercentValue.ts
new file mode 100644
index 00000000000..4a6588ba881
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/getPercentValue.ts
@@ -0,0 +1,2 @@
+export const getPercentValue = (value: number): number =>
+ Math.round(value * 100);
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 98fe0f09e0a..ca7f5952ca4 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/index.ts
@@ -1,7 +1,22 @@
-export { CopyViewState, useCopyView } from './CopyView';
-export { CreateFolderViewState, useCreateFolderView } from './CreateFolderView';
-export { DeleteViewState, useDeleteView } from './DeleteView';
-export { UploadViewState, useUploadView } from './UploadView';
+export { CopyView, CopyViewType, CopyViewState, useCopyView } from './CopyView';
+export {
+ CreateFolderView,
+ CreateFolderViewType,
+ CreateFolderViewState,
+ useCreateFolderView,
+} from './CreateFolderView';
+export {
+ DeleteView,
+ DeleteViewType,
+ DeleteViewState,
+ useDeleteView,
+} from './DeleteView';
+export {
+ UploadView,
+ UploadViewType,
+ UploadViewState,
+ useUploadView,
+} from './UploadView';
export { useActionView } from './useActionView';
export {
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/types.ts
index 32b76ff9331..860ad3a0f4b 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/types.ts
@@ -40,23 +40,16 @@ export interface LocationActionViewProps<
type?: T;
}
-export type LocationActionViewComponent<
+export type LocationActionViewType<
T = string,
K extends TaskData = TaskData,
> = (props: LocationActionViewProps) => React.JSX.Element | null;
-export interface ActionViewComponent {
+export interface ActionViewType {
(
props: ActionViewProps & Partial> & K
): React.JSX.Element | null;
displayName: string;
- Cancel: () => React.JSX.Element | null;
- Destination: () => React.JSX.Element | null;
- Exit: () => React.JSX.Element | null;
- Start: () => React.JSX.Element | null;
- StatusDisplay: () => React.JSX.Element | null;
- Table: () => React.JSX.Element | null;
- Title: () => React.JSX.Element | null;
}
// Custom actions derived views
@@ -65,7 +58,7 @@ export type DerivedActionViews = {
? never
: T[K] extends { componentName: ComponentName }
? T[K]['componentName']
- : never]: ActionViewComponent<
+ : never]: ActionViewType<
T[K] extends TaskActionConfig>>
? X
: never
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailView.tsx
index 97cd3fff4f8..fd64dcb26eb 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailView.tsx
@@ -1,27 +1,26 @@
import React from 'react';
+import {
+ STORAGE_BROWSER_BLOCK,
+ STORAGE_BROWSER_BLOCK_TO_BE_UPDATED,
+} from '../../constants';
import { ViewElement } from '../../context/elements';
import { ActionsListControl } from '../../controls/ActionsListControl';
import { DataTableControl } from '../../controls/DataTableControl';
import { DataRefreshControl } from '../../controls/DataRefreshControl';
import { DropZoneControl } from '../../controls/DropZoneControl';
+import { LoadingIndicatorControl } from '../../controls/LoadingIndicatorControl';
import { MessageControl } from '../../controls/MessageControl';
import { NavigationControl } from '../../controls/NavigationControl';
import { PaginationControl } from '../../controls/PaginationControl';
import { SearchControl } from '../../controls/SearchControl';
import { SearchSubfoldersToggleControl } from '../../controls/SearchSubfoldersToggleControl';
import { TitleControl } from '../../controls/TitleControl';
-import { ControlsContextProvider } from '../../controls/context';
-import { useDisplayText } from '../../displayText';
-import {
- STORAGE_BROWSER_BLOCK,
- STORAGE_BROWSER_BLOCK_TO_BE_UPDATED,
-} from '../../constants';
-import { resolveClassName } from '../utils';
-import { getLocationDetailViewTableData } from './getLocationDetailViewTableData';
+
+import { LocationDetailViewType } from './types';
import { useLocationDetailView } from './useLocationDetailView';
-import { LocationDetailViewProps } from './types';
-import { LoadingIndicator } from '../../composables/LoadingIndicator';
+import { LocationDetailViewProvider } from './LocationDetailViewProvider';
+import { classNames } from '@aws-amplify/ui';
const DEFAULT_PAGE_SIZE = 100;
export const DEFAULT_LIST_OPTIONS = {
@@ -29,123 +28,20 @@ export const DEFAULT_LIST_OPTIONS = {
delimiter: '/',
};
-export function LocationDetailView({
+export const LocationDetailView: LocationDetailViewType = ({
className,
...props
-}: LocationDetailViewProps): React.JSX.Element {
- const {
- LocationDetailView: {
- loadingIndicatorLabel,
- searchSubfoldersToggleLabel,
- selectFileLabel,
- selectAllFilesLabel,
- searchPlaceholder,
- searchSubmitLabel,
- searchClearLabel,
- getTitle,
- getListItemsResultMessage,
- },
- } = useDisplayText();
-
- const loadingIndicator = (
-
- );
-
- const {
- actions,
- page,
- pageItems,
- hasNextPage,
- highestPageVisited,
- isLoading,
- isSearchingSubfolders,
- location,
- areAllFilesSelected,
- fileDataItems,
- hasFiles,
- hasError,
- hasDownloadError,
- message,
- downloadErrorMessage,
- searchQuery,
- hasExhaustedSearch,
- onActionSelect,
- onDropFiles,
- onRefresh,
- onPaginate,
- onDownload,
- onNavigate,
- onNavigateHome,
- onSelect,
- onSelectAll,
- onSearch,
- onSearchQueryChange,
- onSearchClear,
- onToggleSearchSubfolders,
- } = useLocationDetailView(props);
-
- const messageControlContent = getListItemsResultMessage({
- items: pageItems,
- hasError: hasError || hasDownloadError,
- hasExhaustedSearch,
- message: hasError ? message : downloadErrorMessage,
- });
+}) => {
+ const state = useLocationDetailView(props);
+ const { hasError } = state;
return (
-
-
-
+
+
-
+
-
-
+
+
{hasError ? null : (
@@ -171,10 +61,27 @@ export function LocationDetailView({
)}
-
+
+
-
-
+
+
);
-}
+};
+
+LocationDetailView.displayName = 'LocationDetailView';
+
+LocationDetailView.Provider = LocationDetailViewProvider;
+
+LocationDetailView.ActionsList = ActionsListControl;
+LocationDetailView.DropZone = DropZoneControl;
+LocationDetailView.LoadingIndicator = LoadingIndicatorControl;
+LocationDetailView.LocationItemsTable = DataTableControl;
+LocationDetailView.Message = MessageControl;
+LocationDetailView.Navigation = NavigationControl;
+LocationDetailView.Pagination = PaginationControl;
+LocationDetailView.Refresh = DataRefreshControl;
+LocationDetailView.Search = SearchControl;
+LocationDetailView.SearchSubfoldersToggle = SearchSubfoldersToggleControl;
+LocationDetailView.Title = TitleControl;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailViewProvider.tsx
new file mode 100644
index 00000000000..53616d138c1
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/LocationDetailViewProvider.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+
+import { ControlsContextProvider } from '../../controls/context';
+import { useDisplayText } from '../../displayText';
+
+import { LocationDetailViewProviderProps } from './types';
+import { getLocationDetailViewTableData } from './getLocationDetailViewTableData';
+
+export function LocationDetailViewProvider({
+ children,
+ ...props
+}: LocationDetailViewProviderProps): React.JSX.Element {
+ const {
+ LocationDetailView: {
+ loadingIndicatorLabel,
+ searchSubfoldersToggleLabel,
+ selectFileLabel,
+ selectAllFilesLabel,
+ searchPlaceholder,
+ searchSubmitLabel,
+ searchClearLabel,
+ getTitle,
+ getListItemsResultMessage,
+ },
+ } = useDisplayText();
+
+ const {
+ actions,
+ page,
+ pageItems,
+ hasNextPage,
+ highestPageVisited,
+ isLoading,
+ isSearchingSubfolders,
+ location,
+ areAllFilesSelected,
+ fileDataItems,
+ hasFiles,
+ hasError,
+ hasDownloadError,
+ message,
+ downloadErrorMessage,
+ searchQuery,
+ hasExhaustedSearch,
+ onActionSelect,
+ onDropFiles,
+ onRefresh,
+ onPaginate,
+ onDownload,
+ onNavigate,
+ onNavigateHome,
+ onSelect,
+ onSelectAll,
+ onSearch,
+ onSearchQueryChange,
+ onSearchClear,
+ onToggleSearchSubfolders,
+ } = props;
+
+ const messageControlContent = getListItemsResultMessage({
+ isLoading,
+ items: pageItems,
+ hasError: hasError || hasDownloadError,
+ hasExhaustedSearch,
+ message: hasError ? message : downloadErrorMessage,
+ });
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx
index 0c84da29c19..299379e96fd 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/LocationDetailView.spec.tsx
@@ -16,13 +16,7 @@ import {
import { useProcessTasks } from '../../../tasks/useProcessTasks';
import { INITIAL_STATUS_COUNTS } from '../../../tasks';
import { useDisplayText } from '../../../displayText';
-import { SearchOutput } from '../../../actions/createEnhancedListHandler';
-
-// FIXME: Temporarily mock... 😎 temp actions hook
-import { useTempActions } from '../../../do-not-import-from-here/createTempActionsProvider';
-jest.mock('../../../do-not-import-from-here/createTempActionsProvider');
-const mockUseTempActions = useTempActions as jest.Mock;
-mockUseTempActions.mockReturnValue({});
+import { SearchOutput } from '../../../actions/useAction/createEnhancedListHandler';
jest.mock('../../../displayText', () => {
const mockGetListItemsResultMessage = jest.fn();
@@ -160,6 +154,37 @@ describe('LocationDetailView', () => {
jest.clearAllMocks();
});
+ it('has the expected composable components', () => {
+ expect(LocationDetailView.ActionsList).toBeDefined();
+ expect(LocationDetailView.DropZone).toBeDefined();
+ expect(LocationDetailView.LoadingIndicator).toBeDefined();
+ expect(LocationDetailView.LocationItemsTable).toBeDefined();
+ expect(LocationDetailView.Message).toBeDefined();
+ expect(LocationDetailView.Navigation).toBeDefined();
+ expect(LocationDetailView.Pagination).toBeDefined();
+ expect(LocationDetailView.Refresh).toBeDefined();
+ expect(LocationDetailView.Search).toBeDefined();
+ expect(LocationDetailView.SearchSubfoldersToggle).toBeDefined();
+ expect(LocationDetailView.Title).toBeDefined();
+ });
+
+ it('shows a Loading element when first loaded', () => {
+ useStoreSpy.mockReturnValueOnce([
+ {
+ location: { current: location, path: '', key: location.prefix },
+ locationItems: { fileDataItems: undefined },
+ } as StoreModule.UseStoreState,
+ dispatchStoreAction,
+ ]);
+ mockListItemsAction({ isLoading: true, result: [] });
+
+ const { getByTestId } = render();
+
+ const loadingIndicator = getByTestId('loading-indicator-control');
+
+ expect(loadingIndicator).toBeInTheDocument();
+ });
+
it('invokes getListItemsResultMessage() with `errorMessage` param', () => {
const errorMessage = 'A network error occurred.';
@@ -175,12 +200,30 @@ describe('LocationDetailView', () => {
expect(mockGetListItemsResultMessage).toHaveBeenCalledWith({
items: expect.any(Array),
+ isLoading: false,
hasError: true,
message: errorMessage,
hasExhaustedSearch: false,
});
});
+ it('invokes getListItemsResultMessage() with `isLoading` param', () => {
+ mockListItemsAction({
+ isLoading: true,
+ hasError: false,
+ result: [],
+ });
+
+ render();
+
+ expect(mockGetListItemsResultMessage).toHaveBeenCalledWith({
+ items: [],
+ isLoading: true,
+ hasError: false,
+ hasExhaustedSearch: false,
+ });
+ });
+
it('invokes getListItemsResultMessage() with expected params when there is a download error', () => {
mockUseProcessTasks.mockReturnValueOnce([
{
@@ -214,6 +257,7 @@ describe('LocationDetailView', () => {
expect(mockGetListItemsResultMessage).toHaveBeenCalledWith({
items: expect.any(Array),
hasError: true,
+ isLoading: false,
message: 'Failed to download test-key due to error: NotFound.',
hasExhaustedSearch: false,
});
@@ -296,6 +340,7 @@ describe('LocationDetailView', () => {
expect(mockGetListItemsResultMessage).toHaveBeenCalledWith({
items: expect.any(Array),
hasExhaustedSearch: true,
+ isLoading: false,
hasError: false,
message: undefined,
});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/useLocationDetailView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/useLocationDetailView.spec.tsx
index 60f453be34e..0d9a6d53f80 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/useLocationDetailView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/__tests__/useLocationDetailView.spec.tsx
@@ -21,12 +21,6 @@ import {
DEFAULT_LIST_OPTIONS,
} from '../useLocationDetailView';
-// FIXME: Temporarily mock... 😎 temp actions hook
-import { useTempActions } from '../../../do-not-import-from-here/createTempActionsProvider';
-jest.mock('../../../do-not-import-from-here/createTempActionsProvider');
-const mockUseTempActions = useTempActions as jest.Mock;
-mockUseTempActions.mockReturnValue({});
-
const useDataStateSpy = jest.spyOn(AmplifyReactCore, 'useDataState');
const useStoreSpy = jest.spyOn(StoreModule, 'useStore');
const useGetActionSpy = jest.spyOn(ConfigModule, 'useGetActionInput');
@@ -505,7 +499,7 @@ describe('useLocationDetailView', () => {
});
expect(mockDispatchStoreAction).toHaveBeenCalledWith({
type: 'SET_ACTION_TYPE',
- actionType: 'UPLOAD_FILES',
+ actionType: 'upload',
});
});
@@ -528,7 +522,7 @@ describe('useLocationDetailView', () => {
});
expect(mockDispatchStoreAction).toHaveBeenCalledWith({
type: 'SET_ACTION_TYPE',
- actionType: 'UPLOAD_FOLDER',
+ actionType: 'upload',
});
});
@@ -554,7 +548,7 @@ describe('useLocationDetailView', () => {
});
expect(mockDispatchStoreAction).toHaveBeenCalledWith({
type: 'SET_ACTION_TYPE',
- actionType: 'UPLOAD_FILES',
+ actionType: 'upload',
});
});
@@ -661,7 +655,6 @@ describe('useLocationDetailView', () => {
const mockOnActionSelect = jest.fn();
const actionType = 'action-type';
useStoreSpy.mockReturnValue([testStoreState, mockDispatchStoreAction]);
- mockUseTempActions.mockReturnValueOnce({});
const { result } = renderHook(() =>
useLocationDetailView({ onActionSelect: mockOnActionSelect })
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/__tests__/getLocationDetailViewTableData.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/__tests__/getLocationDetailViewTableData.spec.ts
index c0e0155e43b..e8d2ee59ba2 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/__tests__/getLocationDetailViewTableData.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/__tests__/getLocationDetailViewTableData.spec.ts
@@ -111,7 +111,7 @@ describe('getLocationDetailViewTableData', () => {
}),
expect.objectContaining({ content: { label: 'Name' } }),
expect.objectContaining({ content: { label: 'Type' } }),
- expect.objectContaining({ content: { label: 'Last Modified' } }),
+ expect.objectContaining({ content: { label: 'Last modified' } }),
expect.objectContaining({ content: { label: 'Size' } }),
expect.objectContaining({ content: { text: '' } }),
],
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/constants.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/constants.ts
index c61327d023d..f18a883fa87 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/constants.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/constants.ts
@@ -4,7 +4,7 @@ export const LOCATION_DETAIL_VIEW_HEADERS: LocationDetailViewHeaders = [
{ key: 'checkbox', type: 'text', content: { text: '' } },
{ key: 'name', type: 'sort', content: { label: 'Name' } },
{ key: 'type', type: 'sort', content: { label: 'Type' } },
- { key: 'last-modified', type: 'sort', content: { label: 'Last Modified' } },
+ { key: 'last-modified', type: 'sort', content: { label: 'Last modified' } },
{ key: 'size', type: 'sort', content: { label: 'Size' } },
{ key: 'download', type: 'text', content: { text: '' } },
];
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/types.ts
index ad6f9c299e3..3074020d7ed 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/types.ts
@@ -1,6 +1,88 @@
+import {
+ FileData,
+ FileDataItem,
+ LocationData,
+ LocationItemData,
+} from '../../actions';
+import { ActionsListItem } from '../../composables/ActionsList';
+import { LocationState } from '../../providers/store/location';
+
import { ListViewProps } from '../types';
+export interface LocationDetailViewState {
+ actions: ActionsListItem[];
+ hasError: boolean;
+ hasNextPage: boolean;
+ hasDownloadError: boolean;
+ highestPageVisited: number;
+ isLoading: boolean;
+ isSearchingSubfolders: boolean;
+ location: LocationState;
+ areAllFilesSelected: boolean;
+ fileDataItems: FileDataItem[] | undefined;
+ hasFiles: boolean;
+ message: string | undefined;
+ downloadErrorMessage: string | undefined;
+ shouldShowEmptyMessage: boolean;
+ searchQuery: string;
+ hasExhaustedSearch: boolean;
+ pageItems: LocationItemData[];
+ page: number;
+ onActionSelect: (actionType: string) => void;
+ onDropFiles: (files: File[]) => void;
+ onRefresh: () => void;
+ onNavigate: (location: LocationData, path?: string) => void;
+ onNavigateHome: () => void;
+ onPaginate: (page: number) => void;
+ onDownload: (fileItem: FileDataItem) => void;
+ onSelect: (isSelected: boolean, fileItem: FileData) => void;
+ onSelectAll: () => void;
+ onSearch: () => void;
+ onSearchClear: () => void;
+ onSearchQueryChange: (value: string) => void;
+ onToggleSearchSubfolders: () => void;
+}
+
export interface LocationDetailViewProps extends ListViewProps {
onActionSelect?: (type: string) => void;
onExit?: () => void;
}
+
+export interface LocationDetailViewProviderProps
+ extends LocationDetailViewState {
+ children?: React.ReactNode;
+}
+
+export interface LocationDetailViewType {
+ (
+ props: {
+ children?: React.ReactNode;
+ className?: string;
+ } & LocationDetailViewProps
+ ): React.JSX.Element | null;
+ displayName: string;
+ Provider: (props: LocationDetailViewProviderProps) => React.JSX.Element;
+ ActionsList: () => React.JSX.Element | null;
+ DropZone: (props: { children: React.ReactNode }) => React.JSX.Element | null;
+ LoadingIndicator: () => React.JSX.Element | null;
+ LocationItemsTable: () => React.JSX.Element | null;
+ Message: () => React.JSX.Element | null;
+ Navigation: () => React.JSX.Element | null;
+ Pagination: () => React.JSX.Element | null;
+ Refresh: () => React.JSX.Element | null;
+ Search: () => React.JSX.Element | null;
+ SearchSubfoldersToggle: () => React.JSX.Element | null;
+ Title: () => React.JSX.Element | null;
+}
+
+interface InitialValues {
+ pageSize?: number;
+ delimiter?: string;
+}
+
+export interface UseLocationDetailViewOptions {
+ initialValues?: InitialValues;
+ onActionSelect?: (actionType: string) => void;
+ onExit?: () => void;
+ onNavigate?: (location: LocationData, path?: string) => void;
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.ts
index a2d572cbdb6..1ef69380f67 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.ts
@@ -8,80 +8,20 @@ import { useStore } from '../../providers/store';
import {
FileData,
LocationData,
- LocationItemData,
listLocationItemsHandler,
} from '../../actions';
-import { isFile } from '../utils';
-import { createEnhancedListHandler } from '../../actions/createEnhancedListHandler';
+import { createEnhancedListHandler } from '../../actions/useAction/createEnhancedListHandler';
import { useGetActionInput } from '../../providers/configuration';
-import { LocationState } from '../../providers/store/location';
import { useSearch } from '../hooks/useSearch';
-import { ActionsListItem } from '../../composables/ActionsList';
-import { useTempActions } from '../../do-not-import-from-here/createTempActionsProvider';
-import { IconVariant } from '../../context/elements';
-import { toAccessGrantPermission } from '../../adapters/permissionParsers';
+
import { Tasks, useProcessTasks } from '../../tasks';
import {
downloadHandler,
DownloadHandlerData,
FileDataItem,
-} from '../../actions/handlers';
-
-interface UseLocationDetailView {
- actions: ActionsListItem[];
- hasError: boolean;
- hasNextPage: boolean;
- hasDownloadError: boolean;
- highestPageVisited: number;
- isLoading: boolean;
- isSearchingSubfolders: boolean;
- location: LocationState;
- areAllFilesSelected: boolean;
- fileDataItems: FileDataItem[] | undefined;
- hasFiles: boolean;
- message: string | undefined;
- downloadErrorMessage: string | undefined;
- shouldShowEmptyMessage: boolean;
- searchQuery: string;
- hasExhaustedSearch: boolean;
- pageItems: LocationItemData[];
- page: number;
- onActionSelect: (actionType: string) => void;
- onDropFiles: (files: File[]) => void;
- onRefresh: () => void;
- onNavigate: (location: LocationData, path?: string) => void;
- onNavigateHome: () => void;
- onPaginate: (page: number) => void;
- onDownload: (fileItem: FileDataItem) => void;
- onSelect: (isSelected: boolean, fileItem: FileData) => void;
- onSelectAll: () => void;
- onSearch: () => void;
- onSearchClear: () => void;
- onSearchQueryChange: (value: string) => void;
- onToggleSearchSubfolders: () => void;
-}
-
-export type LocationDetailViewActionType =
- | { type: 'REFRESH_DATA' } // refresh data only
- | { type: 'RESET' } // reset view to initial state
- | { type: 'PAGINATE'; page: number }
- | { type: 'ACCESS_ITEM'; key: string }
- | { type: 'NAVIGATE'; location: LocationData; path: string }
- | { type: 'ADD_FILES'; files: File[] }
- | { type: 'SEARCH'; query: string; includeSubfolders?: boolean };
-
-interface InitialValues {
- pageSize?: number;
- delimiter?: string;
-}
-
-export interface UseLocationDetailViewOptions {
- initialValues?: InitialValues;
- onDispatch?: React.Dispatch;
- onActionSelect?: (actionType: string) => void;
- onExit?: () => void;
- onNavigate?: (location: LocationData, path?: string) => void;
-}
+ defaultActionViewConfigs,
+} from '../../actions';
+import { LocationDetailViewState, UseLocationDetailViewOptions } from './types';
const DEFAULT_PAGE_SIZE = 100;
export const DEFAULT_LIST_OPTIONS = {
@@ -105,9 +45,9 @@ const getDownloadErrorMessageFromFailedDownloadTask = (
} due to error: ${tasks[0].message}.`;
};
-export function useLocationDetailView(
+export const useLocationDetailView = (
options?: UseLocationDetailViewOptions
-): UseLocationDetailView {
+): LocationDetailViewState => {
const getConfig = useGetActionInput();
const { initialValues, onExit, onNavigate } = options ?? {};
@@ -118,8 +58,6 @@ export function useLocationDetailView(
const listOptions = listOptionsRef.current;
- const tempActions = useTempActions();
-
const [{ location, locationItems }, dispatchStoreAction] = useStore();
const { current, key } = location;
const { permissions, prefix } = current ?? {};
@@ -130,10 +68,7 @@ export function useLocationDetailView(
const [{ data, isLoading, hasError, message }, handleList] = useDataState(
listLocationItemsAction,
- {
- items: [],
- nextToken: undefined,
- }
+ { items: [], nextToken: undefined }
);
// set up pagination
@@ -228,26 +163,29 @@ export function useLocationDetailView(
const shouldShowEmptyMessage =
pageItems.length === 0 && !isLoading && !hasError;
- // FIXME: Temporarily get from... 😎 temp actions hook
const actions = React.useMemo(() => {
if (!permissions) {
return [];
}
- return Object.entries(tempActions).map(([actionType, { options }]) => {
- const { icon, hide, disable, displayName } = options ?? {};
- return {
- actionType,
- icon: (icon as { props: { variant: IconVariant } }).props.variant,
- isDisabled: isFunction(disable)
- ? disable(fileDataItems ?? [])
- : disable ?? false,
- isHidden: isFunction(hide)
- ? hide(toAccessGrantPermission(permissions))
- : hide,
- label: displayName,
- };
- });
- }, [fileDataItems, permissions, tempActions]);
+
+ return Object.entries(defaultActionViewConfigs).map(
+ ([actionType, config]) => {
+ const { actionsListItemConfig } = config ?? {};
+
+ const { icon, hide, disable, label } = actionsListItemConfig ?? {};
+
+ return {
+ actionType,
+ icon,
+ isDisabled: isFunction(disable)
+ ? disable(fileDataItems)
+ : disable ?? false,
+ isHidden: isFunction(hide) ? hide(permissions) : hide,
+ label,
+ };
+ }
+ );
+ }, [fileDataItems, permissions]);
return {
actions,
@@ -280,13 +218,9 @@ export function useLocationDetailView(
},
onDropFiles: (files: File[]) => {
dispatchStoreAction({ type: 'ADD_FILE_ITEMS', files });
- const actionType = files.some((file) => isFile(file))
- ? 'UPLOAD_FILES'
- : 'UPLOAD_FOLDER';
- dispatchStoreAction({
- type: 'SET_ACTION_TYPE',
- actionType,
- });
+
+ const actionType = 'upload';
+ dispatchStoreAction({ type: 'SET_ACTION_TYPE', actionType });
options?.onActionSelect?.(actionType);
},
onDownload: (data: FileDataItem) => {
@@ -337,4 +271,4 @@ export function useLocationDetailView(
onSearchQueryChange,
onToggleSearchSubfolders,
};
-}
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx
index 1fab2b24982..c149ef5d738 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsView.tsx
@@ -1,162 +1,33 @@
import React from 'react';
+import {
+ STORAGE_BROWSER_BLOCK,
+ STORAGE_BROWSER_BLOCK_TO_BE_UPDATED,
+} from '../../constants';
import { ViewElement } from '../../context/elements';
import { DataRefreshControl } from '../../controls/DataRefreshControl';
import { DataTableControl } from '../../controls/DataTableControl';
+import { LoadingIndicatorControl } from '../../controls/LoadingIndicatorControl';
+import { MessageControl } from '../../controls/MessageControl';
import { PaginationControl } from '../../controls/PaginationControl';
import { SearchControl } from '../../controls/SearchControl';
import { TitleControl } from '../../controls/TitleControl';
-import { ControlsContextProvider } from '../../controls/context';
-import { useDisplayText } from '../../displayText';
-import {
- STORAGE_BROWSER_BLOCK,
- STORAGE_BROWSER_BLOCK_TO_BE_UPDATED,
-} from '../../constants';
-import { resolveClassName } from '../utils';
-import { getLocationsViewTableData } from './getLocationsViewTableData';
-import { LocationViewHeaders } from './getLocationsViewTableData/types';
-import { useLocationsView } from './useLocationsView';
-import { LocationsViewProps } from './types';
-import { LoadingIndicator } from '../../composables/LoadingIndicator';
-import { MessageControl } from '../../controls/MessageControl';
-
-const getHeaders = ({
- hasObjectLocations,
- tableColumnBucketHeader,
- tableColumnFolderHeader,
- tableColumnPermissionsHeader,
- tableColumnActionsHeader,
-}: {
- hasObjectLocations: boolean;
- tableColumnBucketHeader: string;
- tableColumnFolderHeader: string;
- tableColumnPermissionsHeader: string;
- tableColumnActionsHeader: string;
-}): LocationViewHeaders => {
- const headers: LocationViewHeaders = [
- {
- key: 'folder',
- type: 'sort',
- content: { label: tableColumnFolderHeader },
- },
- {
- key: 'bucket',
- type: 'sort',
- content: { label: tableColumnBucketHeader },
- },
- {
- key: 'permission',
- type: 'sort',
- content: { label: tableColumnPermissionsHeader },
- },
- ];
- if (hasObjectLocations) {
- headers.push({
- key: 'action',
- type: 'sort',
- content: { label: tableColumnActionsHeader },
- });
- }
-
- return headers;
-};
-
-export function LocationsView({
- className,
- ...props
-}: LocationsViewProps): React.JSX.Element {
- const {
- LocationsView: {
- loadingIndicatorLabel,
- title,
- tableColumnBucketHeader,
- tableColumnFolderHeader,
- tableColumnPermissionsHeader,
- tableColumnActionsHeader,
- searchPlaceholder,
- searchSubmitLabel,
- searchClearLabel,
- getDownloadLabel,
- getPermissionName,
- getListLocationsResultMessage,
- },
- } = useDisplayText();
-
- const {
- hasError,
- hasNextPage,
- highestPageVisited,
- page,
- isLoading,
- searchQuery,
- pageItems,
- message,
- onDownload,
- onRefresh,
- onPaginate,
- onNavigate,
- onSearch,
- onSearchQueryChange,
- onSearchClear,
- } = useLocationsView(props);
-
- const loadingIndicator = (
-
- );
-
- // TODO: add hasExhaustedSearch + query param
- const messageControlContent = getListLocationsResultMessage({
- locations: pageItems,
- hasError,
- message,
- });
+import { LocationsViewProvider } from './LocationsViewProvider';
+import { LocationsViewType } from './types';
+import { useLocationsView } from './useLocationsView';
+import { classNames } from '@aws-amplify/ui';
- const headers = getHeaders({
- hasObjectLocations: pageItems.some(({ type }) => type === 'OBJECT'),
- tableColumnBucketHeader,
- tableColumnFolderHeader,
- tableColumnPermissionsHeader,
- tableColumnActionsHeader,
- });
+export const LocationsView: LocationsViewType = ({ className, ...props }) => {
+ const state = useLocationsView(props);
+ const { hasError } = state;
return (
-
-
+
-
-
+
+
{hasError ? null : }
+
-
-
+
+
);
-}
+};
+
+LocationsView.displayName = 'LocationsView';
+
+LocationsView.Provider = LocationsViewProvider;
+
+LocationsView.LoadingIndicator = LoadingIndicatorControl;
+LocationsView.LocationsTable = DataTableControl;
+LocationsView.Message = MessageControl;
+LocationsView.Pagination = PaginationControl;
+LocationsView.Refresh = DataRefreshControl;
+LocationsView.Search = SearchControl;
+LocationsView.Title = TitleControl;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsViewProvider.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsViewProvider.tsx
new file mode 100644
index 00000000000..0c87f6c8721
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/LocationsViewProvider.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+
+import { ControlsContextProvider } from '../../controls/context';
+import { useDisplayText } from '../../displayText';
+
+import { LocationsViewProviderProps } from './types';
+import { getLocationsViewTableData } from './getLocationsViewTableData';
+
+export function LocationsViewProvider({
+ children,
+ ...props
+}: LocationsViewProviderProps): React.JSX.Element {
+ const { LocationsView: displayText } = useDisplayText();
+ const {
+ loadingIndicatorLabel,
+ title,
+ searchPlaceholder,
+ searchSubmitLabel,
+ searchClearLabel,
+ getListLocationsResultMessage,
+ } = displayText;
+
+ const {
+ hasError,
+ hasNextPage,
+ highestPageVisited,
+ page,
+ isLoading,
+ searchQuery,
+ pageItems,
+ message,
+ onDownload,
+ onRefresh,
+ onPaginate,
+ onNavigate,
+ onSearch,
+ onSearchQueryChange,
+ onSearchClear,
+ } = props;
+
+ // TODO: add hasExhaustedSearch + query param
+ const messageControlContent = getListLocationsResultMessage({
+ isLoading,
+ locations: pageItems,
+ hasError,
+ message,
+ });
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx
index d5e29c2aac0..de9fae161a5 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/LocationsView.spec.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import * as ActionsModule from '../../../do-not-import-from-here/actions';
+import * as ActionsModule from '../../../actions';
import * as ConfigModule from '../../../providers/configuration';
import * as StoreModule from '../../../providers/store';
@@ -30,7 +30,7 @@ jest
.mockReturnValue([{} as StoreModule.UseStoreState, dispatchStoreAction]);
const useGetActionSpy = jest.spyOn(ConfigModule, 'useGetActionInput');
-const useLocationsDataSpy = jest.spyOn(ActionsModule, 'useLocationsData');
+const useListLocationsSpy = jest.spyOn(ActionsModule, 'useListLocations');
const mockUseDisplayText = jest.mocked(useDisplayText);
const mockGetListLocationsResultMessage = jest.mocked(
mockUseDisplayText().LocationsView.getListLocationsResultMessage
@@ -53,9 +53,9 @@ const generateMockItems = (size: number, page: number): LocationData[] => {
};
const handleListLocations = jest.fn();
-const initialState: ActionsModule.LocationsDataState = [
+const initialState: ActionsModule.UseListLocationsState = [
{
- data: { result: [], nextToken: undefined },
+ data: { items: [], nextToken: undefined },
hasError: false,
isLoading: false,
message: undefined,
@@ -63,9 +63,9 @@ const initialState: ActionsModule.LocationsDataState = [
handleListLocations,
];
-const loadingState: ActionsModule.LocationsDataState = [
+const loadingState: ActionsModule.UseListLocationsState = [
{
- data: { result: [], nextToken: undefined },
+ data: { items: [], nextToken: undefined },
hasError: false,
isLoading: true,
message: undefined,
@@ -74,12 +74,12 @@ const loadingState: ActionsModule.LocationsDataState = [
];
const EXPECTED_PAGE_SIZE = DEFAULT_LIST_OPTIONS.pageSize;
-const results: LocationData[] = generateMockItems(EXPECTED_PAGE_SIZE, 1);
+const items: LocationData[] = generateMockItems(EXPECTED_PAGE_SIZE, 1);
-const resolvedState: ActionsModule.LocationsDataState = [
+const resolvedState: ActionsModule.UseListLocationsState = [
{
data: {
- result: results,
+ items,
nextToken: 'some-token',
},
hasError: false,
@@ -89,12 +89,12 @@ const resolvedState: ActionsModule.LocationsDataState = [
handleListLocations,
];
-const nextPageResults = generateMockItems(EXPECTED_PAGE_SIZE, 2);
+const nextPageitems = generateMockItems(EXPECTED_PAGE_SIZE, 2);
-const nextPageState: ActionsModule.LocationsDataState = [
+const nextPageState: ActionsModule.UseListLocationsState = [
{
data: {
- result: [...results, ...nextPageResults],
+ items: [...items, ...nextPageitems],
nextToken: undefined,
},
hasError: false,
@@ -111,16 +111,26 @@ const config: ActionInputConfig = {
};
useGetActionSpy.mockReturnValue(() => config);
-describe('LocationsListView', () => {
+describe('LocationsView', () => {
afterEach(() => {
mockGetListLocationsResultMessage.mockClear();
jest.clearAllMocks();
});
+ it('has the expected composable components', () => {
+ expect(LocationsView.LoadingIndicator).toBeDefined();
+ expect(LocationsView.LocationsTable).toBeDefined();
+ expect(LocationsView.Message).toBeDefined();
+ expect(LocationsView.Pagination).toBeDefined();
+ expect(LocationsView.Refresh).toBeDefined();
+ expect(LocationsView.Search).toBeDefined();
+ expect(LocationsView.Title).toBeDefined();
+ });
+
it('renders and calls appropriate hooks', () => {
- useLocationsDataSpy.mockReturnValue([
+ useListLocationsSpy.mockReturnValue([
{
- data: { result: results, nextToken: undefined },
+ data: { items, nextToken: undefined },
hasError: true,
isLoading: false,
message: undefined,
@@ -130,15 +140,15 @@ describe('LocationsListView', () => {
render();
- expect(useLocationsDataSpy).toHaveBeenCalled();
+ expect(useListLocationsSpy).toHaveBeenCalled();
});
it('invokes getListLocationsResultMessage() with `errorMessage` param', () => {
const errorMessage = 'Something went wrong.';
- useLocationsDataSpy.mockReturnValue([
+ useListLocationsSpy.mockReturnValue([
{
- data: { result: results, nextToken: undefined },
+ data: { items, nextToken: undefined },
hasError: true,
isLoading: false,
message: errorMessage,
@@ -150,6 +160,7 @@ describe('LocationsListView', () => {
expect(mockGetListLocationsResultMessage).toHaveBeenCalledWith({
locations: expect.any(Array),
+ isLoading: false,
hasError: true,
message: errorMessage,
});
@@ -165,8 +176,32 @@ describe('LocationsListView', () => {
expect(prevPage).toBeDisabled();
});
+ it('does not show Message when items are being loaded', () => {
+ useListLocationsSpy.mockReturnValue([
+ {
+ data: {
+ items: [],
+ nextToken: undefined,
+ search: { hasExhaustedSearch: false },
+ },
+ hasError: false,
+ isLoading: true,
+ message: undefined,
+ },
+ handleListLocations,
+ ]);
+
+ render();
+
+ expect(mockGetListLocationsResultMessage).toHaveBeenCalledWith({
+ locations: [],
+ isLoading: true,
+ hasError: false,
+ });
+ });
+
it('renders a Locations View table', () => {
- useLocationsDataSpy.mockReturnValue(resolvedState);
+ useListLocationsSpy.mockReturnValue(resolvedState);
render();
@@ -180,7 +215,7 @@ describe('LocationsListView', () => {
it.todo('handles empty locations result data as expected');
it('behaves as expected on initial render', () => {
- useLocationsDataSpy
+ useListLocationsSpy
.mockReturnValueOnce(initialState)
.mockReturnValueOnce(loadingState)
.mockReturnValue(resolvedState);
@@ -205,7 +240,7 @@ describe('LocationsListView', () => {
});
it('refreshes table when refresh button is clicked', async () => {
- useLocationsDataSpy.mockReturnValue(resolvedState);
+ useListLocationsSpy.mockReturnValue(resolvedState);
render();
@@ -224,17 +259,17 @@ describe('LocationsListView', () => {
it('refreshes locations on handleListLocations reference change', () => {
const updatedHandleListLocations = jest.fn();
- useLocationsDataSpy.mockReturnValue(initialState);
+ useListLocationsSpy.mockReturnValue(initialState);
// initial
const { rerender } = render();
- useLocationsDataSpy.mockReturnValue(loadingState);
+ useListLocationsSpy.mockReturnValue(loadingState);
// loading
rerender();
- useLocationsDataSpy.mockReturnValueOnce(resolvedState);
+ useListLocationsSpy.mockReturnValueOnce(resolvedState);
// resolved
rerender();
@@ -242,15 +277,14 @@ describe('LocationsListView', () => {
expect(handleListLocations).toHaveBeenCalledTimes(1);
expect(handleListLocations).toHaveBeenCalledWith({
options: {
- // FIXME: update the exclude type after migration to new actions
- exclude: 'WRITE',
+ exclude: { exactPermissions: ['delete', 'write'] },
pageSize: EXPECTED_PAGE_SIZE,
refresh: true,
},
});
expect(updatedHandleListLocations).not.toHaveBeenCalled();
- useLocationsDataSpy.mockReturnValue([
+ useListLocationsSpy.mockReturnValue([
{ ...resolvedState[0] },
updatedHandleListLocations,
]);
@@ -262,8 +296,7 @@ describe('LocationsListView', () => {
expect(updatedHandleListLocations).toHaveBeenCalledTimes(1);
expect(updatedHandleListLocations).toHaveBeenCalledWith({
options: {
- // FIXME: update the exclude type after migration to new actions
- exclude: 'WRITE',
+ exclude: { exactPermissions: ['delete', 'write'] },
pageSize: EXPECTED_PAGE_SIZE,
refresh: true,
},
@@ -271,7 +304,7 @@ describe('LocationsListView', () => {
});
it('can paginate forward and back', async () => {
- useLocationsDataSpy.mockReturnValue(resolvedState);
+ useListLocationsSpy.mockReturnValue(resolvedState);
render();
// table renders
@@ -287,7 +320,7 @@ describe('LocationsListView', () => {
expect(screen.queryByText('item-0/')).toBeInTheDocument();
expect(screen.queryByText('item-101/')).not.toBeInTheDocument();
- useLocationsDataSpy.mockReturnValue(nextPageState);
+ useListLocationsSpy.mockReturnValue(nextPageState);
// go forward
await act(async () => {
@@ -315,7 +348,7 @@ describe('LocationsListView', () => {
});
it('should navigate to detail page when folder is clicked', async () => {
- useLocationsDataSpy.mockReturnValue(resolvedState);
+ useListLocationsSpy.mockReturnValue(resolvedState);
render();
const scopeButton = await screen.findByText('item-0/');
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/getLocationsViewTableData.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/getLocationsViewTableData.spec.ts
index f75f1d07fc6..574cfaf501c 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/getLocationsViewTableData.spec.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/getLocationsViewTableData.spec.ts
@@ -1,9 +1,10 @@
-import { getLocationsViewTableData } from '../getLocationsViewTableData';
-import { LocationViewHeaders } from '../getLocationsViewTableData/types';
-import { DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT } from '../../../displayText/libraries/en/locationsView';
import { LocationData } from '../../../actions';
+import { DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT } from '../../../displayText/libraries/en/locationsView';
+
+import { getLocationsViewTableData } from '../getLocationsViewTableData';
+import { getHeaders } from '../getLocationsViewTableData/getHeaders';
-const { getPermissionName } = DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT;
+jest.mock('../getLocationsViewTableData/getHeaders');
describe('getLocationsViewTableData', () => {
const folderLocation1: LocationData = {
@@ -31,33 +32,35 @@ describe('getLocationsViewTableData', () => {
permissions: ['list'],
};
- const headers: LocationViewHeaders = [
- {
- key: 'folder',
- type: 'sort',
- content: { label: 'Folder' },
- },
- {
- key: 'bucket',
- type: 'sort',
- content: { label: 'Bucket' },
- },
- {
- key: 'permission',
- type: 'sort',
- content: { label: 'Permission' },
- },
- {
- key: 'action',
- type: 'sort',
- content: { label: 'Actions' },
- },
- ];
-
- // create mocks
+ const mockGetHeaders = jest.mocked(getHeaders);
const mockOnNavigate = jest.fn();
const mockOnDownload = jest.fn();
+ beforeAll(() => {
+ mockGetHeaders.mockReturnValue([
+ {
+ key: 'folder',
+ type: 'sort',
+ content: { label: 'Folder' },
+ },
+ {
+ key: 'bucket',
+ type: 'sort',
+ content: { label: 'Bucket' },
+ },
+ {
+ key: 'permission',
+ type: 'sort',
+ content: { label: 'Permission' },
+ },
+ {
+ key: 'action',
+ type: 'sort',
+ content: { label: 'Actions' },
+ },
+ ]);
+ });
+
afterEach(() => {
mockOnNavigate.mockClear();
});
@@ -68,9 +71,7 @@ describe('getLocationsViewTableData', () => {
onDownload: mockOnDownload,
pageItems: [folderLocation1],
onNavigate: mockOnNavigate,
- headers,
- getDownloadLabel: () => 'download',
- getPermissionName,
+ displayText: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
})
).toStrictEqual({
headers: [
@@ -114,9 +115,7 @@ describe('getLocationsViewTableData', () => {
onDownload: mockOnDownload,
pageItems: [objectLocation],
onNavigate: mockOnNavigate,
- headers,
- getDownloadLabel: () => 'download',
- getPermissionName,
+ displayText: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
})
).toMatchObject({
rows: [
@@ -154,9 +153,7 @@ describe('getLocationsViewTableData', () => {
onDownload: mockOnDownload,
pageItems: [listOnlyObjectLocation],
onNavigate: mockOnNavigate,
- headers,
- getDownloadLabel: () => 'download',
- getPermissionName,
+ displayText: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
})
).toMatchObject({
rows: [
@@ -195,9 +192,7 @@ describe('getLocationsViewTableData', () => {
pageItems: [{ ...folderLocation1, prefix: '' }],
onNavigate: mockOnNavigate,
onDownload: mockOnDownload,
- headers,
- getDownloadLabel: () => 'download',
- getPermissionName,
+ displayText: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
})
).toStrictEqual(
expect.objectContaining({
@@ -222,9 +217,7 @@ describe('getLocationsViewTableData', () => {
pageItems: [folderLocation1, folderLocation2],
onNavigate: mockOnNavigate,
onDownload: mockOnDownload,
- headers,
- getDownloadLabel: () => 'download',
- getPermissionName,
+ displayText: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
});
const [row1FirstContent] = tableData.rows[0].content;
const [row2FirstContent] = tableData.rows[1].content;
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.tsx b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.tsx
index f98ce491ce5..442577099b2 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/__tests__/useLocationsView.spec.tsx
@@ -2,17 +2,12 @@ import { renderHook, act } from '@testing-library/react';
import { DataState } from '@aws-amplify/ui-react-core';
import { useLocationsView, DEFAULT_LIST_OPTIONS } from '../useLocationsView';
-import {
- ActionInputConfig,
- FileDataItem,
- LocationData,
-} from '../../../actions';
-import * as ActionsModule from '../../../do-not-import-from-here/actions';
+
+import * as ActionsModule from '../../../actions';
import * as StoreModule from '../../../providers/store';
import * as TasksModule from '../../../tasks';
import * as ConfigModule from '../../../providers/configuration';
-import { ListLocationsActionOutput } from '../../../do-not-import-from-here/actions/listLocationsAction';
import { createFileDataItemFromLocation } from '../../../actions/handlers';
jest.useFakeTimers();
@@ -23,10 +18,10 @@ jest
.spyOn(StoreModule, 'useStore')
.mockReturnValue([{} as StoreModule.UseStoreState, dispatchStoreAction]);
-const useLocationsDataSpy = jest.spyOn(ActionsModule, 'useLocationsData');
+const useLocationsDataSpy = jest.spyOn(ActionsModule, 'useListLocations');
const useGetActionSpy = jest.spyOn(ConfigModule, 'useGetActionInput');
-const mockData: LocationData[] = [
+const mockData: ActionsModule.LocationData[] = [
{
bucket: 'test-bucket',
prefix: `item-a/`,
@@ -66,14 +61,14 @@ const mockData: LocationData[] = [
const EXPECTED_PAGE_SIZE = 3;
function mockUseLocationsData(
- returnValue: DataState
+ returnValue: DataState
) {
const handleList = jest.fn();
useLocationsDataSpy.mockReturnValue([returnValue, handleList]);
return handleList;
}
-const taskOne: TasksModule.Task = {
+const taskOne: TasksModule.Task = {
data: {
fileKey: 'key',
id: 'id',
@@ -99,7 +94,7 @@ jest.spyOn(TasksModule, 'useProcessTasks').mockReturnValue([
handleDownload,
]);
-const config: ActionInputConfig = {
+const config: ActionsModule.ActionInputConfig = {
bucket: 'bucky',
credentials: jest.fn(),
region: 'us-weast-1',
@@ -113,7 +108,7 @@ describe('useLocationsView', () => {
it('should fetch and set location data on mount', () => {
const mockDataState = {
- data: { result: mockData, nextToken: undefined },
+ data: { items: mockData, nextToken: undefined },
message: '',
hasError: false,
isLoading: false,
@@ -140,7 +135,7 @@ describe('useLocationsView', () => {
// empty state
mockUseLocationsData({
data: {
- result: [],
+ items: [],
nextToken: undefined,
},
message: '',
@@ -158,7 +153,7 @@ describe('useLocationsView', () => {
// mock first page data
const mockDataState = {
data: {
- result: mockData.slice(0, EXPECTED_PAGE_SIZE),
+ items: mockData.slice(0, EXPECTED_PAGE_SIZE),
nextToken: 'token123',
},
message: '',
@@ -176,7 +171,7 @@ describe('useLocationsView', () => {
// mock next page
mockUseLocationsData({
- data: { result: mockData, nextToken: undefined },
+ data: { items: mockData, nextToken: undefined },
message: '',
hasError: false,
isLoading: false,
@@ -207,7 +202,7 @@ describe('useLocationsView', () => {
it('should handle refreshing location data', () => {
const mockDataState = {
- data: { result: [], nextToken: 'token123' },
+ data: { items: [], nextToken: 'token123' },
message: '',
hasError: false,
isLoading: false,
@@ -251,7 +246,7 @@ describe('useLocationsView', () => {
it('should handle downloading a file', () => {
const { result } = renderHook(() => useLocationsView());
- const location: LocationData = {
+ const location: ActionsModule.LocationData = {
bucket: 'bucket',
id: 'id',
permissions: ['get'],
@@ -269,7 +264,7 @@ describe('useLocationsView', () => {
it('should handle search', () => {
const mockDataState = {
- data: { result: mockData, nextToken: undefined },
+ data: { items: mockData, nextToken: undefined },
message: '',
hasError: false,
isLoading: false,
@@ -289,7 +284,7 @@ describe('useLocationsView', () => {
expect(result.current.pageItems).toEqual([
{
bucket: 'test-bucket',
- prefix: `item-b/`,
+ prefix: 'item-b/',
permissions: ['get', 'list'],
id: '2',
type: 'PREFIX',
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getHeaders.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getHeaders.ts
new file mode 100644
index 00000000000..9cd959bc381
--- /dev/null
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getHeaders.ts
@@ -0,0 +1,43 @@
+import { LocationViewHeaders } from './types';
+
+export const getHeaders = ({
+ hasObjectLocations,
+ tableColumnActionsHeader,
+ tableColumnBucketHeader,
+ tableColumnFolderHeader,
+ tableColumnPermissionsHeader,
+}: {
+ hasObjectLocations: boolean;
+ tableColumnActionsHeader: string;
+ tableColumnBucketHeader: string;
+ tableColumnFolderHeader: string;
+ tableColumnPermissionsHeader: string;
+}): LocationViewHeaders => {
+ const headers: LocationViewHeaders = [
+ {
+ key: 'folder',
+ type: 'sort',
+ content: { label: tableColumnFolderHeader },
+ },
+ {
+ key: 'bucket',
+ type: 'sort',
+ content: { label: tableColumnBucketHeader },
+ },
+ {
+ key: 'permission',
+ type: 'sort',
+ content: { label: tableColumnPermissionsHeader },
+ },
+ ];
+
+ if (hasObjectLocations) {
+ headers.push({
+ key: 'action',
+ type: 'sort',
+ content: { label: tableColumnActionsHeader },
+ });
+ }
+
+ return headers;
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getLocationsViewTableData.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getLocationsViewTableData.ts
index 3cadea8b7fc..dec6d2231bc 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getLocationsViewTableData.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/getLocationsViewTableData/getLocationsViewTableData.ts
@@ -1,22 +1,35 @@
import { DataTableProps } from '../../../composables/DataTable';
-import { LocationData, LocationPermissions } from '../../../actions';
-import { LocationViewHeaders } from './types';
+import { LocationData } from '../../../actions';
+import { getHeaders } from './getHeaders';
+import { DefaultLocationsViewDisplayText } from '../../../displayText/types';
export const getLocationsViewTableData = ({
+ displayText,
pageItems,
onNavigate,
onDownload,
- headers,
- getDownloadLabel,
- getPermissionName,
}: {
+ displayText: DefaultLocationsViewDisplayText;
pageItems: LocationData[];
onNavigate: (location: LocationData) => void;
- headers: LocationViewHeaders;
onDownload: (location: LocationData) => void;
- getDownloadLabel: (fileName: string) => string;
- getPermissionName: (permissions: LocationPermissions) => string;
}): DataTableProps => {
+ const {
+ tableColumnActionsHeader,
+ tableColumnBucketHeader,
+ tableColumnFolderHeader,
+ tableColumnPermissionsHeader,
+ getDownloadLabel,
+ getPermissionName,
+ } = displayText;
+ const headers = getHeaders({
+ hasObjectLocations: pageItems.some(({ type }) => type === 'OBJECT'),
+ tableColumnActionsHeader,
+ tableColumnBucketHeader,
+ tableColumnFolderHeader,
+ tableColumnPermissionsHeader,
+ });
+
const rows: DataTableProps['rows'] = pageItems.map((location) => {
const { bucket, id, permissions, prefix } = location;
return {
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/types.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/types.ts
index 66e26f3e51f..91b1429ae20 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/types.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/types.ts
@@ -1,3 +1,54 @@
+import { LocationData } from '../../actions';
import { ListViewProps } from '../types';
+export interface LocationsViewState {
+ hasNextPage: boolean;
+ hasError: boolean;
+ highestPageVisited: number;
+ isLoading: boolean;
+ message: string | undefined;
+ shouldShowEmptyMessage: boolean;
+ pageItems: LocationData[];
+ page: number;
+ searchQuery: string;
+ onDownload: (item: LocationData) => void;
+ onNavigate: (location: LocationData) => void;
+ onRefresh: () => void;
+ onPaginate: (page: number) => void;
+ onSearch: () => void;
+ onSearchQueryChange: (value: string) => void;
+ onSearchClear: () => void;
+}
+
export interface LocationsViewProps extends ListViewProps {}
+
+export interface LocationsViewProviderProps extends LocationsViewState {
+ children?: React.ReactNode;
+}
+
+export interface LocationsViewType {
+ (
+ props: {
+ children?: React.ReactNode;
+ className?: string;
+ } & LocationsViewProps
+ ): React.JSX.Element | null;
+ displayName: string;
+ Provider: (props: LocationsViewProviderProps) => React.JSX.Element;
+ LoadingIndicator: () => React.JSX.Element | null;
+ LocationsTable: () => React.JSX.Element | null;
+ Message: () => React.JSX.Element | null;
+ Pagination: () => React.JSX.Element | null;
+ Refresh: () => React.JSX.Element | null;
+ Search: () => React.JSX.Element | null;
+ Title: () => React.JSX.Element | null;
+}
+
+interface InitialValues {
+ pageSize?: number;
+}
+
+export interface UseLocationsViewOptions {
+ initialValues?: InitialValues;
+ onNavigate?: (location: LocationData) => void;
+}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts
index 4ea48ecafeb..c9a6fdf9ca8 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/LocationsView/useLocationsView.ts
@@ -1,66 +1,40 @@
import React from 'react';
-import { useLocationsData } from '../../do-not-import-from-here/actions';
import { usePaginate } from '../hooks/usePaginate';
-import { downloadHandler, LocationData } from '../../actions';
+import {
+ createFileDataItemFromLocation,
+ downloadHandler,
+ ListLocationsExcludeOptions,
+ LocationData,
+ useListLocations,
+} from '../../actions';
import { useStore } from '../../providers/store';
import { useSearch } from '../hooks/useSearch';
import { useGetActionInput } from '../../providers/configuration';
import { useProcessTasks } from '../../tasks';
-import { createFileDataItemFromLocation } from '../../actions/handlers';
-
-interface UseLocationsView {
- hasNextPage: boolean;
- hasError: boolean;
- highestPageVisited: number;
- isLoading: boolean;
- message: string | undefined;
- shouldShowEmptyMessage: boolean;
- pageItems: LocationData[];
- page: number;
- searchQuery: string;
- onDownload: (item: LocationData) => void;
- onNavigate: (location: LocationData) => void;
- onRefresh: () => void;
- onPaginate: (page: number) => void;
- onSearch: () => void;
- onSearchQueryChange: (value: string) => void;
- onSearchClear: () => void;
-}
-
-interface InitialValues {
- pageSize?: number;
-}
-
-export type LocationsViewActionType =
- | { type: 'REFRESH_DATA' }
- | { type: 'RESET' }
- | { type: 'PAGINATE'; page: number }
- | { type: 'NAVIGATE'; location: LocationData }
- | { type: 'SEARCH'; query: string };
-
-export interface UseLocationsViewOptions {
- initialValues?: InitialValues;
- onDispatch?: React.Dispatch;
- onNavigate?: (location: LocationData) => void;
-}
+import { LocationsViewState, UseLocationsViewOptions } from './types';
+const DEFAULT_EXCLUDE: ListLocationsExcludeOptions = {
+ exactPermissions: ['delete', 'write'],
+};
const DEFAULT_PAGE_SIZE = 100;
export const DEFAULT_LIST_OPTIONS = {
- exclude: 'WRITE' as const, // FIXME: update exclude type after migration to new actions
+ exclude: DEFAULT_EXCLUDE,
pageSize: DEFAULT_PAGE_SIZE,
};
-export function useLocationsView(
+export const useLocationsView = (
options?: UseLocationsViewOptions
-): UseLocationsView {
+): LocationsViewState => {
const getConfig = useGetActionInput();
- const [state, handleList] = useLocationsData();
+
+ const [state, handleList] = useListLocations();
+ const { data, message, hasError, isLoading } = state;
+
const [_, handleDownload] = useProcessTasks(downloadHandler);
const [, dispatchStoreAction] = useStore();
const [term, setTerm] = React.useState('');
- const { data, message, hasError, isLoading } = state;
- const { result, nextToken } = data;
+ const { items, nextToken } = data;
const hasNextToken = !!nextToken;
const onNavigate = options?.onNavigate;
@@ -101,7 +75,7 @@ export function useLocationsView(
highestPageVisited,
pageItems,
} = usePaginate({
- items: result,
+ items,
paginateCallback,
pageSize: listOptions.pageSize,
hasNextToken,
@@ -152,4 +126,4 @@ export function useLocationsView(
resetSearch();
},
};
-}
+};
diff --git a/packages/react-storage/src/components/StorageBrowser/views/__tests__/utils.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/__tests__/utils.spec.ts
deleted file mode 100644
index fa3effedd78..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/__tests__/utils.spec.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { isFile, getPercentValue, resolveClassName } from '../utils';
-
-describe('view utils', () => {
- it('uses isFile util to discern between files and folders', () => {
- const file = new File(['file contents'], 'test file', {
- type: 'text/plain',
- });
- const folder = new File([], 'test folder');
-
- expect(isFile(file)).toBe(true);
- expect(isFile(folder)).toBe(false);
- });
-
- it('uses getPercentValue util to calculate the percentage of a number', () => {
- expect(getPercentValue(0.01)).toBe(1);
- expect(getPercentValue(0.5)).toBe(50);
- expect(getPercentValue(1)).toBe(100);
- });
-
- it('uses resolveClassName util to resolve a className', () => {
- const stringClassName = 'test-class';
- const resolvedStringClassName = resolveClassName(
- 'default',
- stringClassName
- );
-
- expect(resolvedStringClassName).toBe('default test-class');
-
- const functionClassName = () => 'test-class';
- const resolvedFunctionClassName = resolveClassName(
- 'default',
- functionClassName
- );
-
- expect(resolvedFunctionClassName).toBe('test-class');
- });
-});
diff --git a/packages/react-storage/src/components/StorageBrowser/views/context.tsx b/packages/react-storage/src/components/StorageBrowser/views/context.tsx
index f4fc38e82b0..db19d1421cf 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/context.tsx
+++ b/packages/react-storage/src/components/StorageBrowser/views/context.tsx
@@ -19,8 +19,10 @@ export interface DefaultViews {
LocationActionView: (
props: LocationActionViewProps
) => React.JSX.Element | null;
- LocationDetailView: (props: LocationDetailViewProps) => React.JSX.Element;
- LocationsView: (props: LocationsViewProps) => React.JSX.Element;
+ LocationDetailView: (
+ props: LocationDetailViewProps
+ ) => React.JSX.Element | null;
+ LocationsView: (props: LocationsViewProps) => React.JSX.Element | null;
}
export interface Views extends Partial> {}
diff --git a/packages/react-storage/src/components/StorageBrowser/views/index.ts b/packages/react-storage/src/components/StorageBrowser/views/index.ts
index 9edbe3bdce0..7a231a2f061 100644
--- a/packages/react-storage/src/components/StorageBrowser/views/index.ts
+++ b/packages/react-storage/src/components/StorageBrowser/views/index.ts
@@ -1,4 +1,14 @@
-export { LocationActionView } from './LocationActionView';
+export {
+ CopyView,
+ CopyViewType,
+ CreateFolderView,
+ CreateFolderViewType,
+ DeleteView,
+ DeleteViewType,
+ LocationActionView,
+ UploadView,
+ UploadViewType,
+} from './LocationActionView';
export {
LocationDetailView,
LocationDetailViewProps,
diff --git a/packages/react-storage/src/components/StorageBrowser/views/utils.ts b/packages/react-storage/src/components/StorageBrowser/views/utils.ts
deleted file mode 100644
index 68646cf8262..00000000000
--- a/packages/react-storage/src/components/StorageBrowser/views/utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { isFunction, isString } from '@aws-amplify/ui';
-
-// checks if a dropped item is a file or a folder, as a folder will not have a type
-export const isFile = (file: File): boolean => file.type !== '';
-
-export const getPercentValue = (value: number): number =>
- Math.round(value * 100);
-
-export const resolveClassName = (
- defaultClassName: string,
- className?: string | ((defaultClassName: string) => string)
-): string => {
- if (isString(className)) return `${defaultClassName} ${className}`;
-
- if (isFunction(className)) return className(defaultClassName);
-
- return defaultClassName;
-};
diff --git a/packages/react-storage/src/styles/storage-browser.css b/packages/react-storage/src/styles/storage-browser.css
index 66ce5e314bf..618d4a4565f 100644
--- a/packages/react-storage/src/styles/storage-browser.css
+++ b/packages/react-storage/src/styles/storage-browser.css
@@ -172,12 +172,24 @@
border-width: 0;
}
+.storage-browser__action-destination,
+.storage-browser__action-destination .storage-browser__description-list {
+ display: flex;
+ gap: var(--storage-browser-gap-small);
+ align-items: center;
+}
+
+.storage-browser__action-destination > span {
+ font-weight: var(--amplify-font-weights-bold);
+}
+
/* Base component styles */
/** DescriptionList **/
.storage-browser__description-list {
margin: 0;
display: flex;
gap: var(--storage-browser-gap-large);
+ align-items: center;
}
.storage-browser__description {
@@ -204,7 +216,8 @@
justify-content: center;
}
-.storage-browser__loading-indicator-icon {
+.storage-browser__loading-indicator-icon,
+.storage-browser__table-text-data-cell-icon--action-progress {
animation: var(--storage-browser-loading-animation);
}
@@ -369,13 +382,6 @@
flex: 1;
}
-.storage-browser__action-destination {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
-}
-
.storage-browser__copy-destination-picker {
width: 100%;
flex: 1;
@@ -405,6 +411,14 @@
width: 100%;
}
+/* Copy view styles */
+.storage-browser__copy-destination-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+}
+
/* Search view styles */
.storage-browser__search {
display: flex;
diff --git a/packages/react/src/primitives/Icon/context/IconsContext.tsx b/packages/react/src/primitives/Icon/context/IconsContext.tsx
index 6ea5714ce29..29d254c3da6 100644
--- a/packages/react/src/primitives/Icon/context/IconsContext.tsx
+++ b/packages/react/src/primitives/Icon/context/IconsContext.tsx
@@ -1,5 +1,38 @@
import * as React from 'react';
+type StorageBrowserIconType =
+ | 'action-canceled'
+ | 'action-error'
+ | 'action-initial'
+ | 'action-progress'
+ | 'action-queued'
+ | 'action-success'
+ | 'cancel'
+ | 'create-folder'
+ | 'copy-file'
+ | 'delete-file'
+ | 'dismiss'
+ | 'download'
+ | 'error'
+ | 'exit'
+ | 'file'
+ | 'folder'
+ | 'info'
+ | 'loading'
+ | 'menu'
+ | 'paginate-next'
+ | 'paginate-previous'
+ | 'refresh'
+ | 'search'
+ | 'sort-ascending'
+ | 'sort-descending'
+ | 'sort-indeterminate'
+ | 'success'
+ | 'upload-file'
+ | 'upload-folder'
+ | 'vertical-kebab'
+ | 'warning';
+
type ComponentIcons = {
[Key in Keys]?: React.ReactNode;
};
@@ -21,6 +54,7 @@ export type IconsContextInterface = {
searchField?: ComponentIcons<'search'>;
select?: ComponentIcons<'expand'>;
stepperField?: ComponentIcons<'add' | 'remove'>;
+ storageBrowser?: ComponentIcons;
storageManager?: ComponentIcons<
'upload' | 'remove' | 'error' | 'success' | 'file'
>;