@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.
- package/dist/browser.js +8 -2
- package/dist/{createStorageBrowser-CotOvK0A.js → createStorageBrowser-B-J76Lyp.js} +1012 -249
- package/dist/esm/browser.mjs +1 -0
- package/dist/esm/components/StorageBrowser/ErrorBoundary/ErrorBoundary.mjs +0 -3
- package/dist/esm/components/StorageBrowser/StorageBrowserAmplify.mjs +1 -0
- package/dist/esm/components/StorageBrowser/actions/configs/defaults.mjs +14 -3
- package/dist/esm/components/StorageBrowser/actions/handlers/defaults.mjs +1 -1
- package/dist/esm/components/StorageBrowser/actions/handlers/delete.mjs +39 -8
- package/dist/esm/components/StorageBrowser/actions/handlers/listLocations.mjs +7 -2
- package/dist/esm/components/StorageBrowser/actions/handlers/utils.mjs +65 -1
- package/dist/esm/components/StorageBrowser/actions/handlers/zipdownload.mjs +195 -0
- package/dist/esm/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.mjs +3 -1
- package/dist/esm/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.mjs +1 -0
- package/dist/esm/components/StorageBrowser/components/ComponentsProvider.mjs +0 -3
- package/dist/esm/components/StorageBrowser/components/base/preview/DownloadButton.mjs +0 -3
- package/dist/esm/components/StorageBrowser/components/composables/ActionConfirmationModal.mjs +34 -0
- package/dist/esm/components/StorageBrowser/components/composables/defaults.mjs +2 -0
- package/dist/esm/components/StorageBrowser/components/elements/definitions.mjs +2 -2
- package/dist/esm/components/StorageBrowser/controls/ActionConfirmationModalControl.mjs +12 -0
- package/dist/esm/components/StorageBrowser/controls/DataTableControl.mjs +0 -3
- package/dist/esm/components/StorageBrowser/controls/hooks/useActionConfirmationModal.mjs +17 -0
- package/dist/esm/components/StorageBrowser/createStorageBrowser/StorageBrowserDefault.mjs +8 -3
- package/dist/esm/components/StorageBrowser/createStorageBrowser/createProvider.mjs +1 -0
- package/dist/esm/components/StorageBrowser/createStorageBrowser/createStorageBrowser.mjs +9 -4
- package/dist/esm/components/StorageBrowser/displayText/libraries/en/deleteView.mjs +117 -5
- package/dist/esm/components/StorageBrowser/displayText/libraries/en/downloadView.mjs +2 -0
- package/dist/esm/components/StorageBrowser/displayText/libraries/en/locationDetailView.mjs +1 -0
- package/dist/esm/components/StorageBrowser/displayText/libraries/en/shared.mjs +3 -0
- package/dist/esm/components/StorageBrowser/locationItems/context.mjs +18 -14
- package/dist/esm/components/StorageBrowser/locationItems/utils.mjs +38 -0
- package/dist/esm/components/StorageBrowser/store/validateStoreProps.mjs +1 -1
- package/dist/esm/components/StorageBrowser/tasks/constants.mjs +2 -0
- package/dist/esm/components/StorageBrowser/tasks/useProcessTasks.mjs +14 -5
- package/dist/esm/components/StorageBrowser/tasks/utils.mjs +4 -1
- package/dist/esm/components/StorageBrowser/useAction/useHandler.mjs +1 -1
- package/dist/esm/components/StorageBrowser/useAction/utils.mjs +0 -4
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.mjs +4 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.mjs +5 -5
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteViewProvider.mjs +7 -6
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/useDeleteView.mjs +69 -6
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/utils.mjs +87 -0
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.mjs +9 -0
- package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.mjs +1 -4
- package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.mjs +4 -0
- package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationDetailView/LocationDetailView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationDetailView/LocationDetailViewProvider.mjs +2 -1
- package/dist/esm/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getFolderRowContent.mjs +38 -27
- package/dist/esm/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getLocationDetailViewTableData.mjs +9 -1
- package/dist/esm/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.mjs +11 -9
- package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsView.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsViewProvider.mjs +0 -3
- package/dist/esm/components/StorageBrowser/views/LocationsView/useLocationsView.mjs +1 -0
- package/dist/esm/components/StorageBrowser/views/context/actionViews.mjs +7 -3
- package/dist/esm/components/StorageBrowser/views/context/primaryViews.mjs +8 -3
- package/dist/esm/components/StorageBrowser/views/hooks/useFilePreview/useFilePreview.mjs +1 -0
- package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/constants.mjs +14 -1
- package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/deleteResolvers.mjs +123 -13
- package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/utils.mjs +4 -4
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +2 -1
- package/dist/styles.css +100 -1
- package/dist/types/components/StorageBrowser/actions/handlers/delete.d.ts +5 -3
- package/dist/types/components/StorageBrowser/actions/handlers/index.d.ts +1 -0
- package/dist/types/components/StorageBrowser/actions/handlers/types.d.ts +18 -2
- package/dist/types/components/StorageBrowser/actions/handlers/utils.d.ts +11 -0
- package/dist/types/components/StorageBrowser/actions/handlers/zipdownload.d.ts +3 -0
- package/dist/types/components/StorageBrowser/components/composables/ActionConfirmationModal.d.ts +12 -0
- package/dist/types/components/StorageBrowser/components/composables/types.d.ts +2 -0
- package/dist/types/components/StorageBrowser/controls/ActionConfirmationModalControl.d.ts +2 -0
- package/dist/types/components/StorageBrowser/controls/hooks/useActionConfirmationModal.d.ts +2 -0
- package/dist/types/components/StorageBrowser/controls/index.d.ts +1 -0
- package/dist/types/components/StorageBrowser/controls/types.d.ts +4 -0
- package/dist/types/components/StorageBrowser/displayText/types.d.ts +9 -0
- package/dist/types/components/StorageBrowser/locationItems/context.d.ts +12 -3
- package/dist/types/components/StorageBrowser/locationItems/index.d.ts +1 -0
- package/dist/types/components/StorageBrowser/locationItems/utils.d.ts +27 -0
- package/dist/types/components/StorageBrowser/tasks/types.d.ts +10 -5
- package/dist/types/components/StorageBrowser/tasks/useProcessTasks.d.ts +1 -1
- package/dist/types/components/StorageBrowser/useAction/useHandler.d.ts +1 -1
- package/dist/types/components/StorageBrowser/views/LocationActionView/DeleteView/types.d.ts +5 -0
- package/dist/types/components/StorageBrowser/views/LocationActionView/DeleteView/utils.d.ts +26 -0
- package/dist/types/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getFolderRowContent.d.ts +4 -1
- package/dist/types/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getLocationDetailViewTableData.d.ts +3 -2
- package/dist/types/components/StorageBrowser/views/LocationDetailView/types.d.ts +8 -1
- package/dist/types/components/StorageBrowser/views/utils/index.d.ts +1 -1
- package/dist/types/components/StorageBrowser/views/utils/tableResolvers/constants.d.ts +5 -0
- package/dist/types/components/StorageBrowser/views/utils/tableResolvers/deleteResolvers.d.ts +5 -2
- package/dist/types/components/StorageBrowser/views/utils/tableResolvers/index.d.ts +1 -1
- package/dist/types/components/StorageBrowser/views/utils/tableResolvers/types.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +12 -9
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 &&
|
|
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 },
|
|
1837
|
-
const
|
|
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,
|
|
2182
|
+
onTaskProgress(task, progressDetails);
|
|
1840
2183
|
}
|
|
1841
2184
|
if (task && ui.isFunction(_onProgress)) {
|
|
1842
|
-
_onProgress(data,
|
|
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 {
|
|
2602
|
+
return {
|
|
2603
|
+
content: `${formatCount(TOTAL, 'item')} deleted successfully.`,
|
|
2604
|
+
type: 'success',
|
|
2605
|
+
};
|
|
2151
2606
|
}
|
|
2152
2607
|
if (FAILED === TOTAL) {
|
|
2153
|
-
return {
|
|
2608
|
+
return {
|
|
2609
|
+
content: `Failed to delete ${formatCount(TOTAL, 'item')}.`,
|
|
2610
|
+
type: 'error',
|
|
2611
|
+
};
|
|
2154
2612
|
}
|
|
2155
2613
|
return {
|
|
2156
|
-
content: `${COMPLETE}
|
|
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
|
|
3773
|
-
? value
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3874
|
-
folder: folder$
|
|
3875
|
-
type: type$
|
|
3876
|
-
size: size$
|
|
4368
|
+
name: name$3,
|
|
4369
|
+
folder: folder$3,
|
|
4370
|
+
type: type$3,
|
|
4371
|
+
size: size$3,
|
|
3877
4372
|
status: status$3,
|
|
3878
|
-
cancel: cancel$
|
|
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 =
|
|
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
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
const
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
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
|
-
|
|
5372
|
+
dataItems: nextDataItems,
|
|
5373
|
+
fileDataItems: nextFileDataItems,
|
|
4698
5374
|
};
|
|
4699
5375
|
}
|
|
4700
5376
|
case 'REMOVE_LOCATION_ITEM': {
|
|
4701
5377
|
const { id } = event;
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
4978
|
-
const
|
|
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, }) =>
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
6609
|
+
onSelect: (isSelected, item) => {
|
|
5848
6610
|
locationItemsDispatch(isSelected
|
|
5849
|
-
? { type: 'REMOVE_LOCATION_ITEM', id:
|
|
5850
|
-
: { type: 'SET_LOCATION_ITEMS', items: [
|
|
6611
|
+
? { type: 'REMOVE_LOCATION_ITEM', id: item.id }
|
|
6612
|
+
: { type: 'SET_LOCATION_ITEMS', items: [item] });
|
|
5851
6613
|
},
|
|
5852
6614
|
onToggleSelectAll: () => {
|
|
5853
|
-
const
|
|
5854
|
-
locationItemsDispatch(
|
|
6615
|
+
const selectableItems = pageItems;
|
|
6616
|
+
locationItemsDispatch(selectableItems.length === dataItems?.length
|
|
5855
6617
|
? { type: 'RESET_LOCATION_ITEMS' }
|
|
5856
|
-
: { type: 'SET_LOCATION_ITEMS', items:
|
|
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;
|