Skip to content

Commit 10d5061

Browse files
zchenweicalebpollman
authored andcommitted
feat(storage-manager): add prefix prop to support gen2 use cases (aws-amplify#5176)
* feat(storage-manager): add prefix prop to support gen2 use cases
1 parent 1c62d82 commit 10d5061

File tree

17 files changed

+230
-51
lines changed

17 files changed

+230
-51
lines changed

.changeset/fair-tips-deliver.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@aws-amplify/ui-react-storage": minor
3+
"@aws-amplify/ui-react": patch
4+
---
5+
6+
feat(storage-manager): add the `prefix` prop to support gen2 use cases

packages/react-storage/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"name": "StorageManager",
5959
"path": "dist/esm/index.mjs",
6060
"import": "{ StorageManager }",
61-
"limit": "25 kB"
61+
"limit": "25.2 kB"
6262
}
6363
]
6464
}

packages/react-storage/src/components/StorageManager/StorageManager.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import * as React from 'react';
22

3-
import { UploadDataOutput } from 'aws-amplify/storage';
3+
import {
4+
UploadDataOutput,
5+
UploadDataWithPathOutput,
6+
} from 'aws-amplify/storage';
47
import { getLogger, ComponentClassName } from '@aws-amplify/ui';
58
import { VisuallyHidden } from '@aws-amplify/ui-react';
69
import { useSetUserAgent } from '@aws-amplify/ui-react-core';
7-
import { useDropZone } from '@aws-amplify/ui-react/internal';
10+
import {
11+
useDropZone,
12+
useDeprecationWarning,
13+
} from '@aws-amplify/ui-react/internal';
814

