@aws-amplify/ui-react-storage 3.14.0 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/browser.js +8 -2
  2. package/dist/{createStorageBrowser-CotOvK0A.js → createStorageBrowser-B-J76Lyp.js} +1012 -249
  3. package/dist/esm/browser.mjs +1 -0
  4. package/dist/esm/components/StorageBrowser/ErrorBoundary/ErrorBoundary.mjs +0 -3
  5. package/dist/esm/components/StorageBrowser/StorageBrowserAmplify.mjs +1 -0
  6. package/dist/esm/components/StorageBrowser/actions/configs/defaults.mjs +14 -3
  7. package/dist/esm/components/StorageBrowser/actions/handlers/defaults.mjs +1 -1
  8. package/dist/esm/components/StorageBrowser/actions/handlers/delete.mjs +39 -8
  9. package/dist/esm/components/StorageBrowser/actions/handlers/listLocations.mjs +7 -2
  10. package/dist/esm/components/StorageBrowser/actions/handlers/utils.mjs +65 -1
  11. package/dist/esm/components/StorageBrowser/actions/handlers/zipdownload.mjs +195 -0
  12. package/dist/esm/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.mjs +3 -1
  13. package/dist/esm/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.mjs +1 -0
  14. package/dist/esm/components/StorageBrowser/components/ComponentsProvider.mjs +0 -3
  15. package/dist/esm/components/StorageBrowser/components/base/preview/DownloadButton.mjs +0 -3
  16. package/dist/esm/components/StorageBrowser/components/composables/ActionConfirmationModal.mjs +34 -0
  17. package/dist/esm/components/StorageBrowser/components/composables/defaults.mjs +2 -0
  18. package/dist/esm/components/StorageBrowser/components/elements/definitions.mjs +2 -2
  19. package/dist/esm/components/StorageBrowser/controls/ActionConfirmationModalControl.mjs +12 -0
  20. package/dist/esm/components/StorageBrowser/controls/DataTableControl.mjs +0 -3
  21. package/dist/esm/components/StorageBrowser/controls/hooks/useActionConfirmationModal.mjs +17 -0
  22. package/dist/esm/components/StorageBrowser/createStorageBrowser/StorageBrowserDefault.mjs +8 -3
  23. package/dist/esm/components/StorageBrowser/createStorageBrowser/createProvider.mjs +1 -0
  24. package/dist/esm/components/StorageBrowser/createStorageBrowser/createStorageBrowser.mjs +9 -4
  25. package/dist/esm/components/StorageBrowser/displayText/libraries/en/deleteView.mjs +117 -5
  26. package/dist/esm/components/StorageBrowser/displayText/libraries/en/downloadView.mjs +2 -0
  27. package/dist/esm/components/StorageBrowser/displayText/libraries/en/locationDetailView.mjs +1 -0
  28. package/dist/esm/components/StorageBrowser/displayText/libraries/en/shared.mjs +3 -0
  29. package/dist/esm/components/StorageBrowser/locationItems/context.mjs +18 -14
  30. package/dist/esm/components/StorageBrowser/locationItems/utils.mjs +38 -0
  31. package/dist/esm/components/StorageBrowser/store/validateStoreProps.mjs +1 -1
  32. package/dist/esm/components/StorageBrowser/tasks/constants.mjs +2 -0
  33. package/dist/esm/components/StorageBrowser/tasks/useProcessTasks.mjs +14 -5
  34. package/dist/esm/components/StorageBrowser/tasks/utils.mjs +4 -1
  35. package/dist/esm/components/StorageBrowser/useAction/useHandler.mjs +1 -1
  36. package/dist/esm/components/StorageBrowser/useAction/utils.mjs +0 -4
  37. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.mjs +0 -3
  38. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.mjs +4 -3
  39. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.mjs +0 -3
  40. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.mjs +0 -3
  41. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.mjs +0 -3
  42. package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.mjs +0 -3
  43. package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.mjs +0 -3
  44. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.mjs +5 -5
  45. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.mjs +7 -6
  46. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/useDeleteView.mjs +69 -6
  47. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/utils.mjs +87 -0
  48. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.mjs +0 -3
  49. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.mjs +9 -0
  50. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.mjs +0 -3
  51. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.mjs +1 -4
  52. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.mjs +4 -0
  53. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.mjs +0 -3
  54. package/dist/esm/components/StorageBrowser/views/LocationDetailView/LocationDetailView.mjs +0 -3
  55. package/dist/esm/components/StorageBrowser/views/LocationDetailView/LocationDetailViewProvider.mjs +2 -1
  56. package/dist/esm/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getFolderRowContent.mjs +38 -27
  57. package/dist/esm/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getLocationDetailViewTableData.mjs +9 -1
  58. package/dist/esm/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.mjs +11 -9
  59. package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsView.mjs +0 -3
  60. package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsViewProvider.mjs +0 -3
  61. package/dist/esm/components/StorageBrowser/views/LocationsView/useLocationsView.mjs +1 -0
  62. package/dist/esm/components/StorageBrowser/views/context/actionViews.mjs +7 -3
  63. package/dist/esm/components/StorageBrowser/views/context/primaryViews.mjs +8 -3
  64. package/dist/esm/components/StorageBrowser/views/hooks/useFilePreview/useFilePreview.mjs +1 -0
  65. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/constants.mjs +14 -1
  66. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/deleteResolvers.mjs +123 -13
  67. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/utils.mjs +4 -4
  68. package/dist/esm/version.mjs +1 -1
  69. package/dist/index.js +2 -1
  70. package/dist/styles.css +100 -1
  71. package/dist/types/components/StorageBrowser/actions/handlers/delete.d.ts +5 -3
  72. package/dist/types/components/StorageBrowser/actions/handlers/index.d.ts +1 -0
  73. package/dist/types/components/StorageBrowser/actions/handlers/types.d.ts +18 -2
  74. package/dist/types/components/StorageBrowser/actions/handlers/utils.d.ts +11 -0
  75. package/dist/types/components/StorageBrowser/actions/handlers/zipdownload.d.ts +3 -0
  76. package/dist/types/components/StorageBrowser/components/composables/ActionConfirmationModal.d.ts +12 -0
  77. package/dist/types/components/StorageBrowser/components/composables/types.d.ts +2 -0
  78. package/dist/types/components/StorageBrowser/controls/ActionConfirmationModalControl.d.ts +2 -0
  79. package/dist/types/components/StorageBrowser/controls/hooks/useActionConfirmationModal.d.ts +2 -0
  80. package/dist/types/components/StorageBrowser/controls/index.d.ts +1 -0
  81. package/dist/types/components/StorageBrowser/controls/types.d.ts +4 -0
  82. package/dist/types/components/StorageBrowser/displayText/types.d.ts +9 -0
  83. package/dist/types/components/StorageBrowser/locationItems/context.d.ts +12 -3
  84. package/dist/types/components/StorageBrowser/locationItems/index.d.ts +1 -0
  85. package/dist/types/components/StorageBrowser/locationItems/utils.d.ts +27 -0
  86. package/dist/types/components/StorageBrowser/tasks/types.d.ts +10 -5
  87. package/dist/types/components/StorageBrowser/tasks/useProcessTasks.d.ts +1 -1
  88. package/dist/types/components/StorageBrowser/useAction/useHandler.d.ts +1 -1
  89. package/dist/types/components/StorageBrowser/views/LocationActionView/DeleteView/types.d.ts +5 -0
  90. package/dist/types/components/StorageBrowser/views/LocationActionView/DeleteView/utils.d.ts +26 -0
  91. package/dist/types/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getFolderRowContent.d.ts +4 -1
  92. package/dist/types/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getLocationDetailViewTableData.d.ts +3 -2
  93. package/dist/types/components/StorageBrowser/views/LocationDetailView/types.d.ts +8 -1
  94. package/dist/types/components/StorageBrowser/views/utils/index.d.ts +1 -1
  95. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/constants.d.ts +5 -0
  96. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/deleteResolvers.d.ts +5 -2
  97. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/index.d.ts +1 -1
  98. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/types.d.ts +4 -0
  99. package/dist/types/version.d.ts +1 -1
  100. package/package.json +12 -9
  101. package/dist/esm/components/StorageBrowser/actions/handlers/download.mjs +0 -38
@@ -10,8 +10,11 @@ var uiReact = require('@aws-amplify/ui-react');
10
10
  var elements = require('@aws-amplify/ui-react-core/elements');
11
11
  var internal = require('@aws-amplify/ui-react/internal');
12
12
  var uiReactCore = require('@aws-amplify/ui-react-core');
13
+ var JSZip = require('jszip');
13
14
  var storage = require('aws-amplify/storage');
14
15
 
16
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
+
15
18
  function _interopNamespace(e) {
16
19
  if (e && e.__esModule) return e;
17
20
  var n = Object.create(null);
@@ -31,8 +34,185 @@ function _interopNamespace(e) {
31
34
  }
32
35
 
33
36
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
37
+ var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
38
+
39
+ const VERSION = '3.16.0';
40
+
41
+ const DEFAULT_CHECKSUM_ALGORITHM = 'crc-32';
42
+ // 5MiB for multipart upload
43
+ // https://github.com/aws-amplify/amplify-js/blob/1a5366d113c9af4ce994168653df3aadb142c581/packages/storage/src/providers/s3/utils/constants.ts#L16
44
+ const MULTIPART_UPLOAD_THRESHOLD_BYTES = 5 * 1024 * 1024;
34
45
 
35
- const VERSION = '3.14.0';
46
+ const getBucketRegion = (bucketName, fallbackRegion) => {
47
+ try {
48
+ const config = awsAmplify.Amplify.getConfig()?.Storage?.S3;
49
+ if (!config?.buckets || typeof config.buckets !== 'object') {
50
+ return fallbackRegion;
51
+ }
52
+ for (const bucketConfig of Object.values(config.buckets)) {
53
+ if (bucketConfig.bucketName === bucketName && bucketConfig.region) {
54
+ return bucketConfig.region;
55
+ }
56
+ }
57
+ return fallbackRegion;
58
+ }
59
+ catch (error) {
60
+ return fallbackRegion;
61
+ }
62
+ };
63
+ const constructBucket$1 = ({ bucket: bucketName, region: globalRegion, }) => {
64
+ const bucketRegion = getBucketRegion(bucketName, globalRegion);
65
+ return { bucketName, region: bucketRegion };
66
+ };
67
+ const parseAccessGrantLocation = (location) => {
68
+ const { permission, scope, type } = location;
69
+ if (!scope.startsWith('s3://')) {
70
+ throw new Error(`Invalid scope: ${scope}`);
71
+ }
72
+ const id = crypto.randomUUID();
73
+ // remove default path
74
+ const slicedScope = scope.slice(5);
75
+ let bucket, prefix;
76
+ switch (type) {
77
+ case 'BUCKET': {
78
+ // { scope: 's3://bucket/*', type: 'BUCKET', },
79
+ bucket = slicedScope.slice(0, -2);
80
+ prefix = '';
81
+ break;
82
+ }
83
+ case 'PREFIX': {
84
+ // { scope: 's3://bucket/path/*', type: 'PREFIX', },
85
+ bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
86
+ prefix = `${slicedScope.slice(bucket.length + 1, -1)}`;
87
+ break;
88
+ }
89
+ case 'OBJECT': {
90
+ // { scope: 's3://bucket/path/to/object', type: 'OBJECT', },
91
+ bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
92
+ prefix = slicedScope.slice(bucket.length + 1);
93
+ break;
94
+ }
95
+ default: {
96
+ throw new Error(`Invalid location type: ${type}`);
97
+ }
98
+ }
99
+ let permissions;
100
+ switch (permission) {
101
+ case 'READ':
102
+ permissions = ['get', 'list'];
103
+ break;
104
+ case 'READWRITE':
105
+ permissions = ['delete', 'get', 'list', 'write'];
106
+ break;
107
+ case 'WRITE':
108
+ permissions = ['delete', 'write'];
109
+ break;
110
+ default:
111
+ throw new Error(`Invalid location permission: ${permission}`);
112
+ }
113
+ return { bucket, id, permissions: permissions, prefix, type };
114
+ };
115
+ const isSamePermissions = (permissionsToExclude, locationPermissions) => {
116
+ if (permissionsToExclude.length !== locationPermissions.length) {
117
+ return false;
118
+ }
119
+ const sortedLocationPermissions = locationPermissions.sort();
120
+ return permissionsToExclude
121
+ .sort()
122
+ .every((permission, index) => permission === sortedLocationPermissions[index]);
123
+ };
124
+ const isSameType = (typeToExclude, locationType) => typeof typeToExclude === 'string'
125
+ ? typeToExclude === locationType
126
+ : typeToExclude.includes(locationType);
127
+ const shouldExcludeLocation = ({ permissions, type }, exclude) => {
128
+ const excludedByPermssions = !!(exclude?.exactPermissions &&
129
+ isSamePermissions(exclude.exactPermissions, permissions));
130
+ const excludedByType = !!(exclude?.type && isSameType(exclude.type, type));
131
+ return excludedByPermssions || excludedByType;
132
+ };
133
+ /**
134
+ * Determines if permissions1 is a strict superset (broader) of permissions2.
135
+ * Returns true only if permissions1 contains all permissions from permissions2
136
+ * AND has more permissions.
137
+ */
138
+ const hasBroaderPermissions = (permissions1, permissions2) => {
139
+ // permissions1 must have more permissions (strict superset, not equal)
140
+ if (permissions1.length <= permissions2.length) {
141
+ return false;
142
+ }
143
+ // permissions1 must contain all permissions from permissions2
144
+ return permissions2.every((perm) => permissions1.includes(perm));
145
+ };
146
+ /**
147
+ * Deduplicates locations with the same bucket and prefix.
148
+ * Only deduplicates when one location's permissions are a superset of another's.
149
+ * This prevents deduplication of incompatible grants like READ + WRITE.
150
+ *
151
+ * Examples:
152
+ * - READ + READWRITE → Keep READWRITE (superset)
153
+ * - READ + READ → Keep first (identical)
154
+ * - READ + WRITE → Keep both (not superset, need separate locations)
155
+ */
156
+ const deduplicateLocations = (locations) => {
157
+ // Group locations by bucket:prefix
158
+ const locationGroups = new Map();
159
+ for (const location of locations) {
160
+ const key = `${location.bucket}:${location.prefix}`;
161
+ const group = locationGroups.get(key) ?? [];
162
+ group.push(location);
163
+ locationGroups.set(key, group);
164
+ }
165
+ // For each group, keep only non-redundant locations
166
+ const result = [];
167
+ for (const group of locationGroups.values()) {
168
+ if (group.length === 1) {
169
+ result.push(group[0]);
170
+ continue;
171
+ }
172
+ // Find locations that are not subsets of any other location in the group
173
+ const nonRedundant = [];
174
+ for (const location of group) {
175
+ // Check if this location is a subset of any other location
176
+ const isSubsetOfAnother = group.some((other) => {
177
+ if (other === location)
178
+ return false;
179
+ return hasBroaderPermissions(other.permissions, location.permissions);
180
+ });
181
+ if (!isSubsetOfAnother) {
182
+ // Check if we already have an identical permission set
183
+ const isDuplicate = nonRedundant.some((existing) => {
184
+ const sortedNew = [...location.permissions].sort().join(',');
185
+ const sortedExisting = [...existing.permissions].sort().join(',');
186
+ return sortedNew === sortedExisting;
187
+ });
188
+ if (!isDuplicate) {
189
+ nonRedundant.push(location);
190
+ }
191
+ }
192
+ }
193
+ result.push(...nonRedundant);
194
+ }
195
+ return result;
196
+ };
197
+ const getFilteredLocations = (locations, exclude) => locations.reduce((filteredLocations, location) => {
198
+ const parsedLocation = parseAccessGrantLocation(location);
199
+ const isNonFolderLikePrefix = !parsedLocation.prefix.endsWith('/') &&
200
+ parsedLocation.type === 'PREFIX';
201
+ if (isNonFolderLikePrefix) {
202
+ return filteredLocations;
203
+ }
204
+ if (!shouldExcludeLocation(parsedLocation, exclude)) {
205
+ filteredLocations.push(parsedLocation);
206
+ }
207
+ return filteredLocations;
208
+ }, []);
209
+ const getFileKey = (key) => key.slice(key.lastIndexOf('/') + 1, key.length);
210
+ const createFileDataItem = (data) => ({
211
+ ...data,
212
+ fileKey: getFileKey(data.key),
213
+ });
214
+ const getProgress = ({ totalBytes, transferredBytes, }) => totalBytes ? transferredBytes / totalBytes : undefined;
215
+ const isMultipartUpload = (file) => file.size > MULTIPART_UPLOAD_THRESHOLD_BYTES;
36
216
 
37
217
  const toAccessGrantPermission = (permission) => {
38
218
  let result = '';
@@ -118,7 +298,8 @@ const createAmplifyListLocationsHandler = () => {
118
298
  id: crypto.randomUUID(),
119
299
  };
120
300
  });