915
import { useStorageManager, useUploadFiles } from './hooks';
1016
import { FileStatus, StorageManagerProps, StorageManagerHandle } from './types';
@@ -43,11 +49,39 @@ function StorageManagerBase(
4349
processFile,
4450
components,
4551
path,
52+
prefix,
4653
}: StorageManagerProps,
4754
ref: React.ForwardedRef<StorageManagerHandle>
4855
): JSX.Element {
49-
if (!accessLevel || !maxFileCount) {
50-
logger.warn('StorageManager requires accessLevel and maxFileCount props');
56+
useDeprecationWarning({
57+
message:
58+
'The `accessLevel` and `path` props have been deprecated in favor of the `prefix` prop.',
59+
shouldWarn: Boolean(accessLevel ?? path),
60+
});
61+
62+
if (!maxFileCount) {
63+
logger.warn(
64+
'`StorageManager` requires the `maxFileCount` prop to be specified'
65+
);
66+
}
67+
68+
if (!prefix && !accessLevel) {
69+
// set accessLevel to private to respect the default before adding the `prefix` prop
70+
accessLevel = 'private';
71+
// accessLevel has been depreacted so only guide the user to pass the `prefix` prop
72+
logger.warn('`StorageManager` requires the `prefix` prop to be specified');
73+
}
74+
75+
if (prefix && accessLevel) {
76+
throw new Error(
77+
'The `prefix` and `accessLevel` props cannot be specified at the same time. Prefer usage of `prefix` as `accessLevel` has been deprecated and will be removed in a future major version'
78+
);
79+
}
80+
81+
if (prefix && path) {
82+
throw new Error(
83+
'The `prefix` and `path` props cannot be specified at the same time. Prefer usage of `prefix` as `path` has been deprecated and will be removed in a future major version'
84+
);
5185
}
5286

5387
const Components = {
@@ -127,6 +161,7 @@ function StorageManagerBase(
127161
setUploadSuccess,
128162
processFile,
129163
path,
164+
prefix,
130165
});
131166

132167
const onFilePickerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -155,7 +190,7 @@ function StorageManagerBase(
155190
uploadTask,
156191
}: {
157192
id: string;
158-
uploadTask: UploadDataOutput;
193+
uploadTask: UploadDataOutput | UploadDataWithPathOutput;
159194
}) => {
160195
uploadTask.pause();
161196
setUploadPaused({ id });
@@ -166,7 +201,7 @@ function StorageManagerBase(
166201
uploadTask,
167202
}: {
168203
id: string;
169-
uploadTask: UploadDataOutput;
204+
uploadTask: UploadDataOutput | UploadDataWithPathOutput;
170205
}) => {
171206
uploadTask.resume();
172207
setUploadResumed({ id });
@@ -177,7 +212,7 @@ function StorageManagerBase(
177212
uploadTask,
178213
}: {
179214
id: string;
180-
uploadTask: UploadDataOutput;
215+
uploadTask: UploadDataOutput | UploadDataWithPathOutput;
181216
}) => {
182217
// At this time we don't know if the delete
183218
// permissions are enabled (required to cancel upload),

packages/react-storage/src/components/StorageManager/__tests__/StorageManager.test.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { defaultStorageManagerDisplayText } from '../utils';
1515

1616
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
17+
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
1718

1819
const uploadDataSpy = jest
1920
.spyOn(Storage, 'uploadData')
@@ -71,7 +72,8 @@ describe('StorageManager', () => {
7172
getByText(defaultStorageManagerDisplayText.dropFilesText)
7273
).toBeVisible();
7374

74-
expect(warnSpy).not.toHaveBeenCalled();
75+
// acceessLevel prop deprecation warning
76+
expect(warnSpy).toHaveBeenCalledTimes(1);
7577
});
7678

7779
it('renders as expected with autoUpload turned off', () => {
@@ -215,7 +217,8 @@ describe('StorageManager', () => {
215217
it('renders a warning if maxFileCount is zero', () => {
216218
render(<StorageManager {...storeManagerProps} maxFileCount={0} />);
217219

218-
expect(warnSpy).toHaveBeenCalledTimes(1);
220+
// missing maxFileCount prop + accessLevel prop deprecation = 2
221+
expect(warnSpy).toHaveBeenCalledTimes(2);
219222
});
220223

221224
it('should trigger hidden input onChange', async () => {
@@ -286,4 +289,38 @@ describe('StorageManager', () => {
286289
expect(mockClearFiles).toHaveBeenCalledTimes(1);
287290
expect(mockClearFiles).toHaveBeenCalledWith();
288291
});
292+
293+
it('should set accessLevel to private if neither prefix nor accessLevel is specified', () => {
294+
const mockUseUploadFiles = jest.fn();
295+
jest
296+
.spyOn(StorageHooks, 'useUploadFiles')
297+
.mockImplementationOnce(mockUseUploadFiles);
298+
299+
render(<StorageManager maxFileCount={1} />);
300+
301+
expect(warnSpy).toHaveBeenCalled();
302+
expect(mockUseUploadFiles).toHaveBeenCalledWith(
303+
expect.objectContaining({ accessLevel: 'private' })
304+
);
305+
});
306+
307+
it('should throw an error if both prefix and accessLevel are specified', () => {
308+
expect(() =>
309+
render(
310+
<StorageManager
311+
prefix="prefix/"
312+
accessLevel="private"
313+
maxFileCount={1}
314+
/>
315+
)
316+
).toThrow();
317+
expect(errorSpy).toHaveBeenCalled();
318+
});
319+
320+
it('should throw an error if both prefix and path are specified', () => {
321+
expect(() =>
322+
render(<StorageManager prefix="prefix/" path="path/" maxFileCount={1} />)
323+
).toThrow();
324+
expect(errorSpy).toHaveBeenCalled();
325+
});
289326
});

packages/react-storage/src/components/StorageManager/hooks/useStorageManager/actions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { UploadDataOutput } from 'aws-amplify/storage';
1+
import {
2+
UploadDataOutput,
3+
UploadDataWithPathOutput,
4+
} from 'aws-amplify/storage';
25

36
import { FileStatus } from '../../types';
47

@@ -38,7 +41,7 @@ export const setUploadingFileAction = ({
3841
uploadTask,
3942
}: {
4043
id: string;
41-
uploadTask: UploadDataOutput | undefined;
44+
uploadTask: UploadDataOutput | UploadDataWithPathOutput | undefined;
4245
}): Action => {
4346
return {
4447
type: StorageManagerActionTypes.SET_STATUS_UPLOADING,

packages/react-storage/src/components/StorageManager/hooks/useStorageManager/reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function storageManagerStateReducer(
7373
...currentFile,
7474
status: FileStatus.UPLOADING,
7575
progress: 0,
76-
uploadTask: uploadTask ? uploadTask : undefined,
76+
uploadTask,
7777
},
7878
];
7979
}

packages/react-storage/src/components/StorageManager/hooks/useStorageManager/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { UploadDataOutput } from 'aws-amplify/storage';
1+
import {
2+
UploadDataOutput,
3+
UploadDataWithPathOutput,
4+
} from 'aws-amplify/storage';
25

36
import { FileStatus, StorageFiles } from '../../types';
47

@@ -39,7 +42,7 @@ export type Action =
3942
| {
4043
type: StorageManagerActionTypes.SET_STATUS_UPLOADING;
4144
id: string;
42-
uploadTask?: UploadDataOutput;
45+
uploadTask?: UploadDataOutput | UploadDataWithPathOutput;
4346
}
4447
| {
4548
type: StorageManagerActionTypes.SET_UPLOAD_PROGRESS;

packages/react-storage/src/components/StorageManager/hooks/useStorageManager/useStorageManager.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React from 'react';
22

3-
import { UploadDataOutput } from 'aws-amplify/storage';
3+
import {
4+
UploadDataOutput,
5+
UploadDataWithPathOutput,
6+
} from 'aws-amplify/storage';
47
import { isObject } from '@aws-amplify/ui';
58

69
import { StorageFiles, FileStatus, DefaultFile } from '../../types';
@@ -26,7 +29,7 @@ export interface UseStorageManager {
2629
queueFiles: () => void;
2730
setUploadingFile: (params: {
2831
id: string;
29-
uploadTask?: UploadDataOutput;
32+
uploadTask?: UploadDataOutput | UploadDataWithPathOutput;
3033
}) => void;
3134
setUploadProgress: (params: { id: string; progress: number }) => void;
3235
setUploadSuccess: (params: { id: string }) => void;

packages/react-storage/src/components/StorageManager/hooks/useUploadFiles/__tests__/useUploadFiles.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ const uploadDataSpy = jest
1414
pause: jest.fn(),
1515
resume: jest.fn(),
1616
state: 'SUCCESS',
17-
result: Promise.resolve({ key: input.key, data: input.data }),
17+
result: Promise.resolve({
18+
key: input.key,
19+
data: input.data,
20+
}),
1821
};
1922
});
2023

@@ -212,4 +215,29 @@ describe('useUploadFiles', () => {
212215
);
213216
});
214217
});
218+
219+
it('prepends valid provided `prefix` to `processedKey`', async () => {
220+
const prefix = 'test-prefix/';
221+
const { waitForNextUpdate } = renderHook(() =>
222+
useUploadFiles({
223+
...props,
224+
isResumable: true,
225+
files: [mockQueuedFile],
226+
prefix,
227+
accessLevel: undefined,
228+
})
229+
);
230+
231+
waitForNextUpdate();
232+
233+
await waitFor(() => {
234+
expect(mockOnUploadStart).toHaveBeenCalledWith({
235+
key: `${prefix}${mockQueuedFile.key}`,
236+
});
237+
expect(uploadDataSpy).toHaveBeenCalledTimes(1);
238+
expect(uploadDataSpy).toHaveBeenCalledWith(
239+
expect.objectContaining({ path: `${prefix}${mockQueuedFile.key}` })
240+
);
241+
});
242+
});
215243
});