121
- cachedItems = sanitizedItems;
301
+ // Deduplicate locations with the same bucket and prefix, keeping broader permissions
302
+ cachedItems = deduplicateLocations(sanitizedItems);
122
303
  return getPaginatedLocations({
123
304
  items: cachedItems,
124
305
  pageSize,
@@ -163,123 +344,11 @@ const createAmplifyAuthAdapter = () => {
163
344
  };
164
345
  };
165
346
 
166
- const DEFAULT_CHECKSUM_ALGORITHM = 'crc-32';
167
- // 5MiB for multipart upload
168
- // https://github.com/aws-amplify/amplify-js/blob/1a5366d113c9af4ce994168653df3aadb142c581/packages/storage/src/providers/s3/utils/constants.ts#L16
169
- const MULTIPART_UPLOAD_THRESHOLD_BYTES = 5 * 1024 * 1024;
170
-
171
- const getBucketRegion = (bucketName, fallbackRegion) => {
172
- try {
173
- const config = awsAmplify.Amplify.getConfig()?.Storage?.S3;
174
- if (!config?.buckets || typeof config.buckets !== 'object') {
175
- return fallbackRegion;
176
- }
177
- for (const bucketConfig of Object.values(config.buckets)) {
178
- if (bucketConfig.bucketName === bucketName && bucketConfig.region) {
179
- return bucketConfig.region;
180
- }
181
- }
182
- return fallbackRegion;
183
- }
184
- catch (error) {
185
- return fallbackRegion;
186
- }
187
- };
188
- const constructBucket = ({ bucket: bucketName, region: globalRegion, }) => {
189
- const bucketRegion = getBucketRegion(bucketName, globalRegion);
190
- return { bucketName, region: bucketRegion };
191
- };
192
- const parseAccessGrantLocation = (location) => {
193
- const { permission, scope, type } = location;
194
- if (!scope.startsWith('s3://')) {
195
- throw new Error(`Invalid scope: ${scope}`);
196
- }
197
- const id = crypto.randomUUID();
198
- // remove default path
199
- const slicedScope = scope.slice(5);
200
- let bucket, prefix;
201
- switch (type) {
202
- case 'BUCKET': {
203
- // { scope: 's3://bucket/*', type: 'BUCKET', },
204
- bucket = slicedScope.slice(0, -2);
205
- prefix = '';
206
- break;
207
- }
208
- case 'PREFIX': {
209
- // { scope: 's3://bucket/path/*', type: 'PREFIX', },
210
- bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
211
- prefix = `${slicedScope.slice(bucket.length + 1, -1)}`;
212
- break;
213
- }
214
- case 'OBJECT': {
215
- // { scope: 's3://bucket/path/to/object', type: 'OBJECT', },
216
- bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
217
- prefix = slicedScope.slice(bucket.length + 1);
218
- break;
219
- }
220
- default: {
221
- throw new Error(`Invalid location type: ${type}`);
222
- }
223
- }
224
- let permissions;
225
- switch (permission) {
226
- case 'READ':
227
- permissions = ['get', 'list'];
228
- break;
229
- case 'READWRITE':
230
- permissions = ['delete', 'get', 'list', 'write'];
231
- break;
232
- case 'WRITE':
233
- permissions = ['delete', 'write'];
234
- break;
235
- default:
236
- throw new Error(`Invalid location permission: ${permission}`);
237
- }
238
- return { bucket, id, permissions: permissions, prefix, type };
239
- };
240
- const isSamePermissions = (permissionsToExclude, locationPermissions) => {
241
- if (permissionsToExclude.length !== locationPermissions.length) {
242
- return false;
243
- }
244
- const sortedLocationPermissions = locationPermissions.sort();
245
- return permissionsToExclude
246
- .sort()
247
- .every((permission, index) => permission === sortedLocationPermissions[index]);
248
- };
249
- const isSameType = (typeToExclude, locationType) => typeof typeToExclude === 'string'
250
- ? typeToExclude === locationType
251
- : typeToExclude.includes(locationType);
252
- const shouldExcludeLocation = ({ permissions, type }, exclude) => {
253
- const excludedByPermssions = !!(exclude?.exactPermissions &&
254
- isSamePermissions(exclude.exactPermissions, permissions));
255
- const excludedByType = !!(exclude?.type && isSameType(exclude.type, type));
256
- return excludedByPermssions || excludedByType;
257
- };
258
- const getFilteredLocations = (locations, exclude) => locations.reduce((filteredLocations, location) => {
259
- const parsedLocation = parseAccessGrantLocation(location);
260
- const isNonFolderLikePrefix = !parsedLocation.prefix.endsWith('/') &&
261
- parsedLocation.type === 'PREFIX';
262
- if (isNonFolderLikePrefix) {
263
- return filteredLocations;
264
- }
265
- if (!shouldExcludeLocation(parsedLocation, exclude)) {
266
- filteredLocations.push(parsedLocation);
267
- }
268
- return filteredLocations;
269
- }, []);
270
- const getFileKey = (key) => key.slice(key.lastIndexOf('/') + 1, key.length);
271
- const createFileDataItem = (data) => ({
272
- ...data,
273
- fileKey: getFileKey(data.key),
274
- });
275
- const getProgress = ({ totalBytes, transferredBytes, }) => totalBytes ? transferredBytes / totalBytes : undefined;
276
- const isMultipartUpload = (file) => file.size > MULTIPART_UPLOAD_THRESHOLD_BYTES;
277
-
278
347
  const copyHandler = (input) => {
279
348
  const { config, data } = input;
280
349
  const { accountId: expectedBucketOwner, credentials, customEndpoint, } = config;
281
350
  const { key, sourceKey, lastModified, eTag } = data;
282
- const bucket = constructBucket(config);
351
+ const bucket = constructBucket$1(config);
283
352
  const source = {
284
353
  bucket,
285
354
  expectedBucketOwner,
@@ -320,7 +389,7 @@ const createFolderHandler = (input) => {
320
389
  const { accountId, credentials, customEndpoint } = config;
321
390
  const { onProgress } = options ?? {};
322
391
  const { key, preventOverwrite } = data;
323
- const bucket = constructBucket(config);
392
+ const bucket = constructBucket$1(config);
324
393
  const { result } = internals.uploadData({
325
394
  path: key,
326
395
  data: '',
@@ -353,42 +422,169 @@ const createFolderHandler = (input) => {
353
422
  };
354
423
  };
355
424
 
356
- const deleteHandler = ({ config, data, }) => {
425
+ const deleteHandler = ({ config, data, options, }) => {
357
426
  const { key } = data;
358
427
  const { accountId, credentials, customEndpoint } = config;
359
- const result = internals.remove({
428
+ const { onProgress } = options ?? {};
429
+ let cumulativeSuccessCount = 0;
430
+ let cumulativeFailureCount = 0;
431
+ let operationCancel = () => {
432
+ // noop
433
+ };
434
+ const cancel = () => {
435
+ operationCancel?.();
436
+ };
437
+ const operation = internals.remove({
360
438
  path: key,
361
439
  options: {
362
- bucket: constructBucket(config),
440
+ bucket: constructBucket$1(config),
363
441
  locationCredentialsProvider: credentials,
364
442
  expectedBucketOwner: accountId,
365
443
  customEndpoint,
444
+ onProgress: (progress) => {
445
+ if (!progress) {
446
+ return;
447
+ }
448
+ const batchSuccessCount = progress?.deleted?.length ?? 0;
449
+ const batchFailureCount = progress?.failed?.length ?? 0;
450
+ cumulativeSuccessCount += batchSuccessCount;
451
+ cumulativeFailureCount += batchFailureCount;
452
+ onProgress?.(data, {
453
+ successCount: cumulativeSuccessCount,
454
+ failureCount: cumulativeFailureCount,
455
+ });
456
+ },
366
457
  },
458
+ });
459
+ operationCancel = operation?.cancel ? operation?.cancel : () => { };
460
+ const operationPromise = operation?.result ?? operation;
461
+ const result = operationPromise
462
+ ?.then?.(({ path }) => {
463
+ return {
464
+ status: 'COMPLETE',
465
+ value: {
466
+ key: path,
467
+ successCount: cumulativeSuccessCount,
468
+ failureCount: cumulativeFailureCount,
469
+ },
470
+ };
367
471
  })
368
- .then(({ path }) => ({
369
- status: 'COMPLETE',
370
- value: { key: path },
371
- }))
372
- .catch((error) => {
472
+ ?.catch?.((error) => {
373
473
  const { message } = error;
374
474
  return { error, message, status: 'FAILED' };
375
475
  });
376
- return { result };
476
+ return { result, cancel };
377
477
  };
378
478
 
379
- function downloadFromUrl(fileName, url) {
380
- const a = document.createElement('a');
381
- a.href = url;
382
- a.download = fileName;
383
- a.target = '_blank';
384
- document.body.appendChild(a);
385
- a.click();
386
- document.body.removeChild(a);
387
- }
388
- const downloadHandler = ({ config, data }) => {
389
- const { accountId, credentials, customEndpoint } = config;
479
+ const zipProgressManager = ({ dataMap, onZipProgress, }) => {
480
+ const iter = (() => {
481
+ let f;
482
+ let i = 0;
483
+ return (str) => {
484
+ if (!f) {
485
+ f = str;
486
+ }
487
+ else if (str !== f) {
488
+ ++i;
489
+ f = str;
490
+ }
491
+ return i;
492
+ };
493
+ })();
494
+ const progressMap = new Map(Array.from(dataMap.keys()).map((k) => [k, 0]));
495
+ const total = dataMap.size;
496
+ dataMap.forEach((data, key) => {
497
+ onZipProgress(key, 0, data);
498
+ });
499
+ return ({ percent, currentFile, }) => {
500
+ if (currentFile) {
501
+ const item = iter(currentFile);
502
+ const sliceSize = 100 / total; // when 3 this is 33.3%
503
+ const start = sliceSize * item; // this is 66.6% for last item of 3;
504
+ // take percent and calculate the percent of the slice
505
+ const progress = percent - start;
506
+ const actualPercent = (progress / sliceSize) * 100;
507
+ progressMap.set(currentFile, Math.max(actualPercent, progressMap.get(currentFile) ?? 0));
508
+ onZipProgress(currentFile, progressMap.get(currentFile), dataMap.get(currentFile));
509
+ }
510
+ };
511
+ };
512
+ const zipper = (() => {
513
+ let zip = null;
514
+ const dataMap = new Map();
515
+ return {
516
+ addFile: (file, name, data) => {
517
+ if (!zip) {
518
+ zip = new JSZip__default["default"]();
519
+ }
520
+ dataMap.set(name, data);
521
+ return new Promise((ok, no) => {
522
+ try {
523
+ zip?.file(name, file);
524
+ ok();
525
+ }
526
+ catch (e) {
527
+ no();
528
+ }
529
+ });
530
+ },
531
+ getBlobUrl: async (onProgress) => {
532
+ if (!zip) {
533
+ throw new Error('no zip');
534
+ }
535
+ const blob = await zip.generateAsync({
536
+ type: 'blob',
537
+ streamFiles: true,
538
+ compression: 'DEFLATE',
539
+ compressionOptions: {
540
+ level: 5,
541
+ },
542
+ }, zipProgressManager({
543
+ dataMap,
544
+ onZipProgress: (file, progress, data) => {
545
+ if (ui.isFunction(onProgress)) {
546
+ onProgress(progress, file, data);
547
+ }
548
+ },
549
+ }));
550
+ zip = null;
551
+ return URL.createObjectURL(blob);
552
+ },
553
+ destroy: () => {
554
+ dataMap.clear();
555
+ zip = null;
556
+ },
557
+ };
558
+ })();
559
+ const constructBucket = ({ bucket: bucketName, region, }) => ({ bucketName, region });
560
+ const readBody = async (response, { data, options }) => {
561
+ let loading = true;
562
+ const chunks = [];
563
+ const reader = response.body.getReader();
564
+ const size = +(response.headers.get('content-length') ?? 0);
565
+ let received = 0;
566
+ while (loading) {
567
+ const { value, done } = await reader.read();
568
+ if (done) {
569
+ loading = false;
570
+ }
571
+ else {
572
+ chunks.push(value);
573
+ received += value.length;
574
+ if (ui.isFunction(options?.onProgress)) {
575
+ options?.onProgress(data, getProgress({
576
+ totalBytes: size,
577
+ transferredBytes: received,
578
+ }), 'PENDING');
579
+ }
580
+ }
581
+ }
582
+ return new Blob(chunks);
583
+ };
584
+ const download = async ({ config, data, all, options }, abortController) => {
585
+ const { customEndpoint, credentials, accountId } = config;
390
586
  const { key } = data;
391
- const result = internals.getUrl({
587
+ const { url } = await internals.getUrl({
392
588
  path: key,
393
589
  options: {
394
590
  bucket: constructBucket(config),
@@ -398,17 +594,76 @@ const downloadHandler = ({ config, data }) => {
398
594
  contentDisposition: 'attachment',
399
595
  expectedBucketOwner: accountId,
400
596
  },
401
- })
402
- .then(({ url }) => {
403
- downloadFromUrl(key, url.toString());
404
- return { status: 'COMPLETE', value: { url } };
405
- })
406
- .catch((error) => {
407
- const { message } = error;
408
- return { error, message, status: 'FAILED' };
409
597
  });
410
- return { result };
411
- };
598
+ const response = await fetch(url, {
599
+ mode: 'cors',
600
+ signal: abortController.signal,
601
+ });
602
+ const blob = await readBody(response, { data, options });
603
+ const [filename] = key.split('/').reverse();
604
+ await zipper.addFile(blob, filename, data);
605
+ return filename;
606
+ };
607
+ const downloadHandler = (() => {
608
+ const fileDownloadQueue = new Map();
609
+ const handler = ({ config, data, all, options }) => {
610
+ const { key } = data;
611
+ const [, folder] = key.split('/').reverse();
612
+ fileDownloadQueue.set(key, false);
613
+ const abortController = new AbortController();
614
+ return {
615
+ cancel: () => {
616
+ abortController.abort();
617
+ fileDownloadQueue.set(key, true);
618
+ },
619
+ result: download({ config, data, all, options }, abortController)
620
+ .then(() => {
621
+ fileDownloadQueue.set(key, true);
622
+ return {
623
+ status: 'LOADED',
624
+ };
625
+ })
626
+ .catch((e) => {
627
+ const error = e;
628
+ fileDownloadQueue.set(key, true);
629
+ return {
630
+ status: 'FAILED',
631
+ message: error.message,
632
+ error,
633
+ };
634
+ })
635
+ .finally(() => {
636
+ const done = all.every(({ key }) => {
637
+ return fileDownloadQueue.get(key);
638
+ });
639
+ if (done) {
640
+ zipper
641
+ .getBlobUrl((percent, name, _data) => {
642
+ if (ui.isFunction(options?.onProgress)) {
643
+ const progress = percent / 100;
644
+ options?.onProgress(_data, progress, progress === 1 ? 'COMPLETE' : 'FINISHING');
645
+ }
646
+ })
647
+ .then((blobURL) => {
648
+ if (blobURL) {
649
+ zipper.destroy();
650
+ const anchor = document.createElement('a');
651
+ const clickEvent = new MouseEvent('click');
652
+ anchor.href = blobURL;
653
+ anchor.download = `${folder || 'archive'}.zip`;
654
+ anchor.dispatchEvent(clickEvent);
655
+ }
656
+ })
657
+ .catch(() => {
658
+ // this catch happens, when no zip was created.
659
+ // it is handled by the UI showing "FAILED" for all files
660
+ });
661
+ }
662
+ }),
663
+ };
664
+ };
665
+ return handler;
666
+ })();
412
667
 
413
668
  const DEFAULT_PAGE_SIZE$5 = 1000;
414
669
  const parseItems = (items, excludedPath) => items
@@ -494,7 +749,7 @@ const uploadHandler = ({ config, data, options }) => {
494
749
  path: key,
495
750
  data: file,
496
751
  options: {
497
- bucket: constructBucket(config),
752
+ bucket: constructBucket$1(config),
498
753
  expectedBucketOwner: accountId,
499
754
  locationCredentialsProvider: credentials,
500
755
  onProgress: (event) => {
@@ -568,10 +823,50 @@ const defaultValue$7 = {
568
823
  };
569
824
  const { useActionConfigs, ActionConfigsProvider } = uiReactCore.createContextUtilities({ contextName: 'ActionConfigs', defaultValue: defaultValue$7 });
570
825
 
826
+ /**
827
+ * Selection utility functions for LocationItems
828
+ */
829
+ /**
830
+ * Get selected files from dataItems
831
+ */
832
+ const getSelectedFiles = (dataItems) => {
833
+ return dataItems?.filter((item) => item.type === 'FILE') ?? [];
834
+ };
835
+ /**
836
+ * Get selected folders from dataItems
837
+ */
838
+ const getSelectedFolders = (dataItems) => {
839
+ return dataItems?.filter((item) => item.type === 'FOLDER') ?? [];
840
+ };
841
+ /**
842
+ * Check if selection contains folders
843
+ */
844
+ const hasSelectedFolders = (dataItems) => {
845
+ return dataItems?.some((item) => item.type === 'FOLDER') ?? false;
846
+ };
847
+ /**
848
+ * Get selection summary
849
+ */
850
+ const getSelectionSummary = (dataItems) => {
851
+ const files = getSelectedFiles(dataItems);
852
+ const folders = getSelectedFolders(dataItems);
853
+ return {
854
+ total: dataItems?.length ?? 0,
855
+ files: files.length,
856
+ folders: folders.length,
857
+ hasFiles: files.length > 0,
858
+ hasFolders: folders.length > 0,
859
+ isMixed: files.length > 0 && folders.length > 0,
860
+ };
861
+ };
862
+
571
863
  const copyActionConfig = {
572
864
  viewName: 'CopyView',
573
865
  actionListItem: {
574
- disable: (selected) => !selected || selected.length === 0,
866
+ disable: (selected) => {
867
+ const hasNoSelection = !selected || selected.length === 0;
868
+ return hasNoSelection || hasSelectedFolders(selected);
869
+ },
575
870
  hide: (permissions) => !permissions.includes('write'),
576
871
  icon: 'copy-file',
577
872
  label: 'Copy',
@@ -581,7 +876,10 @@ const copyActionConfig = {
581
876
  const deleteActionConfig = {
582
877
  viewName: 'DeleteView',
583
878
  actionListItem: {
584
- disable: (selected) => !selected || selected.length === 0,
879
+ disable: (selected) => {
880
+ const hasNoSelection = !selected || selected.length === 0;
881
+ return hasNoSelection;
882
+ },
585
883
  hide: (permissions) => !permissions.includes('delete'),
586
884
  icon: 'delete-file',
587
885
  label: 'Delete',
@@ -609,7 +907,10 @@ const uploadActionConfig = {
609
907
  const downloadActionConfig = {
610
908
  viewName: 'DownloadView',
611
909
  actionListItem: {
612
- disable: (selected) => !selected || selected.length === 0,
910
+ disable: (selected) => {
911
+ const hasNoSelection = !selected || selected.length === 0;
912
+ return hasNoSelection || hasSelectedFolders(selected);
913
+ },
613
914
  hide: (permissions) => !permissions.includes('get'),
614
915
  icon: 'download',
615
916
  label: 'Download',
@@ -814,7 +1115,7 @@ const OrderedListElement = elements.defineBaseElement({
814
1115
  type: 'ol',
815
1116
  displayName: 'OrderedList',
816
1117
  });
817
- elements.defineBaseElement({
1118
+ const UnorderedListElement = elements.defineBaseElement({
818
1119
  type: 'ul',
819
1120
  displayName: 'UnorderedList',
820
1121
  });
@@ -954,6 +1255,34 @@ const { useComposables, ComposablesProvider } = uiReactCore.createContextUtiliti
954
1255
 
955
1256
  const ActionCancel = ({ onCancel, isDisabled, label, }) => (React__namespace["default"].createElement(ButtonElement, { variant: "cancel", className: `${STORAGE_BROWSER_BLOCK}__cancel`, onClick: onCancel, disabled: isDisabled }, label));
956
1257
 
1258
+ const ActionConfirmationModal = ({ isOpen = false, title, message, content, onConfirm, onCancel, confirmLabel, cancelLabel, }) => {
1259
+ const modalRef = React__namespace["default"].useRef(null);
1260
+ React__namespace["default"].useEffect(() => {
1261
+ if (isOpen && modalRef.current) {
1262
+ modalRef.current.focus();
1263
+ }
1264
+ }, [isOpen]);
1265
+ const handleKeyDown = (event) => {
1266
+ if (event.key === 'Escape') {
1267
+ onCancel?.();
1268
+ }
1269
+ };
1270
+ if (!isOpen)
1271
+ return null;
1272
+ return (React__namespace["default"].createElement("div", { ref: modalRef, className: "amplify-modal__overlay", role: "dialog", "aria-modal": "true", tabIndex: -1, onKeyDown: handleKeyDown, onClick: (e) => {
1273
+ if (e.target === e.currentTarget) {
1274
+ onCancel?.();
1275
+ }
1276
+ } },
1277
+ React__namespace["default"].createElement(ViewElement, { className: "amplify-modal__content", role: "document" },
1278
+ React__namespace["default"].createElement(HeadingElement, { className: "amplify-modal__title" }, title),
1279
+ message && (React__namespace["default"].createElement(TextElement, { className: "amplify-modal__body" }, message)),
1280
+ content && (React__namespace["default"].createElement(ViewElement, { className: "amplify-modal__body" }, content)),
1281
+ React__namespace["default"].createElement(ViewElement, { className: "amplify-modal__footer" },
1282
+ React__namespace["default"].createElement(ButtonElement, { className: "amplify-modal__cancel", onClick: onCancel, variant: "cancel" }, cancelLabel),
1283
+ React__namespace["default"].createElement(ButtonElement, { className: "amplify-modal__confirm", onClick: onConfirm, variant: "primary" }, confirmLabel)))));
1284
+ };
1285
+
957
1286
  const ActionDestination$1 = ({ isNavigable, items, label, }) => {
958
1287
  if (!items.length) {
959
1288
  return null;
@@ -1580,7 +1909,7 @@ const DEPRECATED_PROP_KEYS = ['actionType', 'location', 'path'];
1580
1909
  const template = (key, index, values) => `\`${key}\`${index < values.length - 1 ? ', ' : ''}`;
1581
1910
  const getMissingLocationKeys = (location) => location === null
1582
1911
  ? []
1583
- : REQUIRED_LOCATION_KEYS.filter((key) => !location[key]);
1912
+ : REQUIRED_LOCATION_KEYS.filter((key) => location[key] === undefined || location[key] === null);
1584
1913
  let didWarnConflictingBehavior = false;
1585
1914
  let didWarnDeprecatedAndConflictingProps = false;
1586
1915
  let didWarnDeprecatedProps = false;
@@ -1719,8 +2048,10 @@ const USE_LIST_ERROR_MESSAGE = '`useList` must be called from within `StorageBro
1719
2048
  const INITIAL_STATUS_COUNTS = {
1720
2049
  CANCELED: 0,
1721
2050
  COMPLETE: 0,
2051
+ LOADED: 0,
1722
2052
  FAILED: 0,
1723
2053
  PENDING: 0,
2054
+ FINISHING: 0,
1724
2055
  OVERWRITE_PREVENTED: 0,
1725
2056
  QUEUED: 0,
1726
2057
  TOTAL: 0,
@@ -1731,7 +2062,10 @@ const isProcessingTasks = (statusCounts) => {
1731
2062
  if (statusCounts.TOTAL === 0 || statusCounts.TOTAL === statusCounts.QUEUED) {
1732
2063
  return false;
1733
2064
  }
1734
- return !(statusCounts.QUEUED === 0 && statusCounts.PENDING === 0);
2065
+ return !(statusCounts.QUEUED === 0 &&
2066
+ statusCounts.PENDING === 0 &&
2067
+ statusCounts.LOADED === 0 &&
2068
+ statusCounts.FINISHING === 0);
1735
2069
  };
1736
2070
  const hasCompletedProcessingTasks = (statusCounts) => {
1737
2071
  if (statusCounts.TOTAL === 0 || isProcessingTasks(statusCounts))
@@ -1829,20 +2163,29 @@ function useProcessTasks(handler, options) {
1829
2163
  : [...tasksRef.current.values()].find(({ status }) => status === 'QUEUED') ?? {};
1830
2164
  if (!data)
1831
2165
  return;
2166
+ const all = isSingleTask
2167
+ ? [_input.data]
2168
+ : [...tasksRef.current.values()].map(({ data }) => data);
1832
2169
  const { onTaskCancel, onTaskComplete, onTaskError, onTaskProgress, onTaskSuccess, } = callbacksRef.current;
1833
2170
  const getTask = () => tasksRef.current.get(data.id);
1834
2171
  const { options } = _input;
1835
2172
  const { onProgress: _onProgress } = options ?? {};
1836
- const onProgress = ({ id }, progress) => {
1837
- const task = updateTask(id, { progress });
2173
+ const onProgress = ({ id }, progressDetails, status = 'PENDING') => {
2174
+ const isNumber = typeof progressDetails === 'number';
2175
+ const task = updateTask(id, {
2176
+ progress: isNumber ? progressDetails : progressDetails.progress,
2177
+ failureCount: isNumber ? undefined : progressDetails.failureCount,
2178
+ successCount: isNumber ? undefined : progressDetails.successCount,
2179
+ status,
2180
+ });
1838
2181
  if (task && ui.isFunction(onTaskProgress)) {
1839
- onTaskProgress(task, progress);
2182
+ onTaskProgress(task, progressDetails);
1840
2183
  }
1841
2184
  if (task && ui.isFunction(_onProgress)) {
1842
- _onProgress(data, progress);
2185
+ _onProgress(data, progressDetails, status);
1843
2186
  }
1844
2187
  };
1845
- const input = { ..._input, data, options: { ...options, onProgress } };
2188
+ const input = { ..._input, data, all, options: { ...options, onProgress } };
1846
2189
  const { cancel: _cancel, result } = handler(input);
1847
2190
  const cancel = !_cancel
1848
2191
  ? undefined
@@ -1927,7 +2270,7 @@ function useHandler(handler, options) {
1927
2270
  handleProcessing({
1928
2271
  config,
1929
2272
  ...(hasData
1930
- ? { data: input.data }
2273
+ ? { data: input.data, all: [input.data] }
1931
2274
  : // if no `data` provided, provide `concurrency` to `options`
1932
2275
  { options: { concurrency: DEFAULT_ACTION_CONCURRENCY } }),
1933
2276
  });
@@ -2036,8 +2379,10 @@ const DEFAULT_ACTION_VIEW_DISPLAY_TEXT = {
2036
2379
  actionDestinationLabel: 'Destination',
2037
2380
  statusDisplayCanceledLabel: 'Canceled',
2038
2381
  statusDisplayCompletedLabel: 'Completed',
2382
+ statusDisplayLoadedLabel: 'Completed',
2039
2383
  statusDisplayFailedLabel: 'Failed',
2040
2384
  statusDisplayInProgressLabel: 'In progress',
2385
+ statusDisplayFinishingLabel: 'In progress',
2041
2386
  statusDisplayTotalLabel: 'Total',
2042
2387
  statusDisplayQueuedLabel: 'Not started',
2043
2388
  // empty by default
@@ -2047,6 +2392,7 @@ const DEFAULT_ACTION_VIEW_DISPLAY_TEXT = {
2047
2392
  tableColumnNameHeader: 'Name',
2048
2393
  tableColumnTypeHeader: 'Type',
2049
2394
  tableColumnSizeHeader: 'Size',
2395
+ tableColumnProgressHeader: 'Progress',
2050
2396
  };
2051
2397
  const DEFAULT_LIST_VIEW_DISPLAY_TEXT = {
2052
2398
  loadingIndicatorLabel: 'Loading',
@@ -2139,21 +2485,133 @@ const DEFAULT_COPY_VIEW_DISPLAY_TEXT = {
2139
2485
  searchClearLabel: 'Clear search',
2140
2486
  };
2141
2487
 
2488
+ const pluralize = (count, word) => count === 1 ? word : `${word}s`;
2489
+ const formatCount = (count, word) => `${count === 1 ? '' : 'All '}${count} ${pluralize(count, word)}`;
2142
2490
  const DEFAULT_DELETE_VIEW_DISPLAY_TEXT = {
2143
2491
  ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2144
2492
  title: 'Delete',
2145
2493
  actionStartLabel: 'Delete',
2494
+ confirmationModalTitle: 'Confirm Deletion',
2495
+ confirmationModalConfirmLabel: 'Delete',
2496
+ confirmationModalCancelLabel: 'Cancel',
2497
+ confirmationModalMessage: 'The items that will be deleted contain {count} folder{plural}',
2498
+ confirmationModalFolderListTitle: 'Folder list:',
2146
2499
  getActionCompleteMessage: (data) => {
2147
- const { counts } = data ?? {};
2148
- const { COMPLETE, FAILED, TOTAL } = counts ?? {};
2500
+ const { counts, tasks } = data ?? {};
2501
+ const { COMPLETE = 0, FAILED = 0, TOTAL = 0 } = counts ?? {};
2502
+ if (!TOTAL || TOTAL === 0) {
2503
+ return { content: 'No items to delete.', type: 'info' };
2504
+ }
2505
+ if (tasks && tasks.length > 0) {
2506
+ const folderTasks = tasks.filter((task) => task.data.type === 'FOLDER');
2507
+ const fileTasks = tasks.filter((task) => task.data.type === 'FILE');
2508
+ const completeFolders = folderTasks.filter((task) => task.status === 'COMPLETE').length;
2509
+ const failedFolders = folderTasks.filter((task) => task.status === 'FAILED').length;
2510
+ const completeFiles = fileTasks.filter((task) => task.status === 'COMPLETE').length;
2511
+ const failedFiles = fileTasks.filter((task) => task.status === 'FAILED').length;
2512
+ const hasFolders = folderTasks.length > 0;
2513
+ const hasFiles = fileTasks.length > 0;
2514
+ const isMixed = hasFolders && hasFiles;
2515
+ // All successful
2516
+ if (COMPLETE === TOTAL) {
2517
+ if (isMixed) {
2518
+ return {
2519
+ content: `${formatCount(completeFolders, 'folder')} and ${completeFiles} ${pluralize(completeFiles, 'file')} deleted successfully.`,
2520
+ type: 'success',
2521
+ };
2522
+ }
2523
+ else if (hasFolders) {
2524
+ return {
2525
+ content: `${formatCount(completeFolders, 'folder')} deleted successfully.`,
2526
+ type: 'success',
2527
+ };
2528
+ }
2529
+ else {
2530
+ return {
2531
+ content: `${formatCount(completeFiles, 'file')} deleted successfully.`,
2532
+ type: 'success',
2533
+ };
2534
+ }
2535
+ }
2536
+ // Complete failure
2537
+ if (FAILED === TOTAL) {
2538
+ if (isMixed) {
2539
+ return {
2540
+ content: `Failed to delete ${failedFolders} ${pluralize(failedFolders, 'folder')} and ${failedFiles} ${pluralize(failedFiles, 'file')}. Some contents may have been deleted.`,
2541
+ type: 'error',
2542
+ };
2543
+ }
2544
+ else if (hasFolders) {
2545
+ return {
2546
+ content: `Failed to delete ${failedFolders} ${pluralize(failedFolders, 'folder')}. Some items may have been deleted.`,
2547
+ type: 'error',
2548
+ };
2549
+ }
2550
+ else {
2551
+ return {
2552
+ content: `Failed to delete ${failedFiles} ${pluralize(failedFiles, 'file')}.`,
2553
+ type: 'error',
2554
+ };
2555
+ }
2556
+ }
2557
+ // Partial failure
2558
+ if (isMixed) {
2559
+ const messages = [];
2560
+ if (completeFiles > 0) {
2561
+ messages.push(`${completeFiles} ${pluralize(completeFiles, 'file')} deleted`);
2562
+ }
2563
+ if (completeFolders > 0) {
2564
+ messages.push(`${completeFolders} ${pluralize(completeFolders, 'folder')} deleted`);
2565
+ }
2566
+ if (failedFiles > 0) {
2567
+ messages.push(`${failedFiles} ${pluralize(failedFiles, 'file')} failed`);
2568
+ }
2569
+ if (failedFolders > 0) {
2570
+ messages.push(`${failedFolders} ${pluralize(failedFolders, 'folder')} failed`);
2571
+ }
2572
+ return {
2573
+ content: messages.join(', ') + '. Some items may have been deleted.',
2574
+ type: 'error',
2575
+ };
2576
+ }
2577
+ else if (hasFolders) {
2578
+ // Folders only partial failure
2579
+ if (completeFolders > 0) {
2580
+ return {
2581
+ content: `${completeFolders} ${pluralize(completeFolders, 'folder')} deleted, ${failedFolders} ${pluralize(failedFolders, 'folder')} failed. Some items may have been deleted.`,
2582
+ type: 'error',
2583
+ };
2584
+ }
2585
+ else {
2586
+ return {
2587
+ content: `Failed to delete ${failedFolders} ${pluralize(failedFolders, 'folder')}. Some items may have been deleted.`,
2588
+ type: 'error',
2589
+ };
2590
+ }
2591
+ }
2592
+ else {
2593
+ // Files only partial failure
2594
+ return {
2595
+ content: `${completeFiles} ${pluralize(completeFiles, 'file')} deleted, ${failedFiles} ${pluralize(failedFiles, 'file')} failed.`,
2596
+ type: 'error',
2597
+ };
2598
+ }
2599
+ }
2600
+ // Fallback to generic messaging if tasks not available
2149
2601
  if (COMPLETE === TOTAL) {
2150
- return { content: 'All files deleted.', type: 'success' };
2602
+ return {
2603
+ content: `${formatCount(TOTAL, 'item')} deleted successfully.`,
2604
+ type: 'success',
2605
+ };
2151
2606
  }
2152
2607
  if (FAILED === TOTAL) {
2153
- return { content: 'All files failed to delete.', type: 'error' };
2608
+ return {
2609
+ content: `Failed to delete ${formatCount(TOTAL, 'item')}.`,
2610
+ type: 'error',
2611
+ };
2154
2612
  }
2155
2613
  return {
2156
- content: `${COMPLETE} files deleted, ${FAILED} files failed to delete.`,
2614
+ content: `${COMPLETE} ${pluralize(COMPLETE, 'item')} deleted, ${FAILED} ${pluralize(FAILED, 'item')} failed to delete.`,
2157
2615
  type: 'error',
2158
2616
  };
2159
2617
  },
@@ -2228,6 +2686,7 @@ const DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT = {
2228
2686
  tableColumnSizeHeader: 'Size',
2229
2687
  tableColumnTypeHeader: 'Type',
2230
2688
  selectFileLabel: 'Select file',
2689
+ selectFolderLabel: 'Select folder',
2231
2690
  selectAllFilesLabel: 'Select all files',
2232
2691
  getActionListItemLabel: (key = '') => {
2233
2692
  switch (key) {
@@ -2390,6 +2849,8 @@ const DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT = {
2390
2849
  ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2391
2850
  title: 'Download',
2392
2851
  actionStartLabel: 'Download',
2852
+ statusDisplayFinishingLabel: 'Zipping',
2853
+ statusDisplayLoadedLabel: 'Loaded',
2393
2854
  getActionCompleteMessage: (data) => {
2394
2855
  const { counts } = data ?? {};
2395
2856
  const { COMPLETE, FAILED, TOTAL } = counts ?? {};
@@ -3048,6 +3509,7 @@ const Title$1 = ({ title }) => (React__namespace["default"].createElement(Headin
3048
3509
 
3049
3510
  const DEFAULT_COMPOSABLES = {
3050
3511
  ActionCancel,
3512
+ ActionConfirmationModal,
3051
3513
  ActionDestination: ActionDestination$1,
3052
3514
  ActionExit,
3053
3515
  ActionStart,
@@ -3238,6 +3700,26 @@ const ActionCancelControl = () => {
3238
3700
  return React__namespace["default"].createElement(Resolved, { ...props });
3239
3701
  };
3240
3702
 
3703
+ const useActionConfirmationModal = () => {
3704
+ const { data: { confirmationModal = {} }, onConfirmationModalConfirm = () => { }, onConfirmationModalCancel = () => { }, } = useControlsContext();
3705
+ return {
3706
+ isOpen: false,
3707
+ title: 'Confirm Action',
3708
+ message: '',
3709
+ confirmLabel: 'Confirm',
3710
+ cancelLabel: 'Cancel',
3711
+ ...confirmationModal,
3712
+ onConfirm: onConfirmationModalConfirm,
3713
+ onCancel: onConfirmationModalCancel,
3714
+ };
3715
+ };
3716
+
3717
+ const ActionConfirmationModalControl = () => {
3718
+ const props = useActionConfirmationModal();
3719
+ const Resolved = useResolvedComposable(ActionConfirmationModal, 'ActionConfirmationModal');
3720
+ return React__namespace["default"].createElement(Resolved, { ...props });
3721
+ };
3722
+
3241
3723
  const getNavigationItems = ({ destinationParts, location, onNavigate, }) => {
3242
3724
  const { bucket, permissions, prefix = '', type } = location;
3243
3725
  const destinationSubpaths = [];
@@ -3746,15 +4228,19 @@ function useResolveTableData(keys, { getCell, getHeader, getRowKey }, { items, p
3746
4228
 
3747
4229
  const STATUS_LABELS = {
3748
4230
  PENDING: 'statusDisplayInProgressLabel',
4231
+ FINISHING: 'statusDisplayFinishingLabel',
3749
4232
  CANCELED: 'statusDisplayCanceledLabel',
3750
4233
  COMPLETE: 'statusDisplayCompletedLabel',
4234
+ LOADED: 'statusDisplayLoadedLabel',
3751
4235
  FAILED: 'statusDisplayFailedLabel',
3752
4236
  QUEUED: 'statusDisplayQueuedLabel',
3753
4237
  OVERWRITE_PREVENTED: 'statusDisplayOverwritePreventedLabel',
3754
4238
  };
3755
4239
  const STATUS_ICONS = {
3756
4240
  PENDING: 'action-progress',
4241
+ FINISHING: 'action-progress',
3757
4242
  COMPLETE: 'action-success',
4243
+ LOADED: 'action-success',
3758
4244
  FAILED: 'action-error',
3759
4245
  OVERWRITE_PREVENTED: 'action-info',
3760
4246
  CANCELED: 'action-canceled',
@@ -3768,9 +4254,18 @@ const FILE_DATA_ITEM_TABLE_KEYS = [
3768
4254
  'status',
3769
4255
  'cancel',
3770
4256
  ];
4257
+ const DELETE_TABLE_KEYS = [
4258
+ 'name',
4259
+ 'folder',
4260
+ 'type',
4261
+ 'size',
4262
+ 'status',
4263
+ 'progress',
4264
+ 'cancel',
4265
+ ];
3771
4266
 
3772
- const getFileType = (value, fallback = '') => value.lastIndexOf('.') !== -1
3773
- ? value.slice(value.lastIndexOf('.') + 1)
4267
+ const getFileType = (value, fallback = '') => value?.lastIndexOf?.('.') !== -1
4268
+ ? value?.slice(value?.lastIndexOf?.('.') + 1)
3774
4269
  : fallback;
3775
4270
  const getCellName = (value) =>
3776
4271
  // `value.split` always returns an array with at least one entry
@@ -3785,7 +4280,7 @@ const getFileDataCellFolder = (task) => {
3785
4280
  ? task.data.sourceKey
3786
4281
  : task.data.key;
3787
4282
  const { fileKey } = task.data;
3788
- return targetKey.slice(0, -fileKey.length);
4283
+ return targetKey.slice(0, -fileKey?.length);
3789
4284
  };
3790
4285
  const getUploadCellProgress = ({ progress, status, }) => {
3791
4286
  // prefer `progress` if available, 1 if status is complete, default 0
@@ -3795,7 +4290,7 @@ const getUploadCellProgress = ({ progress, status, }) => {
3795
4290
  };
3796
4291
  const getDownloadCellProgress = ({ progress, status, }) => {
3797
4292
  // prefer `progress` if available, 1 if status is complete, default 0
3798
- const value = progress ?? (status === 'COMPLETE' ? 1 : 0);
4293
+ const value = progress ?? (status === 'LOADED' || status === 'COMPLETE' ? 1 : 0);
3799
4294
  const displayValue = `${Math.round(value * 100)}%`;
3800
4295
  return { displayValue, value };
3801
4296
  };
@@ -3830,31 +4325,31 @@ const getFileDataCancelCellContent = (data) => {
3830
4325
  * Generates a unique key for a table cell based on the key and item id
3831
4326
  */
3832
4327
  const getFileDataCellKey = ({ key, item, }) => `${key}-${item.data.id}`;
3833
- const name$2 = (data) => {
4328
+ const name$3 = (data) => {
3834
4329
  const key = getFileDataCellKey(data);
3835
4330
  const { item } = data;
3836
4331
  const text = item.data.fileKey;
3837
4332
  const icon = STATUS_ICONS[item.status];
3838
4333
  return { key, type: 'text', content: { icon, text } };
3839
4334
  };
3840
- const folder$2 = (data) => {
4335
+ const folder$3 = (data) => {
3841
4336
  const key = getFileDataCellKey(data);
3842
4337
  const text = getFileDataCellFolder(data.item);
3843
4338
  return { key, type: 'text', content: { text } };
3844
4339
  };
3845
- const type$2 = (data) => {
4340
+ const type$3 = (data) => {
3846
4341
  const key = getFileDataCellKey(data);
3847
4342
  const { fileKey } = data.item.data;
3848
4343
  const text = getFileType(fileKey);
3849
4344
  return { key, type: 'text', content: { text } };
3850
4345
  };
3851
- const size$2 = (data) => {
4346
+ const size$3 = (data) => {
3852
4347
  const key = getFileDataCellKey(data);
3853
4348
  const { size: value } = data.item.data;
3854
4349
  const displayValue = getFileSize(value);
3855
4350
  return { key, type: 'number', content: { value, displayValue } };
3856
4351
  };
3857
- const cancel$2 = (data) => {
4352
+ const cancel$3 = (data) => {
3858
4353
  const key = getFileDataCellKey(data);
3859
4354
  const content = getFileDataCancelCellContent(data);
3860
4355
  return { key, type: 'button', content };
@@ -3870,12 +4365,12 @@ const status$3 = (data) => {
3870
4365
  return { key, type: 'text', content: { text } };
3871
4366
  };
3872
4367
  const COPY_CELL_RESOLVERS = {
3873
- name: name$2,
3874
- folder: folder$2,
3875
- type: type$2,
3876
- size: size$2,
4368
+ name: name$3,
4369
+ folder: folder$3,
4370
+ type: type$3,
4371
+ size: size$3,
3877
4372
  status: status$3,
3878
- cancel: cancel$2,
4373
+ cancel: cancel$3,
3879
4374
  /**
3880
4375
  * @deprecated
3881
4376
  *
@@ -3897,8 +4392,160 @@ const COPY_TABLE_RESOLVERS = {
3897
4392
  getRowKey: ({ item }) => item.data.id,
3898
4393
  };
3899
4394
 
4395
+ const getFolderName = (folderKey) => {
4396
+ return folderKey.replace(/\/$/, '').split('/').pop() ?? folderKey;
4397
+ };
4398
+ /**
4399
+ * Creates JSX content showing a list of folders to be deleted
4400
+ */
4401
+ const createFolderListContent = (folders, folderListTitle) => {
4402
+ return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null,
4403
+ React__namespace["default"].createElement(TextElement, { className: "amplify-modal__list-title" },
4404
+ React__namespace["default"].createElement("strong", null, folderListTitle)),
4405
+ React__namespace["default"].createElement(UnorderedListElement, { className: "amplify-modal__list" }, folders.map((folder) => (React__namespace["default"].createElement(ListItemElement, { key: folder.id, className: "amplify-modal__list-item" }, getFolderName(folder.key)))))));
4406
+ };
4407
+ const createDeleteConfirmationModalProps = ({ items, showConfirmation, displayText, }) => {
4408
+ const folders = getSelectedFolders(items);
4409
+ const folderCount = folders.length;
4410
+ return {
4411
+ isOpen: showConfirmation,
4412
+ title: displayText.confirmationModalTitle,
4413
+ message: displayText.confirmationModalMessage
4414
+ .replace('{count}', folderCount.toString())
4415
+ .replace('{plural}', folderCount !== 1 ? 's' : ''),
4416
+ confirmLabel: displayText.confirmationModalConfirmLabel,
4417
+ cancelLabel: displayText.confirmationModalCancelLabel,
4418
+ content: createFolderListContent(folders, displayText.confirmationModalFolderListTitle),
4419
+ };
4420
+ };
4421
+ /**
4422
+ * Maximum number of files to count before showing "+" notation
4423
+ * This prevents expensive operations on very large folders
4424
+ */
4425
+ const MAX_FILE_COUNT_LIMIT = 5000;
4426
+ const LIST_PAGE_SIZE = 1000;
4427
+ /**
4428
+ * Count the total number of files in a folder with pagination and limits
4429
+ * @param folderKey - The folder path to count files in
4430
+ * @param config - Storage configuration
4431
+ * @returns Promise<number | string> - File count or "5000+" if exceeds limit
4432
+ */
4433
+ const countFilesInFolder = async (folderKey, config) => {
4434
+ try {
4435
+ const { accountId, credentials, customEndpoint } = config;
4436
+ const bucket = constructBucket$1(config);
4437
+ let fileCount = 0;
4438
+ let nextToken;
4439
+ let hasMoreItems = true;
4440
+ while (hasMoreItems && fileCount < MAX_FILE_COUNT_LIMIT) {
4441
+ const { items, nextToken: listNextToken } = await internals.list({
4442
+ path: folderKey,
4443
+ options: {
4444
+ bucket,
4445
+ locationCredentialsProvider: credentials,
4446
+ expectedBucketOwner: accountId,
4447
+ customEndpoint,
4448
+ pageSize: LIST_PAGE_SIZE,
4449
+ nextToken,
4450
+ },
4451
+ });
4452
+ const batchFileCount = items.filter((item) => !item.path.endsWith('/')).length;
4453
+ fileCount += batchFileCount;
4454
+ nextToken = listNextToken;
4455
+ hasMoreItems = !!nextToken;
4456
+ if (fileCount >= MAX_FILE_COUNT_LIMIT) {
4457
+ return `${MAX_FILE_COUNT_LIMIT}+`;
4458
+ }
4459
+ }
4460
+ if (hasMoreItems) {
4461
+ return `${fileCount}+`;
4462
+ }
4463
+ return fileCount;
4464
+ }
4465
+ catch (error) {
4466
+ return 0;
4467
+ }
4468
+ };
4469
+
4470
+ const getDeleteCellKey = (data) => `${data.key}-${data.item.data.id}`;
4471
+ const getCellDataFolder = (data) => {
4472
+ const { type, key } = data;
4473
+ const fileKey = getFileKey(key);
4474
+ const targetKey = data.key;
4475
+ if (type === 'FOLDER') {
4476
+ const pathWithoutTrailingSlash = targetKey.replace(/\/$/, '');
4477
+ const lastSlashIndex = pathWithoutTrailingSlash.lastIndexOf('/');
4478
+ return lastSlashIndex >= 0
4479
+ ? pathWithoutTrailingSlash.slice(0, lastSlashIndex + 1)
4480
+ : '';
4481
+ }
4482
+ return targetKey.slice(0, -fileKey?.length);
4483
+ };
4484
+ const name$2 = (data) => {
4485
+ const key = getDeleteCellKey(data);
4486
+ const { item } = data;
4487
+ let text;
4488
+ if (item.data.type === 'FOLDER') {
4489
+ text = `${getFolderName(item.data.key)}/`;
4490
+ }
4491
+ else {
4492
+ text = item.data.fileKey ?? getCellName(item.data.key);
4493
+ }
4494
+ const icon = STATUS_ICONS[item.status];
4495
+ return { key, type: 'text', content: { icon, text } };
4496
+ };
4497
+ const folder$2 = (data) => {
4498
+ const key = getDeleteCellKey(data);
4499
+ const text = getCellDataFolder(data.item.data);
4500
+ return { key, type: 'text', content: { text } };
4501
+ };
4502
+ const type$2 = (data) => {
4503
+ const key = getDeleteCellKey(data);
4504
+ if (data.item.data.type === 'FOLDER') {
4505
+ return { key, type: 'text', content: { text: 'Folder' } };
4506
+ }
4507
+ const text = getFileType(data.item.data.key);
4508
+ return { key, type: 'text', content: { text } };
4509
+ };
4510
+ const size$2 = (data) => {
4511
+ const key = getDeleteCellKey(data);
4512
+ const itemData = data.item.data;
4513
+ if (data.item.data.type === 'FOLDER') {
4514
+ return { key, type: 'text', content: { text: '-' } };
4515
+ }
4516
+ const value = 'size' in itemData ? itemData.size : 0;
4517
+ const displayValue = getFileSize(value);
4518
+ return { key, type: 'number', content: { value, displayValue } };
4519
+ };
4520
+ const getCancelCellContent = (data) => {
4521
+ const { item, props } = data;
4522
+ const { cancel, status } = item;
4523
+ const { isProcessing, onTaskRemove } = props;
4524
+ const isQueued = status === 'QUEUED';
4525
+ const isRemovable = isQueued && !isProcessing;
4526
+ const isCancelable = isProcessing && !!cancel;
4527
+ const itemAriaValue = getCellName(item.data.fileKey ?? item.data.key);
4528
+ const ariaLabel = `${isRemovable ? 'Remove' : 'Cancel'} item: ${itemAriaValue}`;
4529
+ const isDisabled = !isRemovable && !isCancelable;
4530
+ const onClick = !isCancelable && !isRemovable
4531
+ ? undefined
4532
+ : () => {
4533
+ if (isRemovable) {
4534
+ onTaskRemove?.(item);
4535
+ return;
4536
+ }
4537
+ if (isCancelable)
4538
+ cancel();
4539
+ };
4540
+ return { ariaLabel, isDisabled, onClick, icon: 'cancel' };
4541
+ };
4542
+ const cancel$2 = (data) => {
4543
+ const key = getDeleteCellKey(data);
4544
+ const content = getCancelCellContent(data);
4545
+ return { key, type: 'button', content };
4546
+ };
3900
4547
  const status$2 = (data) => {
3901
- const key = getFileDataCellKey(data);
4548
+ const key = getDeleteCellKey(data);
3902
4549
  const { item: { status }, props: { displayText }, } = data;
3903
4550
  const statusLabelKey = STATUS_LABELS[status];
3904
4551
  const text = isDeleteViewDisplayTextKey(statusLabelKey)
@@ -3906,21 +4553,47 @@ const status$2 = (data) => {
3906
4553
  : '';
3907
4554
  return { key, type: 'text', content: { text } };
3908
4555
  };
4556
+ const progress$2 = (data) => {
4557
+ const key = getDeleteCellKey(data);
4558
+ const { item } = data;
4559
+ const itemIsFile = item.data.type === 'FILE';
4560
+ if (itemIsFile) {
4561
+ const text = item.status === 'COMPLETE' ? 'Deleted' : '-';
4562
+ return { key, type: 'text', content: { text } };
4563
+ }
4564
+ if ((item.status === 'PENDING' ||
4565
+ item.status === 'FAILED' ||
4566
+ item.status === 'CANCELED') &&
4567
+ item.data.totalCount !== undefined) {
4568
+ const countDisplay = item.data.totalCount ?? '?';
4569
+ const text = `${item.successCount ?? 0}/${countDisplay} files`;
4570
+ return { key, type: 'text', content: { text } };
4571
+ }
4572
+ else if (item.status === 'COMPLETE') {
4573
+ if (item.data.totalCount === null) {
4574
+ const text = `${item.successCount ?? 0} files deleted`;
4575
+ return { key, type: 'text', content: { text } };
4576
+ }
4577
+ const text = `${item.data.totalCount} files deleted`;
4578
+ return { key, type: 'text', content: { text } };
4579
+ }
4580
+ else {
4581
+ const text = item.data.totalCount === null
4582
+ ? 'Count failed'
4583
+ : item.data.totalCount !== undefined
4584
+ ? `${item.data.totalCount} files`
4585
+ : 'Calculating...';
4586
+ return { key, type: 'text', content: { text } };
4587
+ }
4588
+ };
3909
4589
  const DELETE_CELL_RESOLVERS = {
3910
4590
  name: name$2,
3911
4591
  folder: folder$2,
3912
4592
  type: type$2,
3913
4593
  size: size$2,
3914
4594
  status: status$2,
4595
+ progress: progress$2,
3915
4596
  cancel: cancel$2,
3916
- /**
3917
- * @deprecated
3918
- *
3919
- * non-upload view tables do not include "progress" headers but include here to
3920
- * keep TS happy as "progress" headers were included in display text interfaces
3921
- * and cannot be removed from the tables without a breaking change
3922
- */
3923
- progress: ui.noop,
3924
4597
  };
3925
4598
  const DELETE_TABLE_RESOLVERS = {
3926
4599
  getCell: (data) => DELETE_CELL_RESOLVERS[data.key](data),
@@ -4677,6 +5350,7 @@ function CopyViewProvider({ children, ...props }) {
4677
5350
  }
4678
5351
 
4679
5352
  const DEFAULT_STATE = {
5353
+ dataItems: undefined,
4680
5354
  fileDataItems: undefined,
4681
5355
  };
4682
5356
  const locationItemsReducer = (prevState, event) => {
@@ -4685,27 +5359,29 @@ const locationItemsReducer = (prevState, event) => {
4685
5359
  const { items } = event;
4686
5360
  if (!items?.length)
4687
5361
  return prevState;
4688
- if (!prevState.fileDataItems?.length) {
4689
- return { fileDataItems: items.map(createFileDataItem) };
4690
- }
4691
- const nextFileDataItems = items?.reduce((fileDataItems, data) => prevState.fileDataItems?.some(({ id }) => id === data.id)
4692
- ? fileDataItems
4693
- : fileDataItems.concat(createFileDataItem(data)), []);
4694
- if (!nextFileDataItems?.length)
4695
- return prevState;
5362
+ const nextDataItems = !prevState.dataItems?.length
5363
+ ? items
5364
+ : prevState.dataItems.concat(items.filter((data) => !prevState.dataItems?.some(({ id }) => id === data.id)));
5365
+ const fileItems = items.filter((item) => item.type === 'FILE');
5366
+ const nextFileDataItems = !prevState.fileDataItems?.length
5367
+ ? fileItems.map(createFileDataItem)
5368
+ : prevState.fileDataItems.concat(fileItems
5369
+ .filter((data) => !prevState.fileDataItems?.some(({ id }) => id === data.id))
5370
+ .map(createFileDataItem));
4696
5371
  return {
4697
- fileDataItems: prevState.fileDataItems.concat(nextFileDataItems),
5372
+ dataItems: nextDataItems,
5373
+ fileDataItems: nextFileDataItems,
4698
5374
  };
4699
5375
  }
4700
5376
  case 'REMOVE_LOCATION_ITEM': {
4701
5377
  const { id } = event;
4702
- if (!prevState.fileDataItems)
4703
- return prevState;
4704
- const fileDataItems = prevState.fileDataItems.filter((item) => item.id !== id);
4705
- if (fileDataItems.length === prevState.fileDataItems.length) {
5378
+ const dataItems = prevState.dataItems?.filter((item) => item.id !== id);
5379
+ const fileDataItems = prevState.fileDataItems?.filter((item) => item.id !== id);
5380
+ if (dataItems?.length === prevState.dataItems?.length &&
5381
+ fileDataItems?.length === prevState.fileDataItems?.length) {
4706
5382
  return prevState;
4707
5383
  }
4708
- return { fileDataItems };
5384
+ return { dataItems, fileDataItems };
4709
5385
  }
4710
5386
  case 'RESET_LOCATION_ITEMS': {
4711
5387
  return DEFAULT_STATE;
@@ -4941,12 +5617,12 @@ CopyView.Title = TitleControl;
4941
5617
  function DeleteViewProvider({ children, ...props }) {
4942
5618
  const { DeleteView: displayText } = useDisplayText();
4943
5619
  const { actionCancelLabel, actionExitLabel, actionStartLabel, title, statusDisplayCanceledLabel, statusDisplayCompletedLabel, statusDisplayFailedLabel, statusDisplayQueuedLabel, getActionCompleteMessage, } = displayText;
4944
- const { isProcessing, isProcessingComplete, statusCounts, tasks: items, onActionCancel, onActionStart, onActionExit, onTaskRemove, } = props;
5620
+ const { isProcessing, isProcessingComplete, statusCounts, tasks, confirmationModal, onActionCancel, onActionStart, onActionExit, onTaskRemove, onConfirmDelete, onCancelConfirmation, } = props;
4945
5621
  const message = isProcessingComplete
4946
- ? getActionCompleteMessage({ counts: statusCounts })
5622
+ ? getActionCompleteMessage({ counts: statusCounts, tasks })
4947
5623
  : undefined;
4948
- const tableData = useResolveTableData(FILE_DATA_ITEM_TABLE_KEYS, DELETE_TABLE_RESOLVERS, {
4949
- items,
5624
+ const tableData = useResolveTableData(DELETE_TABLE_KEYS, DELETE_TABLE_RESOLVERS, {
5625
+ items: tasks,
4950
5626
  props: { displayText, isProcessing, onTaskRemove },
4951
5627
  });
4952
5628
  return (React__namespace["default"].createElement(ControlsContextProvider, { data: {
@@ -4964,24 +5640,80 @@ function DeleteViewProvider({ children, ...props }) {
4964
5640
  tableData,
4965
5641
  title,
4966
5642
  message,
4967
- }, onActionStart: onActionStart, onActionExit: onActionExit, onActionCancel: onActionCancel }, children));
5643
+ confirmationModal,
5644
+ }, onActionStart: onActionStart, onActionExit: onActionExit, onActionCancel: onActionCancel, onConfirmationModalConfirm: onConfirmDelete, onConfirmationModalCancel: onCancelConfirmation }, children));
4968
5645
  }
4969
5646
 
4970
5647
  // assign to constant to ensure referential equality
4971
5648
  const EMPTY_ITEMS$1 = [];
4972
5649
  const useDeleteView = (options) => {
4973
5650
  const { onExit: _onExit } = options ?? {};
5651
+ const { DeleteView: displayText } = useDisplayText();
4974
5652
  const [{ location }, storeDispatch] = useStore();
4975
5653
  const [locationItems, locationItemsDispatch] = useLocationItems();
4976
5654
  const { current } = location;
4977
- const { fileDataItems: items = EMPTY_ITEMS$1 } = locationItems;
4978
- const [processState, handleProcess] = useAction('delete', { items });
5655
+ const { dataItems = EMPTY_ITEMS$1 } = locationItems;
5656
+ const getConfig = useGetActionInput();
5657
+ const [itemsWithCount, setItemsWithCount] = React__namespace["default"].useState(dataItems);
5658
+ const folderCountsRef = React__namespace["default"].useRef(new Map());
5659
+ // Sync itemsWithCount with dataItems when dataItems changes (e.g., item removal)
5660
+ React__namespace["default"].useEffect(() => {
5661
+ const itemsWithAppliedCounts = dataItems.map((item) => ({
5662
+ ...item,
5663
+ totalCount: folderCountsRef.current.get(item.id),
5664
+ }));
5665
+ setItemsWithCount(itemsWithAppliedCounts);
5666
+ }, [dataItems]);
5667
+ const [processState, handleProcess] = useAction('delete', {
5668
+ items: itemsWithCount,
5669
+ });
5670
+ const [showConfirmation, setShowConfirmation] = React__namespace["default"].useState(false);
4979
5671
  const { isProcessing, isProcessingComplete, statusCounts, tasks } = processState;
5672
+ const selectionSummary = getSelectionSummary(dataItems);
5673
+ const { hasFolders } = selectionSummary;
5674
+ React__namespace["default"].useEffect(() => {
5675
+ const initializeFolderCounts = async () => {
5676
+ if (!selectionSummary.hasFolders || !current) {
5677
+ return;
5678
+ }
5679
+ const config = getConfig(current);
5680
+ const foldersToCount = dataItems.filter((item) => item.type === 'FOLDER' && !folderCountsRef.current.has(item.id));
5681
+ if (foldersToCount.length === 0) {
5682
+ return;
5683
+ }
5684
+ await Promise.all(foldersToCount.map(async (folder) => {
5685
+ try {
5686
+ const totalCount = await countFilesInFolder(folder.key, config);
5687
+ folderCountsRef.current.set(folder.id, totalCount);
5688
+ }
5689
+ catch (error) {
5690
+ folderCountsRef.current.set(folder.id, null);
5691
+ }
5692
+ }));
5693
+ setItemsWithCount((currentItems) => currentItems.map((item) => ({
5694
+ ...item,
5695
+ totalCount: folderCountsRef.current.get(item.id),
5696
+ })));
5697
+ };
5698
+ initializeFolderCounts();
5699
+ }, [current, getConfig, selectionSummary.hasFolders, dataItems]);
4980
5700
  const onActionStart = () => {
4981
5701
  if (!current)
4982
5702
  return;
5703
+ if (hasFolders) {
5704
+ setShowConfirmation(true);
5705
+ }
5706
+ else {
5707
+ handleProcess();
5708
+ }
5709
+ };
5710
+ const onConfirmDelete = () => {
5711
+ setShowConfirmation(false);
4983
5712
  handleProcess();
4984
5713
  };
5714
+ const onCancelConfirmation = () => {
5715
+ setShowConfirmation(false);
5716
+ };
4985
5717
  const onActionCancel = () => {
4986
5718
  tasks.forEach((task) => {
4987
5719
  // @TODO Fixme, calling cancel on task doesn't currently work
@@ -5000,6 +5732,11 @@ const useDeleteView = (options) => {
5000
5732
  const onTaskRemove = React__namespace["default"].useCallback(({ data }) => {
5001
5733
  locationItemsDispatch({ type: 'REMOVE_LOCATION_ITEM', id: data.id });
5002
5734
  }, [locationItemsDispatch]);
5735
+ const confirmationModal = React__namespace["default"].useMemo(() => createDeleteConfirmationModalProps({
5736
+ items: dataItems,
5737
+ showConfirmation,
5738
+ displayText,
5739
+ }), [dataItems, showConfirmation, displayText]);
5003
5740
  return {
5004
5741
  isProcessing,
5005
5742
  isProcessingComplete,
@@ -5010,6 +5747,9 @@ const useDeleteView = (options) => {
5010
5747
  onActionExit,
5011
5748
  onActionStart,
5012
5749
  onTaskRemove,
5750
+ onConfirmDelete,
5751
+ onCancelConfirmation,
5752
+ confirmationModal,
5013
5753
  };
5014
5754
  };
5015
5755
 
@@ -5028,11 +5768,13 @@ const DeleteView = ({ className, ...props }) => {
5028
5768
  React__namespace["default"].createElement(MessageControl, null)),
5029
5769
  React__namespace["default"].createElement(ViewElement, { className: `${STORAGE_BROWSER_BLOCK}__buttons` },
5030
5770
  React__namespace["default"].createElement(ActionCancelControl, null),
5031
- React__namespace["default"].createElement(ActionStartControl, null))))));
5771
+ React__namespace["default"].createElement(ActionStartControl, null))),
5772
+ React__namespace["default"].createElement(ActionConfirmationModalControl, null))));
5032
5773
  };
5033
5774
  DeleteView.displayName = 'DeleteView';
5034
5775
  DeleteView.Provider = DeleteViewProvider;
5035
5776
  DeleteView.Cancel = ActionCancelControl;
5777
+ DeleteView.ConfirmationModal = ActionConfirmationModalControl;
5036
5778
  DeleteView.Exit = ActionExitControl;
5037
5779
  DeleteView.Message = MessageControl;
5038
5780
  DeleteView.Start = ActionStartControl;
@@ -5246,34 +5988,45 @@ const getFileRowContent = ({ filePreviewEnabled, permissions, isSelected, itemLo
5246
5988
  }
5247
5989
  });
5248
5990
 
5249
- const getFolderRowContent = ({ itemSubPath, rowId, onNavigate, }) => LOCATION_DETAIL_VIEW_HEADERS.map((columnKey) => {
5250
- const key = `${columnKey}-${rowId}`;
5251
- switch (columnKey) {
5252
- case 'checkbox': {
5253
- return { key, type: 'text', content: { text: '' } };
5254
- }
5255
- case 'name': {
5256
- return {
5257
- key,
5258
- type: 'button',
5259
- content: {
5260
- icon: 'folder',
5261
- ariaLabel: itemSubPath,
5262
- label: itemSubPath,
5263
- onClick: onNavigate,
5264
- },
5265
- };
5266
- }
5267
- case 'type': {
5268
- return { key, type: 'text', content: { text: 'Folder' } };
5269
- }
5270
- case 'last-modified':
5271
- case 'size':
5272
- case 'download': {
5273
- return { key, type: 'text', content: { text: '' } };
5991
+ const getFolderRowContent = ({ itemSubPath, rowId, isSelected, selectFolderLabel, onNavigate, onSelect, }) => {
5992
+ return LOCATION_DETAIL_VIEW_HEADERS.map((columnKey) => {
5993
+ const key = `${columnKey}-${rowId}`;
5994
+ switch (columnKey) {
5995
+ case 'checkbox': {
5996
+ return {
5997
+ key,
5998
+ type: 'checkbox',
5999
+ content: {
6000
+ checked: isSelected,
6001
+ id: key,
6002
+ label: `${selectFolderLabel} ${itemSubPath}`,
6003
+ onSelect,
6004
+ },
6005
+ };
6006
+ }
6007
+ case 'name': {
6008
+ return {
6009
+ key,
6010
+ type: 'button',
6011
+ content: {
6012
+ icon: 'folder',
6013
+ ariaLabel: itemSubPath,
6014
+ label: itemSubPath,
6015
+ onClick: onNavigate,
6016
+ },
6017
+ };
6018
+ }
6019
+ case 'type': {
6020
+ return { key, type: 'text', content: { text: 'Folder' } };
6021
+ }
6022
+ case 'last-modified':
6023
+ case 'size':
6024
+ case 'download': {
6025
+ return { key, type: 'text', content: { text: '' } };
6026
+ }
5274
6027
  }
5275
- }
5276
- });
6028
+ });
6029
+ };
5277
6030
 
5278
6031
  const getHeaders$1 = ({ tableColumnLastModifiedHeader, tableColumnNameHeader, tableColumnSizeHeader, tableColumnTypeHeader, areAllFilesSelected, selectAllFilesLabel, onSelectAll, hasFiles, }) => LOCATION_DETAIL_VIEW_HEADERS.map((key) => {
5279
6032
  switch (key) {
@@ -5340,7 +6093,7 @@ const getHeaders$1 = ({ tableColumnLastModifiedHeader, tableColumnNameHeader, ta
5340
6093
  }
5341
6094
  });
5342
6095
 
5343
- const getLocationDetailViewTableData = ({ filePreviewEnabled, activeFile, onSelectActiveFile, areAllFilesSelected, displayText, location, fileDataItems, hasFiles, pageItems, selectFileLabel, selectAllFilesLabel, getDateDisplayValue, onDownload, onNavigate, onSelect, onSelectAll, }) => {
6096
+ const getLocationDetailViewTableData = ({ filePreviewEnabled, activeFile, onSelectActiveFile, areAllFilesSelected, displayText, location, fileDataItems, dataItems, hasFiles, pageItems, selectFileLabel, selectAllFilesLabel, getDateDisplayValue, onDownload, onNavigate, onSelect, onSelectAll, }) => {
5344
6097
  const { tableColumnLastModifiedHeader, tableColumnNameHeader, tableColumnSizeHeader, tableColumnTypeHeader, } = displayText;
5345
6098
  const headers = getHeaders$1({
5346
6099
  areAllFilesSelected,
@@ -5398,6 +6151,10 @@ const getLocationDetailViewTableData = ({ filePreviewEnabled, activeFile, onSele
5398
6151
  }
5399
6152
  onNavigate({ ...current, id }, itemLocationPath);
5400
6153
  };
6154
+ const isSelected = dataItems?.some((item) => item.id === id) ?? false;
6155
+ const onFolderSelect = () => {
6156
+ onSelect(isSelected, locationItem);
6157
+ };
5401
6158
  return {
5402
6159
  key: id,
5403
6160
  active: false,
@@ -5405,6 +6162,9 @@ const getLocationDetailViewTableData = ({ filePreviewEnabled, activeFile, onSele
5405
6162
  itemSubPath,
5406
6163
  rowId: id,
5407
6164
  onNavigate: onFolderNavigate,
6165
+ selectFolderLabel: selectFileLabel,
6166
+ isSelected,
6167
+ onSelect: onFolderSelect,
5408
6168
  }),
5409
6169
  };
5410
6170
  }
@@ -5416,7 +6176,7 @@ const getLocationDetailViewTableData = ({ filePreviewEnabled, activeFile, onSele
5416
6176
  function LocationDetailViewProvider({ children, ...props }) {
5417
6177
  const { LocationDetailView: displayText } = useDisplayText();
5418
6178
  const { LocationDetailView: { loadingIndicatorLabel, searchSubfoldersToggleLabel, selectFileLabel, selectAllFilesLabel, searchPlaceholder, searchSubmitLabel, searchClearLabel, getActionListItemLabel, getDateDisplayValue, getTitle, getListItemsResultMessage, }, } = useDisplayText();
5419
- const { actionItems, activeFile, activeFileHasNext, activeFileHasPrev, page, pageItems, hasNextPage, highestPageVisited, isLoading, isSearchSubfoldersEnabled, location, fileDataItems, hasError, hasDownloadError, message, downloadErrorMessage, searchQuery, hasExhaustedSearch, onActionSelect, onDropFiles, onRefresh, onPaginate, onDownload, onNavigate, onNavigateHome, onSelect, onSelectActiveFile, onToggleSelectAll, onSearch, onSearchQueryChange, onSearchClear, onToggleSearchSubfolders, filePreviewState, filePreviewEnabled, onRetryFilePreview, } = props;
6179
+ const { actionItems, activeFile, activeFileHasNext, activeFileHasPrev, page, pageItems, hasNextPage, highestPageVisited, isLoading, isSearchSubfoldersEnabled, location, fileDataItems, hasError, hasDownloadError, message, downloadErrorMessage, searchQuery, hasExhaustedSearch, onActionSelect, onDropFiles, onRefresh, onPaginate, onDownload, onNavigate, onNavigateHome, onSelect, onSelectActiveFile, onToggleSelectAll, onSearch, onSearchQueryChange, onSearchClear, onToggleSearchSubfolders, filePreviewState, filePreviewEnabled, onRetryFilePreview, dataItems, } = props;
5420
6180
  const actionsWithDisplayText = actionItems.map((item) => ({
5421
6181
  ...item,
5422
6182
  label: getActionListItemLabel(item.label),
@@ -5473,6 +6233,7 @@ function LocationDetailViewProvider({ children, ...props }) {
5473
6233
  onNavigate,
5474
6234
  onSelect,
5475
6235
  onSelectAll: onToggleSelectAll,
6236
+ dataItems,
5476
6237
  }),
5477
6238
  title: getTitle(location),
5478
6239
  message: messageControlContent,
@@ -5536,7 +6297,7 @@ function useFilePreview({ activeFile, }) {
5536
6297
  const config = getConfig(location.current);
5537
6298
  const { accountId, customEndpoint, credentials } = config;
5538
6299
  const sharedOptions = {
5539
- bucket: constructBucket(config),
6300
+ bucket: constructBucket$1(config),
5540
6301
  expectedBucketOwner: accountId,
5541
6302
  };
5542
6303
  try {
@@ -5645,7 +6406,7 @@ const useLocationDetailView = (options) => {
5645
6406
  const [activeFile, setActiveFile] = React__namespace["default"].useState();
5646
6407
  const { current, key } = location;
5647
6408
  const { permissions, prefix } = current ?? {};
5648
- const { fileDataItems } = locationItems;
6409
+ const { dataItems, fileDataItems } = locationItems;
5649
6410
  const hasInvalidPrefix = ui.isUndefined(prefix);
5650
6411
  const [{ task }, handleDownload] = useAction('download');
5651
6412
  const [{ value, isLoading, hasError, message }, handleList] = useList('locationItems');
@@ -5772,13 +6533,13 @@ const useLocationDetailView = (options) => {
5772
6533
  actionType: type,
5773
6534
  icon,
5774
6535
  isDisabled: ui.isFunction(disable)
5775
- ? disable(fileDataItems)
6536
+ ? disable(dataItems)
5776
6537
  : disable ?? false,
5777
6538
  isHidden: ui.isFunction(hide) ? hide(permissions) : hide,
5778
6539
  label,
5779
6540
  };
5780
6541
  });
5781
- }, [actionConfigs, fileDataItems, permissions]);
6542
+ }, [actionConfigs, dataItems, permissions]);
5782
6543
  return {
5783
6544
  actionItems,
5784
6545
  actionType,
@@ -5789,6 +6550,7 @@ const useLocationDetailView = (options) => {
5789
6550
  page: currentPage,
5790
6551
  pageItems,
5791
6552
  location,
6553
+ dataItems,
5792
6554
  fileDataItems,
5793
6555
  hasError,
5794
6556
  hasDownloadError: task?.status === 'FAILED',
@@ -5844,16 +6606,16 @@ const useLocationDetailView = (options) => {
5844
6606
  storeDispatch({ type: 'RESET_ACTION_TYPE' });
5845
6607
  locationItemsDispatch({ type: 'RESET_LOCATION_ITEMS' });
5846
6608
  },
5847
- onSelect: (isSelected, fileItem) => {
6609
+ onSelect: (isSelected, item) => {
5848
6610
  locationItemsDispatch(isSelected
5849
- ? { type: 'REMOVE_LOCATION_ITEM', id: fileItem.id }
5850
- : { type: 'SET_LOCATION_ITEMS', items: [fileItem] });
6611
+ ? { type: 'REMOVE_LOCATION_ITEM', id: item.id }
6612
+ : { type: 'SET_LOCATION_ITEMS', items: [item] });
5851
6613
  },
5852
6614
  onToggleSelectAll: () => {
5853
- const fileItems = pageItems.filter((item) => item.type === 'FILE');
5854
- locationItemsDispatch(fileItems.length === fileDataItems?.length
6615
+ const selectableItems = pageItems;
6616
+ locationItemsDispatch(selectableItems.length === dataItems?.length
5855
6617
  ? { type: 'RESET_LOCATION_ITEMS' }
5856
- : { type: 'SET_LOCATION_ITEMS', items: fileItems });
6618
+ : { type: 'SET_LOCATION_ITEMS', items: selectableItems });
5857
6619
  },
5858
6620
  onSearch: () => {
5859
6621
  setActiveFile(undefined);
@@ -6368,6 +7130,7 @@ exports.VERSION = VERSION;
6368
7130
  exports.componentsDefault = componentsDefault;
6369
7131
  exports.createAmplifyAuthAdapter = createAmplifyAuthAdapter;
6370
7132
  exports.createStorageBrowser = createStorageBrowser;
7133
+ exports.deduplicateLocations = deduplicateLocations;
6371
7134
  exports.defaultActionConfigs = defaultActionConfigs;
6372
7135
  exports.defaultHandlers = defaultHandlers;
6373
7136
  exports.getFilteredLocations = getFilteredLocations;