packages/react-storage/src/components/StorageManager/hooks/useUploadFiles/useUploadFiles.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22

33
import { TransferProgressEvent } from 'aws-amplify/storage';
4-
import { isFunction, isString } from '@aws-amplify/ui';
4+
import { isFunction } from '@aws-amplify/ui';
55

66
import { uploadFile } from '../../utils/uploadFile';
77
import { FileStatus } from '../../types';
@@ -20,6 +20,7 @@ export interface UseUploadFilesProps
2020
| 'maxFileCount'
2121
| 'processFile'
2222
| 'path'
23+
| 'prefix'
2324
>,
2425
Pick<
2526
UseStorageManager,
@@ -39,6 +40,7 @@ export function useUploadFiles({
3940
maxFileCount,
4041
processFile,
4142
path,
43+
prefix,
4244
}: UseUploadFilesProps): void {
4345
React.useEffect(() => {
4446
const filesReadyToUpload = files.filter(
@@ -72,18 +74,19 @@ export function useUploadFiles({
7274
if (file) {
7375
resolveFile({ processFile, file, key }).then(
7476
({ key: processedKey, ...rest }) => {
75-
// prepend `path` to `processedKey`
76-
const resolvedKey = isString(path)
77-
? `${path}${processedKey}`
78-
: processedKey;
77+
// prepend `prefix` or `path` to `processedKey`
78+
// TODO: path will be deprecated in the future
79+
const resolvedKey = `${prefix ?? path ?? ''}${processedKey}`;
7980

8081
if (isFunction(onUploadStart)) {
8182
onUploadStart({ key: resolvedKey });
8283
}
8384

8485
const uploadTask = uploadFile({
8586
...rest,
87+
// key will be deprecated in the future
8688
key: resolvedKey,
89+
path: resolvedKey,
8790
level: accessLevel,
8891
progressCallback: onProgress,
8992
errorCallback: (error: string) => {
@@ -112,5 +115,6 @@ export function useUploadFiles({
112115
setUploadSuccess,
113116
processFile,
114117
path,
118+
prefix,
115119
]);
116120
}

packages/react-storage/src/components/StorageManager/types.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as React from 'react';
22

3-
import type { UploadDataOutput } from 'aws-amplify/storage';
3+
import type {
4+
UploadDataOutput,
5+
UploadDataWithPathOutput,
6+
} from 'aws-amplify/storage';
47
import type { StorageAccessLevel } from '@aws-amplify/core';
58

69
import {
@@ -27,7 +30,7 @@ export interface StorageFile {
2730
file?: File;
2831
status: FileStatus;
2932
progress: number;
30-
uploadTask?: UploadDataOutput;
33+
uploadTask?: UploadDataOutput | UploadDataWithPathOutput;
3134
key: string;
3235
error: string;
3336
isImage: boolean;
@@ -55,10 +58,10 @@ export interface StorageManagerProps {
5558
*/
5659
acceptedFileTypes?: string[];
5760
/**
58-
* Access level for file uploads
59-
* @see https://docs.amplify.aws/lib/storage/configureaccess/q/platform/js/
61+
* @deprecated
62+
* `accessLevel` has been deprecated in favor of `prefix` and will be removed in a future major version
6063
*/
61-
accessLevel: StorageAccessLevel;
64+
accessLevel?: StorageAccessLevel;
6265

6366
/**
6467
* Determines if the upload will automatically start after a file is selected, default value: true
@@ -120,9 +123,12 @@ export interface StorageManagerProps {
120123
*/
121124
showThumbnails?: boolean;
122125
/**
123-
* A path to put files in the s3 bucket.
124-
* This will be prepended to the key sent to
125-
* s3 for each file.
126+
* @deprecated
127+
* `path` has been deprecated in favor of `prefix` and will be removed in a future major version
126128
*/
127129
path?: string;
130+
/**
131+
* A string prefixed to the key of each file
132+
*/
133+
prefix?: string;
128134
}

0 commit comments

Comments
 (0)