@aws-amplify/ui-react-storage 3.7.1 → 3.7.2

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.
@@ -32,7 +32,7 @@ function _interopNamespace(e) {
32
32
 
33
33
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
34
34
 
35
- const VERSION = '3.7.1';
35
+ const VERSION = '3.7.2';
36
36
 
37
37
  const constructBucket = ({ bucket: bucketName, region, }) => ({ bucketName, region });
38
38
  const parseAccessGrantLocation = (location) => {
@@ -1184,1282 +1184,1282 @@ const componentsDefault = {
1184
1184
  Title,
1185
1185
  };
1186
1186
 
1187
- const Fallback = () => (React__namespace["default"].createElement("div", { className: STORAGE_BROWSER_BLOCK_TO_BE_UPDATED },
1188
- React__namespace["default"].createElement("div", { className: `${STORAGE_BROWSER_BLOCK_TO_BE_UPDATED}__error-boundary` }, "Something went wrong.")));
1189
- class ErrorBoundary extends React__namespace["default"].Component {
1190
- constructor(props) {
1191
- super(props);
1192
- this.state = { hasError: false };
1193
- }
1194
- static getDerivedStateFromError(_error) {
1195
- // Update state so the next render will show the fallback UI.
1196
- return { hasError: true };
1197
- }
1198
- render() {
1199
- const { hasError } = this.state;
1200
- const { children } = this.props;
1201
- if (hasError) {
1202
- return React__namespace["default"].createElement(Fallback, null);
1203
- }
1204
- return children;
1205
- }
1206
- }
1207
-
1208
- const CREDENTIALS_STORE_DEFAULT_SIZE = 10;
1209
- const CREDENTIALS_REFRESH_WINDOW_MS = 30000;
1210
-
1211
- const serializedPermissions = (permissions) => permissions.sort().join('_');
1212
- const createCacheKey = (location) => `${location.scope}_${serializedPermissions(location.permissions)}`;
1213
- const pastTTL = (credentials) => {
1214
- const { expiration } = credentials;
1215
- return expiration.getTime() - CREDENTIALS_REFRESH_WINDOW_MS <= Date.now();
1187
+ const DEFAULT_ACTION_VIEW_DISPLAY_TEXT = {
1188
+ actionCancelLabel: 'Cancel',
1189
+ actionExitLabel: 'Exit',
1190
+ actionDestinationLabel: 'Destination',
1191
+ statusDisplayCanceledLabel: 'Canceled',
1192
+ statusDisplayCompletedLabel: 'Completed',
1193
+ statusDisplayFailedLabel: 'Failed',
1194
+ statusDisplayInProgressLabel: 'In progress',
1195
+ statusDisplayTotalLabel: 'Total',
1196
+ statusDisplayQueuedLabel: 'Not started',
1197
+ // empty by default
1198
+ tableColumnCancelHeader: '',
1199
+ tableColumnStatusHeader: 'Status',
1200
+ tableColumnFolderHeader: 'Folder',
1201
+ tableColumnNameHeader: 'Name',
1202
+ tableColumnTypeHeader: 'Type',
1203
+ tableColumnSizeHeader: 'Size',
1204
+ tableColumnProgressHeader: 'Progress',
1216
1205
  };
1217
- const setCacheRecord = (store, key, value) => {
1218
- if (store.capacity === store.values.size) {
1219
- // Pop least used entry. The Map's key are in insertion order.
1220
- // So first key is the last recently inserted.
1221
- const [oldestKey] = store.values.keys();
1222
- store.values.delete(oldestKey);
1223
- // TODO(@AllanZhengYP): Add log info when record is evicted.
1224
- }
1225
- // Add latest used value to the cache.
1226
- store.values.set(key, value);
1206
+ const DEFAULT_LIST_VIEW_DISPLAY_TEXT = {
1207
+ loadingIndicatorLabel: 'Loading',
1208
+ searchSubmitLabel: 'Submit',
1209
+ searchClearLabel: 'Clear search',
1210
+ getDateDisplayValue: (date) => new Intl.DateTimeFormat('en-US', {
1211
+ month: 'short',
1212
+ day: 'numeric',
1213
+ hour: 'numeric',
1214
+ year: 'numeric',
1215
+ minute: 'numeric',
1216
+ hourCycle: 'h12',
1217
+ }).format(date),
1227
1218
  };
1228
- const dispatchRefresh = (refreshHandler, value, onRefreshFailure) => {
1229
- if (value.inflightCredentials) {
1230
- return value.inflightCredentials;
1231
- }
1232
- value.inflightCredentials = (async () => {
1233
- try {
1234
- const { credentials } = await refreshHandler({
1235
- scope: value.scope,
1236
- permissions: value.permissions,
1237
- });
1238
- value.credentials = credentials;
1239
- return { credentials };
1240
- }
1241
- catch (e) {
1242
- onRefreshFailure();
1243
- throw e;
1219
+
1220
+ const DEFAULT_CREATE_FOLDER_VIEW_DISPLAY_TEXT = {
1221
+ ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
1222
+ title: 'Create folder',
1223
+ actionStartLabel: 'Create folder',
1224
+ folderNameLabel: 'Folder name',
1225
+ folderNamePlaceholder: 'Folder name cannot contain "/", nor end or start with "."',
1226
+ getValidationMessage: () => 'Folder name cannot contain "/", nor end or start with "."',
1227
+ getActionCompleteMessage: (data) => {
1228
+ const { counts } = data ?? {};
1229
+ const { FAILED, OVERWRITE_PREVENTED } = counts ?? {};
1230
+ if (OVERWRITE_PREVENTED) {
1231
+ return {
1232
+ content: 'A folder already exists with the provided name',
1233
+ type: 'warning',
1234
+ };
1244
1235
  }
1245
- finally {
1246
- value.inflightCredentials = undefined;
1236
+ if (FAILED) {
1237
+ return {
1238
+ content: 'There was an issue creating the folder.',
1239
+ type: 'error',
1240
+ };
1247
1241
  }
1248
- })();
1249
- return value.inflightCredentials;
1250
- };
1251
- /**
1252
- * @internal
1253
- */
1254
- const initStore = (refreshHandler, size = CREDENTIALS_STORE_DEFAULT_SIZE) => {
1255
- internals.assertValidationError(size > 0, internals.StorageValidationErrorCode.InvalidLocationCredentialsCacheSize);
1256
- return {
1257
- capacity: size,
1258
- refreshHandler,
1259
- values: new Map(),
1260
- };
1261
- };
1262
- const getCacheValue = (store, location) => {
1263
- const cacheKey = createCacheKey(location);
1264
- const cachedValue = store.values.get(cacheKey);
1265
- const cachedCredentials = cachedValue?.credentials;
1266
- if (!cachedCredentials) {
1267
- return null;
1268
- }
1269
- // Delete and re-insert to key to map to indicate a latest reference in LRU.
1270
- store.values.delete(cacheKey);
1271
- if (!pastTTL(cachedCredentials)) {
1272
- // TODO(@AllanZhengYP): If the credential is still valid but will expire
1273
- // soon, we should return credentials AND dispatch a refresh.
1274
- store.values.set(cacheKey, cachedValue);
1275
- return cachedCredentials;
1276
- }
1277
- return null;
1278
- };
1279
- /**
1280
- * Fetch new credentials value with refresh handler and cache the result in
1281
- * LRU cache.
1282
- * @internal
1283
- */
1284
- const fetchNewValue = async (store, location) => {
1285
- const storeValues = store.values;
1286
- const key = createCacheKey(location);
1287
- if (!storeValues.has(key)) {
1288
- const newStoreValue = {
1289
- scope: location.scope,
1290
- permissions: location.permissions,
1291
- };
1292
- setCacheRecord(store, key, newStoreValue);
1293
- }
1294
- const storeValue = storeValues.get(key);
1295
- return dispatchRefresh(store.refreshHandler, storeValue, () => {
1296
- store.values.delete(key);
1297
- });
1242
+ return { content: 'Folder created.', type: 'success' };
1243
+ },
1298
1244
  };
1299
1245
 
1300
- /**
1301
- * Keep all cache records for all instances of credentials store in a singleton
1302
- * so we can reliably de-reference from the memory when we destroy a store
1303
- * instance.
1304
- */
1305
- const storeRegistry = new WeakMap();
1306
- /**
1307
- * @internal
1308
- */
1309
- const createStore = (refreshHandler, size) => {
1310
- const storeSymbol = { value: Symbol('LocationCredentialsStore') };
1311
- storeRegistry.set(storeSymbol, initStore(refreshHandler, size));
1312
- return storeSymbol;
1313
- };
1314
- const getCredentialsStore = (storeSymbol) => {
1315
- internals.assertValidationError(storeRegistry.has(storeSymbol), internals.StorageValidationErrorCode.LocationCredentialsStoreDestroyed);
1316
- return storeRegistry.get(storeSymbol);
1317
- };
1318
- /**
1319
- * @internal
1320
- */
1321
- const getValue = async (input) => {
1322
- const { storeSymbol: storeReference, location, forceRefresh } = input;
1323
- const store = getCredentialsStore(storeReference);
1324
- if (!forceRefresh) {
1325
- const credentials = getCacheValue(store, location);
1326
- if (credentials !== null) {
1327
- return { credentials };
1246
+ const DEFAULT_COPY_VIEW_DISPLAY_TEXT = {
1247
+ ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
1248
+ title: 'Copy',
1249
+ actionStartLabel: 'Copy',
1250
+ actionDestinationLabel: 'Copy destination',
1251
+ getListFoldersResultsMessage: ({ folders, query, message, hasError, hasExhaustedSearch, }) => {
1252
+ if (!folders?.length) {
1253
+ return {
1254
+ content: query
1255
+ ? `No folders found matching "${query}"`
1256
+ : 'No subfolders found within selected folder.',
1257
+ type: 'info',
1258
+ };
1328
1259
  }
1329
- }
1330
- return fetchNewValue(store, location);
1331
- };
1332
- const removeStore = (storeSymbol) => {
1333
- storeRegistry.delete(storeSymbol);
1334
- };
1335
-
1336
- const validateS3Uri = (uri) => {
1337
- const s3UrlSchemaRegex = /^s3:\/\/[^/]+/;
1338
- internals.assertValidationError(s3UrlSchemaRegex.test(uri), internals.StorageValidationErrorCode.InvalidS3Uri);
1339
- };
1340
- const createLocationCredentialsStore = (input) => {
1341
- const storeSymbol = createStore(input.handler);
1342
- const store = {
1343
- getProvider(providerLocation) {
1344
- const locationCredentialsProvider = async ({ forceRefresh = false, } = {}) => {
1345
- validateS3Uri(providerLocation.scope);
1346
- // TODO(@AllanZhengYP): validate the action bucket and paths matches provider scope.
1347
- return getValue({
1348
- storeSymbol,
1349
- location: { ...providerLocation },
1350
- forceRefresh,
1351
- });
1260
+ if (message && !!query) {
1261
+ return { content: 'Error loading folders.', type: 'error' };
1262
+ }
1263
+ if (hasError) {
1264
+ return { content: 'Error loading folders.', type: 'error' };
1265
+ }
1266
+ if (hasExhaustedSearch) {
1267
+ return {
1268
+ content: 'Showing results for up to the first 10,000 items.',
1269
+ type: 'info',
1352
1270
  };
1353
- return locationCredentialsProvider;
1354
- },
1355
- destroy() {
1356
- removeStore(storeSymbol);
1357
- },
1358
- };
1359
- return store;
1271
+ }
1272
+ },
1273
+ loadingIndicatorLabel: 'Loading',
1274
+ overwriteWarningMessage: 'Copied files will overwrite existing files at selected destination.',
1275
+ searchPlaceholder: 'Search for folders',
1276
+ getActionCompleteMessage: (data) => {
1277
+ const { counts } = data ?? {};
1278
+ const { COMPLETE, FAILED, TOTAL } = counts ?? {};
1279
+ if (COMPLETE === TOTAL) {
1280
+ return {
1281
+ content: 'All files copied.',
1282
+ type: 'success',
1283
+ };
1284
+ }
1285
+ if (FAILED === TOTAL) {
1286
+ return { content: 'All files failed to copy.', type: 'error' };
1287
+ }
1288
+ return {
1289
+ content: `${COMPLETE} files copied, ${FAILED} files failed to copy.`,
1290
+ type: 'error',
1291
+ };
1292
+ },
1293
+ searchSubmitLabel: 'Submit',
1294
+ searchClearLabel: 'Clear search',
1360
1295
  };
1361
1296
 
1362
- const createCredentialsStore = ({ ...input }) => {
1363
- const { destroy, getProvider } = createLocationCredentialsStore(input);
1364
- return {
1365
- destroy,
1366
- getCredentials: ({ scope, permissions }) => getProvider({
1367
- scope,
1368
- permissions,
1369
- }),
1370
- };
1371
- };
1372
- const isCredentialsStore = (value) => ui.isFunction(value?.getCredentials);
1373
- function useCredentialsStore({ getLocationCredentials: handler, initialValue, onDestroy, registerAuthListener, }) {
1374
- const hasExistingStore = isCredentialsStore(initialValue);
1375
- const [store, setStore] = React__namespace["default"].useState(() => hasExistingStore ? initialValue : createCredentialsStore({ handler }));
1376
- const { destroy } = store;
1377
- React__namespace["default"].useEffect(() => {
1378
- if (hasExistingStore) {
1379
- return;
1297
+ const DEFAULT_DELETE_VIEW_DISPLAY_TEXT = {
1298
+ ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
1299
+ title: 'Delete',
1300
+ actionStartLabel: 'Delete',
1301
+ getActionCompleteMessage: (data) => {
1302
+ const { counts } = data ?? {};
1303
+ const { COMPLETE, FAILED, TOTAL } = counts ?? {};
1304
+ if (COMPLETE === TOTAL) {
1305
+ return { content: 'All files deleted.', type: 'success' };
1380
1306
  }
1381
- const handleAuthStatusChange = () => {
1382
- destroy();
1383
- if (ui.isFunction(onDestroy)) {
1384
- onDestroy();
1385
- }
1386
- setStore(createCredentialsStore({ handler }));
1307
+ if (FAILED === TOTAL) {
1308
+ return { content: 'All files failed to delete.', type: 'error' };
1309
+ }
1310
+ return {
1311
+ content: `${COMPLETE} files deleted, ${FAILED} files failed to delete.`,
1312
+ type: 'error',
1387
1313
  };
1388
- // provide `handleAuthStatusChange` to consumer
1389
- registerAuthListener(handleAuthStatusChange);
1390
- }, [destroy, handler, hasExistingStore, onDestroy, registerAuthListener]);
1391
- return store;
1392
- }
1393
-
1394
- const ERROR_MESSAGE$3 = '`useCredentials` must be called from within a `CredentialsProvider`.';
1395
- const { useCredentials, CredentialsContext } = uiReactCore.createContextUtilities({
1396
- contextName: 'Credentials',
1397
- errorMessage: ERROR_MESSAGE$3,
1398
- });
1399
- function CredentialsProvider({ children, ...props }) {
1400
- const initialValue = React__namespace["default"].useContext(CredentialsContext);
1401
- const value = useCredentialsStore({ ...props, initialValue });
1402
- return (React__namespace["default"].createElement(CredentialsContext.Provider, { value: value }, children));
1403
- }
1404
-
1405
- const ERROR_MESSAGE$2 = 'Invalid `location` value provided as initial value to `LocationProvider.';
1406
- const DEFAULT_STATE$1 = {
1407
- current: undefined,
1408
- path: '',
1409
- key: '',
1314
+ },
1410
1315
  };
1411
- function handleAction$1(state, action) {
1412
- switch (action.type) {
1413
- case 'NAVIGATE': {
1414
- const { location, path = '' } = action;
1415
- if (state.current?.id === location.id && state.path === path) {
1416
- return state;
1417
- }
1316
+
1317
+ const DEFAULT_ERROR_MESSAGE$1 = 'There was an error loading items.';
1318
+ const DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT = {
1319
+ ...DEFAULT_LIST_VIEW_DISPLAY_TEXT,
1320
+ getListItemsResultMessage: (data) => {
1321
+ const { items, hasExhaustedSearch, hasError = false, message, isLoading, } = data ?? {};
1322
+ if (isLoading) {
1323
+ return undefined;
1324
+ }
1325
+ if (hasError) {
1418
1326
  return {
1419
- current: location,
1420
- path,
1421
- key: `${location.prefix ?? ''}${path}`,
1327
+ type: 'error',
1328
+ content: message ?? DEFAULT_ERROR_MESSAGE$1,
1422
1329
  };
1423
1330
  }
1424
- case 'RESET_LOCATION': {
1425
- return DEFAULT_STATE$1;
1426
- }
1427
- }
1428
- }
1429
- const defaultValue$7 = [DEFAULT_STATE$1, ui.noop];
1430
- const { LocationContext, useLocation } = uiReactCore.createContextUtilities({
1431
- contextName: 'Location',
1432
- defaultValue: defaultValue$7,
1433
- });
1434
- function LocationProvider({ children, location, path = '', }) {
1435
- if (location) {
1436
- assertLocationData(location, ERROR_MESSAGE$2);
1437
- }
1438
- const value = React__namespace["default"].useReducer(handleAction$1, location
1439
- ? {
1440
- current: location,
1441
- path,
1442
- key: `${location.prefix ?? ''}${path}`,
1331
+ if (!items?.length && hasExhaustedSearch) {
1332
+ return {
1333
+ type: 'info',
1334
+ content: `No results found in the first 10,000 items.`,
1335
+ };
1443
1336
  }
1444
- : DEFAULT_STATE$1);
1445
- return (React__namespace["default"].createElement(LocationContext.Provider, { value: value }, children));
1446
- }
1447
-
1448
- const compareFileItems = (prev, next) => prev.key.localeCompare(next.key);
1449
- const resolveFiles = (prevItems, files) => {
1450
- if (!files?.length)
1451
- return prevItems;
1452
- // construct `nextItems` and filter out existing `file` entries
1453
- const nextItems = files.reduce((items, file) => {
1454
- const { name, webkitRelativePath } = file;
1455
- return prevItems.some(({ file: existing }) => existing.name === name &&
1456
- existing.webkitRelativePath === webkitRelativePath)
1457
- ? items
1458
- : items.concat({
1459
- key: ui.isEmpty(webkitRelativePath) ? name : webkitRelativePath,
1460
- id: crypto.randomUUID(),
1461
- file,
1462
- });
1463
- }, []);
1464
- if (!nextItems.length)
1465
- return prevItems;
1466
- if (!prevItems.length) {
1467
- return nextItems.sort(compareFileItems);
1468
- }
1469
- return prevItems.concat(nextItems).sort(compareFileItems);
1470
- };
1471
- const filesReducer = (prevItems, input) => {
1472
- switch (input.type) {
1473
- case 'ADD_FILE_ITEMS': {
1474
- return resolveFiles(prevItems, input.files);
1337
+ if (!items?.length) {
1338
+ return {
1339
+ type: 'info',
1340
+ content: 'No files.',
1341
+ };
1475
1342
  }
1476
- case 'REMOVE_FILE_ITEM': {
1477
- const filteredItems = prevItems.filter(({ id }) => id !== input.id);
1478
- return filteredItems.length === prevItems.length
1479
- ? prevItems
1480
- : filteredItems;
1343
+ if (hasExhaustedSearch) {
1344
+ return {
1345
+ type: 'info',
1346
+ content: `Showing results for up to the first 10,000 items.`,
1347
+ };
1481
1348
  }
1482
- case 'RESET_FILE_ITEMS': {
1483
- return [];
1349
+ // TODO: add more cases as needed
1350
+ return undefined;
1351
+ },
1352
+ searchSubfoldersToggleLabel: 'Include subfolders',
1353
+ searchPlaceholder: 'Search current folder',
1354
+ tableColumnLastModifiedHeader: 'Last modified',
1355
+ tableColumnNameHeader: 'Name',
1356
+ tableColumnSizeHeader: 'Size',
1357
+ tableColumnTypeHeader: 'Type',
1358
+ selectFileLabel: 'Select file',
1359
+ selectAllFilesLabel: 'Select all files',
1360
+ getActionListItemLabel: (key = '') => {
1361
+ switch (key) {
1362
+ case 'Copy':
1363
+ return 'Copy';
1364
+ case 'Delete':
1365
+ return 'Delete';
1366
+ case 'Create folder':
1367
+ return 'Create folder';
1368
+ case 'Upload':
1369
+ return 'Upload';
1370
+ default:
1371
+ return key;
1484
1372
  }
1485
- // TODO: clear message
1486
- }
1487
- };
1488
- const parseFileSelectParams = (value) => {
1489
- if (ui.isUndefined(value))
1490
- return ['FILE', undefined];
1491
- if (ui.isString(value))
1492
- return [value, undefined];
1493
- const [selectType, ...rest] = value;
1494
- return [selectType, !rest?.length ? undefined : { accept: rest.join() }];
1373
+ },
1374
+ getTitle: (location) => {
1375
+ const { current, key } = location;
1376
+ const { bucket = '' } = current ?? {};
1377
+ return key || bucket;
1378
+ },
1495
1379
  };
1496
1380
 
1497
- const defaultValue$6 = [undefined, ui.noop];
1498
- const { FilesContext, useFiles } = uiReactCore.createContextUtilities({
1499
- contextName: 'Files',
1500
- defaultValue: defaultValue$6,
1501
- });
1502
- function FilesProvider({ children, }) {
1503
- const [items, dispatch] = React__namespace["default"].useReducer(filesReducer, []);
1504
- const [fileInput, handleFileSelect] = internal.useFileSelect((nextFiles) => {
1505
- dispatch({ type: 'ADD_FILE_ITEMS', files: nextFiles });
1506
- });
1507
- const handleFilesAction = React__namespace["default"].useCallback((action) => {
1508
- if (action.type === 'SELECT_FILES') {
1509
- handleFileSelect(...parseFileSelectParams(action.selectionType));
1381
+ const DEFAULT_ERROR_MESSAGE = 'There was an error loading locations.';
1382
+ const DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT = {
1383
+ ...DEFAULT_LIST_VIEW_DISPLAY_TEXT,
1384
+ title: 'Home',
1385
+ searchPlaceholder: 'Filter folders and files',
1386
+ getListLocationsResultMessage: (data) => {
1387
+ const { isLoading, items, hasExhaustedSearch, hasError = false, message, } = data ?? {};
1388
+ if (isLoading) {
1389
+ return undefined;
1510
1390
  }
1511
- else {
1512
- dispatch(action);
1391
+ if (hasError) {
1392
+ return {
1393
+ type: 'error',
1394
+ content: message ?? DEFAULT_ERROR_MESSAGE,
1395
+ };
1513
1396
  }
1514
- }, [handleFileSelect]);
1515
- const value = React__namespace["default"].useMemo(() => [items, handleFilesAction], [items, handleFilesAction]);
1516
- return (React__namespace["default"].createElement(FilesContext.Provider, { value: value },
1517
- fileInput,
1518
- children));
1519
- }
1520
-
1521
- const handleAction = (event) => {
1522
- switch (event.type) {
1523
- case 'SET_ACTION_TYPE': {
1524
- return event.actionType;
1397
+ if (items?.length === 0 && !hasExhaustedSearch) {
1398
+ return {
1399
+ type: 'info',
1400
+ content: 'No folders or files.',
1401
+ };
1402
+ }
1403
+ if (hasExhaustedSearch) {
1404
+ return {
1405
+ type: 'info',
1406
+ content: `Showing results for up to the first 10,000 items.`,
1407
+ };
1408
+ }
1409
+ // TODO: add more cases as needed
1410
+ return undefined;
1411
+ },
1412
+ getPermissionName: (permissions) => {
1413
+ let text = '';
1414
+ if (permissions.includes('get') || permissions.includes('list')) {
1415
+ text = 'Read';
1525
1416
  }
1526
- case 'RESET_ACTION_TYPE': {
1527
- return undefined;
1417
+ if (permissions.includes('write') || permissions.includes('delete')) {
1418
+ text = text ? 'Read/Write' : 'Write';
1528
1419
  }
1529
- }
1420
+ if (!text) {
1421
+ text = permissions.join('/');
1422
+ }
1423
+ return text;
1424
+ },
1425
+ getDownloadLabel: (fileName) => `Download ${fileName}`,
1426
+ tableColumnBucketHeader: 'Bucket',
1427
+ tableColumnFolderHeader: 'Folder',
1428
+ tableColumnPermissionsHeader: 'Permissions',
1429
+ tableColumnActionsHeader: 'Actions',
1530
1430
  };
1531
- function useActionTypeState(initialState) {
1532
- const [actionType, setActionType] = React__namespace["default"].useState(initialState);
1533
- const handler = React__namespace["default"].useCallback((action) => setActionType(handleAction(action)), []);
1534
- return [actionType, handler];
1535
- }
1536
-
1537
- const defaultValue$5 = [undefined, ui.noop];
1538
- const { ActionTypeContext, useActionType } = uiReactCore.createContextUtilities({
1539
- contextName: 'ActionType',
1540
- defaultValue: defaultValue$5,
1541
- });
1542
- function ActionTypeProvider({ actionType, children, }) {
1543
- const value = useActionTypeState(actionType);
1544
- return (React__namespace["default"].createElement(ActionTypeContext.Provider, { value: value }, children));
1545
- }
1546
1431
 
1547
- const DEFAULT_STATE = {
1548
- fileDataItems: undefined,
1549
- };
1550
- const locationItemsReducer = (prevState, event) => {
1551
- switch (event.type) {
1552
- case 'SET_LOCATION_ITEMS': {
1553
- const { items } = event;
1554
- if (!items?.length)
1555
- return prevState;
1556
- if (!prevState.fileDataItems?.length) {
1557
- return { fileDataItems: items.map(createFileDataItem) };
1558
- }
1559
- const nextFileDataItems = items?.reduce((fileDataItems, data) => prevState.fileDataItems?.some(({ id }) => id === data.id)
1560
- ? fileDataItems
1561
- : fileDataItems.concat(createFileDataItem(data)), []);
1562
- if (!nextFileDataItems?.length)
1563
- return prevState;
1432
+ const DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT = {
1433
+ ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
1434
+ title: 'Upload',
1435
+ actionStartLabel: 'Upload',
1436
+ addFilesLabel: 'Add files',
1437
+ addFolderLabel: 'Add folder',
1438
+ getActionCompleteMessage: (data) => {
1439
+ const { counts } = data ?? {};
1440
+ const { COMPLETE, FAILED, OVERWRITE_PREVENTED, CANCELED, TOTAL } = counts ?? {};
1441
+ const hasPreventedOverwrite = !!OVERWRITE_PREVENTED;
1442
+ const hasFailure = !!FAILED;
1443
+ const hasSuccess = !!COMPLETE;
1444
+ const hasCanceled = !!CANCELED;
1445
+ const type = hasFailure
1446
+ ? 'error'
1447
+ : hasPreventedOverwrite || hasCanceled
1448
+ ? 'warning'
1449
+ : 'success';
1450
+ const preventedOverwriteMessage = hasPreventedOverwrite
1451
+ ? [
1452
+ 'Overwrite prevented for',
1453
+ OVERWRITE_PREVENTED === TOTAL ? 'all' : String(OVERWRITE_PREVENTED),
1454
+ OVERWRITE_PREVENTED > 1 || OVERWRITE_PREVENTED === TOTAL
1455
+ ? `files`
1456
+ : 'file',
1457
+ ].join(' ')
1458
+ : undefined;
1459
+ const canceledMessage = hasCanceled
1460
+ ? [
1461
+ CANCELED === TOTAL ? 'All' : String(CANCELED),
1462
+ CANCELED > 1 || CANCELED === TOTAL ? `uploads` : 'upload',
1463
+ 'canceled',
1464
+ ].join(' ')
1465
+ : undefined;
1466
+ const failedMessage = hasFailure
1467
+ ? [
1468
+ FAILED === TOTAL ? 'All' : String(FAILED),
1469
+ FAILED > 1 || FAILED === TOTAL ? `files` : 'file',
1470
+ 'failed to upload',
1471
+ ].join(' ')
1472
+ : undefined;
1473
+ const completedMessage = hasSuccess
1474
+ ? [
1475
+ COMPLETE === TOTAL ? 'All' : String(COMPLETE),
1476
+ COMPLETE > 1 || COMPLETE === TOTAL ? `files` : 'file',
1477
+ 'uploaded',
1478
+ ].join(' ')
1479
+ : undefined;
1480
+ const messages = [
1481
+ preventedOverwriteMessage,
1482
+ failedMessage,
1483
+ canceledMessage,
1484
+ completedMessage,
1485
+ ].filter(Boolean);
1486
+ if (messages.length > 0) {
1564
1487
  return {
1565
- fileDataItems: prevState.fileDataItems.concat(nextFileDataItems),
1488
+ content: messages.join(', ') + '.',
1489
+ type,
1566
1490
  };
1567
1491
  }
1568
- case 'REMOVE_LOCATION_ITEM': {
1569
- const { id } = event;
1570
- if (!prevState.fileDataItems)
1571
- return prevState;
1572
- const fileDataItems = prevState.fileDataItems.filter((item) => item.id !== id);
1573
- if (fileDataItems.length === prevState.fileDataItems.length) {
1574
- return prevState;
1575
- }
1576
- return { fileDataItems };
1492
+ return { content: 'All files uploaded.', type };
1493
+ },
1494
+ getFilesValidationMessage: (data) => {
1495
+ if (!data?.invalidFiles) {
1496
+ return undefined;
1577
1497
  }
1578
- case 'RESET_LOCATION_ITEMS': {
1579
- return DEFAULT_STATE;
1498
+ const tooBigFileNames = data.invalidFiles
1499
+ .filter(({ file }) => isFileTooBig(file))
1500
+ .map(({ file }) => file.name)
1501
+ .join(', ');
1502
+ if (tooBigFileNames) {
1503
+ return {
1504
+ content: `Files larger than 160GB cannot be added to the upload queue: ${tooBigFileNames}`,
1505
+ type: 'warning',
1506
+ };
1580
1507
  }
1581
- }
1508
+ return undefined;
1509
+ },
1510
+ statusDisplayOverwritePreventedLabel: 'Overwrite prevented',
1511
+ overwriteToggleLabel: 'Overwrite existing files',
1582
1512
  };
1583
- const defaultValue$4 = [DEFAULT_STATE, ui.noop];
1584
- const { LocationItemsContext, useLocationItems } = uiReactCore.createContextUtilities({ contextName: 'LocationItems', defaultValue: defaultValue$4 });
1585
- function LocationItemsProvider({ children, }) {
1586
- const value = React__namespace["default"].useReducer(locationItemsReducer, DEFAULT_STATE);
1587
- return (React__namespace["default"].createElement(LocationItemsContext.Provider, { value: value }, children));
1588
- }
1589
1513
 
1590
- function StoreProvider(props) {
1591
- const { actionType, children, location, path } = props;
1592
- return (React__namespace["default"].createElement(FilesProvider, null,
1593
- React__namespace["default"].createElement(LocationProvider, { location: location, path: path },
1594
- React__namespace["default"].createElement(LocationItemsProvider, null,
1595
- React__namespace["default"].createElement(ActionTypeProvider, { actionType: actionType }, children)))));
1596
- }
1597
-
1598
- function useStore() {
1599
- const [actionType, dispatchActionType] = useActionType();
1600
- const [files, dispatchFilesAction] = useFiles();
1601
- const [location, dispatchLocationAction] = useLocation();
1602
- const [locationItems, dispatchLocationItemsAction] = useLocationItems();
1603
- const dispatchHandler = React__namespace["default"].useCallback((action) => {
1604
- switch (action.type) {
1605
- case 'ADD_FILE_ITEMS':
1606
- case 'REMOVE_FILE_ITEM':
1607
- case 'SELECT_FILES':
1608
- case 'RESET_FILE_ITEMS': {
1609
- dispatchFilesAction(action);
1610
- break;
1611
- }
1612
- case 'NAVIGATE':
1613
- case 'RESET_LOCATION': {
1614
- dispatchLocationAction(action);
1615
- break;
1616
- }
1617
- case 'SET_LOCATION_ITEMS':
1618
- case 'REMOVE_LOCATION_ITEM':
1619
- case 'RESET_LOCATION_ITEMS': {
1620
- dispatchLocationItemsAction(action);
1621
- break;
1622
- }
1623
- case 'SET_ACTION_TYPE':
1624
- case 'RESET_ACTION_TYPE': {
1625
- dispatchActionType(action);
1626
- break;
1627
- }
1628
- }
1629
- }, [
1630
- dispatchActionType,
1631
- dispatchFilesAction,
1632
- dispatchLocationAction,
1633
- dispatchLocationItemsAction,
1634
- ]);
1635
- return [{ actionType, files, location, locationItems }, dispatchHandler];
1636
- }
1514
+ const DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT = {
1515
+ CopyView: DEFAULT_COPY_VIEW_DISPLAY_TEXT,
1516
+ CreateFolderView: DEFAULT_CREATE_FOLDER_VIEW_DISPLAY_TEXT,
1517
+ DeleteView: DEFAULT_DELETE_VIEW_DISPLAY_TEXT,
1518
+ LocationDetailView: DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT,
1519
+ LocationsView: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
1520
+ UploadView: DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT,
1521
+ };
1637
1522
 
1638
- const getErrorMessage = (propertyName) => `Unable to resolve credentials due to invalid value of '${propertyName}'`;
1639
- function useGetActionInputCallback({ accountId, customEndpoint, region, }) {
1640
- const { getCredentials } = useCredentials();
1641
- const [{ location }] = useStore();
1642
- const { current, key } = location;
1643
- return React__namespace["default"].useCallback((_location) => {
1644
- // prefer passed in location / prefix over current location in state
1645
- const location = _location ?? current;
1646
- // when `location` has been provided as a param, resolve `_prefix` to `location.prefix`.
1647
- // in the default scenario where `current` is the target `location` use the fully qualified `key`
1648
- // that includes the default `prefix` and any additional prefixes from navigation
1649
- const prefix = _location ? _location.prefix : key;
1650
- assertLocationData(location, getErrorMessage('locationData'));
1651
- assertPrefix(prefix, getErrorMessage('prefix'));
1652
- const { bucket, permissions, type } = location;
1653
- // BUCKET/PREFIX grants end with `*`, but object grants do not.
1654
- const scope = `s3://${bucket}/${prefix}${type === 'OBJECT' ? '' : '*'}`;
1655
- return {
1656
- accountId,
1657
- bucket,
1658
- credentials: getCredentials({
1659
- permissions,
1660
- scope,
1661
- }),
1662
- region,
1663
- customEndpoint,
1664
- };
1665
- }, [accountId, current, customEndpoint, getCredentials, key, region]);
1523
+ const { DisplayTextContext, useDisplayText } = uiReactCore.createContextUtilities({
1524
+ contextName: 'DisplayText',
1525
+ errorMessage: '`useDisplayText` must be called inside `DisplayTextProvider`',
1526
+ });
1527
+ function resolveDisplayText(displayText) {
1528
+ if (!displayText)
1529
+ return DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT;
1530
+ // override
1531
+ const { CopyView, CreateFolderView, DeleteView, LocationDetailView, LocationsView, UploadView, } = displayText;
1532
+ return {
1533
+ CopyView: { ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.CopyView, ...CopyView },
1534
+ CreateFolderView: {
1535
+ ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.CreateFolderView,
1536
+ ...CreateFolderView,
1537
+ },
1538
+ DeleteView: {
1539
+ ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.DeleteView,
1540
+ ...DeleteView,
1541
+ },
1542
+ LocationDetailView: {
1543
+ ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.LocationDetailView,
1544
+ ...LocationDetailView,
1545
+ },
1546
+ LocationsView: {
1547
+ ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.LocationsView,
1548
+ ...LocationsView,
1549
+ },
1550
+ UploadView: {
1551
+ ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.UploadView,
1552
+ ...UploadView,
1553
+ },
1554
+ };
1666
1555
  }
1667
-
1668
- const ERROR_MESSAGE$1 = '`useGetActionInput` must be called from within a `ConfigurationProvider`.';
1669
- const { useGetActionInput, GetActionInputContext } = uiReactCore.createContextUtilities({
1670
- contextName: 'GetActionInput',
1671
- errorMessage: ERROR_MESSAGE$1,
1672
- });
1673
- function GetActionInputProvider({ accountId, children, customEndpoint, region, }) {
1674
- const value = useGetActionInputCallback({
1675
- accountId,
1676
- customEndpoint,
1677
- region,
1678
- });
1679
- return (React__namespace["default"].createElement(GetActionInputContext.Provider, { value: value }, children));
1556
+ function DisplayTextProvider({ children, displayText: _override, }) {
1557
+ // do deep merge here of default and override here
1558
+ const resolvedDisplayText = React__namespace["default"].useMemo(() => resolveDisplayText(_override), [_override]);
1559
+ return (React__namespace["default"].createElement(DisplayTextContext.Provider, { value: resolvedDisplayText }, children));
1680
1560
  }
1681
1561
 
1682
- const Passthrough = ({ children }) => (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, children));
1683
- function createConfigurationProvider(input) {
1684
- const { accountId, ChildComponent, displayName, region, customEndpoint, ...rest } = input;
1685
- const Child = elements.isComponent(ChildComponent) ? ChildComponent : Passthrough;
1686
- const Provider = (props) => (React__namespace["default"].createElement(CredentialsProvider, { ...rest },
1687
- React__namespace["default"].createElement(GetActionInputProvider, { accountId: accountId, region: region, customEndpoint: customEndpoint },
1688
- React__namespace["default"].createElement(Child, { ...props }))));
1689
- Provider.displayName = displayName;
1690
- return Provider;
1562
+ const Fallback = () => (React__namespace["default"].createElement("div", { className: STORAGE_BROWSER_BLOCK_TO_BE_UPDATED },
1563
+ React__namespace["default"].createElement("div", { className: `${STORAGE_BROWSER_BLOCK_TO_BE_UPDATED}__error-boundary` }, "Something went wrong.")));
1564
+ class ErrorBoundary extends React__namespace["default"].Component {
1565
+ constructor(props) {
1566
+ super(props);
1567
+ this.state = { hasError: false };
1568
+ }
1569
+ static getDerivedStateFromError(_error) {
1570
+ // Update state so the next render will show the fallback UI.
1571
+ return { hasError: true };
1572
+ }
1573
+ render() {
1574
+ const { hasError } = this.state;
1575
+ const { children } = this.props;
1576
+ if (hasError) {
1577
+ return React__namespace["default"].createElement(Fallback, null);
1578
+ }
1579
+ return children;
1580
+ }
1691
1581
  }
1692
1582
 
1693
- const defaultValue$3 = { data: {} };
1694
- const { useControlsContext, ControlsContextProvider } = uiReactCore.createContextUtilities({
1695
- contextName: 'ControlsContext',
1696
- defaultValue: defaultValue$3,
1697
- });
1583
+ const CREDENTIALS_STORE_DEFAULT_SIZE = 10;
1584
+ const CREDENTIALS_REFRESH_WINDOW_MS = 30000;
1698
1585
 
1699
- const useActionCancel = () => {
1700
- const { data: { actionCancelLabel, isActionCancelDisabled }, onActionCancel, } = useControlsContext();
1701
- return {
1702
- onCancel: onActionCancel,
1703
- isDisabled: isActionCancelDisabled,
1704
- label: actionCancelLabel,
1705
- };
1586
+ const serializedPermissions = (permissions) => permissions.sort().join('_');
1587
+ const createCacheKey = (location) => `${location.scope}_${serializedPermissions(location.permissions)}`;
1588
+ const pastTTL = (credentials) => {
1589
+ const { expiration } = credentials;
1590
+ return expiration.getTime() - CREDENTIALS_REFRESH_WINDOW_MS <= Date.now();
1706
1591
  };
1707
-
1708
- function useResolvedComposable(DefaultComposable, name) {
1709
- const { composables } = useComposables();
1710
- const Composable = React__namespace["default"].useMemo(() => {
1711
- const ResolvedComposable = (props) => {
1712
- const Resolved = composables?.[name] ?? DefaultComposable;
1713
- return React__namespace["default"].createElement(Resolved, { ...props });
1714
- };
1715
- ResolvedComposable.displayName = name;
1716
- return ResolvedComposable;
1717
- }, [composables, DefaultComposable, name]);
1718
- return Composable;
1719
- }
1720
-
1721
- const ActionCancelControl = () => {
1722
- const props = useActionCancel();
1723
- const Resolved = useResolvedComposable(ActionCancel, 'ActionCancel');
1724
- return React__namespace["default"].createElement(Resolved, { ...props });
1592
+ const setCacheRecord = (store, key, value) => {
1593
+ if (store.capacity === store.values.size) {
1594
+ // Pop least used entry. The Map's key are in insertion order.
1595
+ // So first key is the last recently inserted.
1596
+ const [oldestKey] = store.values.keys();
1597
+ store.values.delete(oldestKey);
1598
+ // TODO(@AllanZhengYP): Add log info when record is evicted.
1599
+ }
1600
+ // Add latest used value to the cache.
1601
+ store.values.set(key, value);
1725
1602
  };
1726
-
1727
- const getNavigationItems = ({ destinationParts, location, onNavigate, }) => {
1728
- const { bucket, permissions, prefix = '', type } = location;
1729
- const destinationSubpaths = [];
1730
- return destinationParts.map((part, index) => {
1731
- const isCurrent = index === destinationParts.length - 1;
1732
- if (index !== 0) {
1733
- destinationSubpaths.push(part);
1603
+ const dispatchRefresh = (refreshHandler, value, onRefreshFailure) => {
1604
+ if (value.inflightCredentials) {
1605
+ return value.inflightCredentials;
1606
+ }
1607
+ value.inflightCredentials = (async () => {
1608
+ try {
1609
+ const { credentials } = await refreshHandler({
1610
+ scope: value.scope,
1611
+ permissions: value.permissions,
1612
+ });
1613
+ value.credentials = credentials;
1614
+ return { credentials };
1734
1615
  }
1735
- const destinationPath = `${destinationSubpaths.concat('').join('/')}`;
1736
- const destination = {
1737
- id: crypto.randomUUID(),
1738
- type,
1739
- permissions,
1740
- bucket,
1741
- prefix,
1742
- };
1743
- return {
1744
- name: part,
1745
- ...(isCurrent && { isCurrent }),
1746
- onNavigate: () => {
1747
- onNavigate?.(destination, destinationPath);
1748
- },
1749
- };
1750
- });
1751
- };
1752
-
1753
- const getNavigationParts = ({ location, path, includeBucketInPrefix, }) => {
1754
- const { bucket, prefix = '', type } = location;
1755
- const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
1756
- const trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path;
1757
- const firstPrefixPart = [];
1758
- if (type !== 'BUCKET') {
1759
- if (includeBucketInPrefix) {
1760
- firstPrefixPart.push(bucket);
1616
+ catch (e) {
1617
+ onRefreshFailure();
1618
+ throw e;
1761
1619
  }
1762
- if (trimmedPrefix) {
1763
- if (includeBucketInPrefix) {
1764
- firstPrefixPart.push('/');
1765
- }
1766
- firstPrefixPart.push(trimmedPrefix);
1620
+ finally {
1621
+ value.inflightCredentials = undefined;
1767
1622
  }
1623
+ })();
1624
+ return value.inflightCredentials;
1625
+ };
1626
+ /**
1627
+ * @internal
1628
+ */
1629
+ const initStore = (refreshHandler, size = CREDENTIALS_STORE_DEFAULT_SIZE) => {
1630
+ internals.assertValidationError(size > 0, internals.StorageValidationErrorCode.InvalidLocationCredentialsCacheSize);
1631
+ return {
1632
+ capacity: size,
1633
+ refreshHandler,
1634
+ values: new Map(),
1635
+ };
1636
+ };
1637
+ const getCacheValue = (store, location) => {
1638
+ const cacheKey = createCacheKey(location);
1639
+ const cachedValue = store.values.get(cacheKey);
1640
+ const cachedCredentials = cachedValue?.credentials;
1641
+ if (!cachedCredentials) {
1642
+ return null;
1768
1643
  }
1769
- const prefixParts = type === 'BUCKET' ? [bucket] : [firstPrefixPart.join('')];
1770
- if (type === 'BUCKET' && trimmedPrefix) {
1771
- prefixParts.push(trimmedPrefix);
1644
+ // Delete and re-insert to key to map to indicate a latest reference in LRU.
1645
+ store.values.delete(cacheKey);
1646
+ if (!pastTTL(cachedCredentials)) {
1647
+ // TODO(@AllanZhengYP): If the credential is still valid but will expire
1648
+ // soon, we should return credentials AND dispatch a refresh.
1649
+ store.values.set(cacheKey, cachedValue);
1650
+ return cachedCredentials;
1772
1651
  }
1773
- const pathParts = trimmedPath ? trimmedPath.split('/') : [];
1774
- return prefixParts.concat(pathParts);
1652
+ return null;
1775
1653
  };
1776
-
1777
- // import { ActionDestinationProps } from '../../composables/ActionDestination';
1778
- const useActionDestination = () => {
1779
- const { data, onSelectDestination } = useControlsContext();
1780
- const { actionDestinationLabel, isActionDestinationNavigable, destination } = data;
1781
- return React__namespace["default"].useMemo(() => {
1782
- if (!destination?.current) {
1783
- return { items: [] };
1784
- }
1785
- const { current, path } = destination;
1786
- const destinationParts = getNavigationParts({
1787
- location: current,
1788
- path,
1789
- });
1790
- return {
1791
- label: actionDestinationLabel,
1792
- items: getNavigationItems({
1793
- location: current,
1794
- destinationParts,
1795
- onNavigate: onSelectDestination,
1796
- }),
1797
- isNavigable: isActionDestinationNavigable,
1654
+ /**
1655
+ * Fetch new credentials value with refresh handler and cache the result in
1656
+ * LRU cache.
1657
+ * @internal
1658
+ */
1659
+ const fetchNewValue = async (store, location) => {
1660
+ const storeValues = store.values;
1661
+ const key = createCacheKey(location);
1662
+ if (!storeValues.has(key)) {
1663
+ const newStoreValue = {
1664
+ scope: location.scope,
1665
+ permissions: location.permissions,
1798
1666
  };
1799
- }, [
1800
- actionDestinationLabel,
1801
- isActionDestinationNavigable,
1802
- destination,
1803
- onSelectDestination,
1804
- ]);
1667
+ setCacheRecord(store, key, newStoreValue);
1668
+ }
1669
+ const storeValue = storeValues.get(key);
1670
+ return dispatchRefresh(store.refreshHandler, storeValue, () => {
1671
+ store.values.delete(key);
1672
+ });
1673
+ };
1674
+
1675
+ /**
1676
+ * Keep all cache records for all instances of credentials store in a singleton
1677
+ * so we can reliably de-reference from the memory when we destroy a store
1678
+ * instance.
1679
+ */
1680
+ const storeRegistry = new WeakMap();
1681
+ /**
1682
+ * @internal
1683
+ */
1684
+ const createStore = (refreshHandler, size) => {
1685
+ const storeSymbol = { value: Symbol('LocationCredentialsStore') };
1686
+ storeRegistry.set(storeSymbol, initStore(refreshHandler, size));
1687
+ return storeSymbol;
1688
+ };
1689
+ const getCredentialsStore = (storeSymbol) => {
1690
+ internals.assertValidationError(storeRegistry.has(storeSymbol), internals.StorageValidationErrorCode.LocationCredentialsStoreDestroyed);
1691
+ return storeRegistry.get(storeSymbol);
1692
+ };
1693
+ /**
1694
+ * @internal
1695
+ */
1696
+ const getValue = async (input) => {
1697
+ const { storeSymbol: storeReference, location, forceRefresh } = input;
1698
+ const store = getCredentialsStore(storeReference);
1699
+ if (!forceRefresh) {
1700
+ const credentials = getCacheValue(store, location);
1701
+ if (credentials !== null) {
1702
+ return { credentials };
1703
+ }
1704
+ }
1705
+ return fetchNewValue(store, location);
1805
1706
  };
1806
-
1807
- const ActionDestinationControl = () => {
1808
- const props = useActionDestination();
1809
- const Resolved = useResolvedComposable(ActionDestination$1, 'ActionDestination');
1810
- return React__namespace["default"].createElement(Resolved, { ...props });
1707
+ const removeStore = (storeSymbol) => {
1708
+ storeRegistry.delete(storeSymbol);
1811
1709
  };
1812
1710
 
1813
- const useActionExit = () => {
1814
- const { data: { actionExitLabel: label, isActionExitDisabled: isDisabled }, onActionExit: onExit, } = useControlsContext();
1815
- return { label, isDisabled, onExit };
1711
+ const validateS3Uri = (uri) => {
1712
+ const s3UrlSchemaRegex = /^s3:\/\/[^/]+/;
1713
+ internals.assertValidationError(s3UrlSchemaRegex.test(uri), internals.StorageValidationErrorCode.InvalidS3Uri);
1816
1714
  };
1817
-
1818
- const ActionExitControl = () => {
1819
- const props = useActionExit();
1820
- const Resolved = useResolvedComposable(ActionExit, 'ActionExit');
1821
- return React__namespace["default"].createElement(Resolved, { ...props });
1715
+ const createLocationCredentialsStore = (input) => {
1716
+ const storeSymbol = createStore(input.handler);
1717
+ const store = {
1718
+ getProvider(providerLocation) {
1719
+ const locationCredentialsProvider = async ({ forceRefresh = false, } = {}) => {
1720
+ validateS3Uri(providerLocation.scope);
1721
+ // TODO(@AllanZhengYP): validate the action bucket and paths matches provider scope.
1722
+ return getValue({
1723
+ storeSymbol,
1724
+ location: { ...providerLocation },
1725
+ forceRefresh,
1726
+ });
1727
+ };
1728
+ return locationCredentialsProvider;
1729
+ },
1730
+ destroy() {
1731
+ removeStore(storeSymbol);
1732
+ },
1733
+ };
1734
+ return store;
1822
1735
  };
1823
1736
 
1824
- const useActionStart = () => {
1825
- const { data: { actionStartLabel, isActionStartDisabled }, onActionStart, } = useControlsContext();
1737
+ const createCredentialsStore = ({ ...input }) => {
1738
+ const { destroy, getProvider } = createLocationCredentialsStore(input);
1826
1739
  return {
1827
- label: actionStartLabel,
1828
- isDisabled: isActionStartDisabled,
1829
- onStart: onActionStart,
1740
+ destroy,
1741
+ getCredentials: ({ scope, permissions }) => getProvider({
1742
+ scope,
1743
+ permissions,
1744
+ }),
1830
1745
  };
1831
1746
  };
1747
+ const isCredentialsStore = (value) => ui.isFunction(value?.getCredentials);
1748
+ function useCredentialsStore({ getLocationCredentials: handler, initialValue, onDestroy, registerAuthListener, }) {
1749
+ const hasExistingStore = isCredentialsStore(initialValue);
1750
+ const [store, setStore] = React__namespace["default"].useState(() => hasExistingStore ? initialValue : createCredentialsStore({ handler }));
1751
+ const { destroy } = store;
1752
+ React__namespace["default"].useEffect(() => {
1753
+ if (hasExistingStore) {
1754
+ return;
1755
+ }
1756
+ const handleAuthStatusChange = () => {
1757
+ destroy();
1758
+ if (ui.isFunction(onDestroy)) {
1759
+ onDestroy();
1760
+ }
1761
+ setStore(createCredentialsStore({ handler }));
1762
+ };
1763
+ // provide `handleAuthStatusChange` to consumer
1764
+ registerAuthListener(handleAuthStatusChange);
1765
+ }, [destroy, handler, hasExistingStore, onDestroy, registerAuthListener]);
1766
+ return store;
1767
+ }
1832
1768
 
1833
- const ActionStartControl = () => {
1834
- const props = useActionStart();
1835
- const Resolved = useResolvedComposable(ActionStart, 'ActionStart');
1836
- return React__namespace["default"].createElement(Resolved, { ...props });
1837
- };
1769
+ const ERROR_MESSAGE$3 = '`useCredentials` must be called from within a `CredentialsProvider`.';
1770
+ const { useCredentials, CredentialsContext } = uiReactCore.createContextUtilities({
1771
+ contextName: 'Credentials',
1772
+ errorMessage: ERROR_MESSAGE$3,
1773
+ });
1774
+ function CredentialsProvider({ children, ...props }) {
1775
+ const initialValue = React__namespace["default"].useContext(CredentialsContext);
1776
+ const value = useCredentialsStore({ ...props, initialValue });
1777
+ return (React__namespace["default"].createElement(CredentialsContext.Provider, { value: value }, children));
1778
+ }
1838
1779
 
1839
- const useAddFiles = () => {
1840
- const { data: { addFilesLabel, isAddFilesDisabled }, onAddFiles, } = useControlsContext();
1841
- return {
1842
- isDisabled: isAddFilesDisabled,
1843
- label: addFilesLabel,
1844
- onAddFiles,
1845
- };
1780
+ const ERROR_MESSAGE$2 = 'Invalid `location` value provided as initial value to `LocationProvider.';
1781
+ const DEFAULT_STATE$1 = {
1782
+ current: undefined,
1783
+ path: '',
1784
+ key: '',
1846
1785
  };
1786
+ function handleAction$1(state, action) {
1787
+ switch (action.type) {
1788
+ case 'NAVIGATE': {
1789
+ const { location, path = '' } = action;
1790
+ if (state.current?.id === location.id && state.path === path) {
1791
+ return state;
1792
+ }
1793
+ return {
1794
+ current: location,
1795
+ path,
1796
+ key: `${location.prefix ?? ''}${path}`,
1797
+ };
1798
+ }
1799
+ case 'RESET_LOCATION': {
1800
+ return DEFAULT_STATE$1;
1801
+ }
1802
+ }
1803
+ }
1804
+ const defaultValue$7 = [DEFAULT_STATE$1, ui.noop];
1805
+ const { LocationContext, useLocation } = uiReactCore.createContextUtilities({
1806
+ contextName: 'Location',
1807
+ defaultValue: defaultValue$7,
1808
+ });
1809
+ function LocationProvider({ children, location, path = '', }) {
1810
+ if (location) {
1811
+ assertLocationData(location, ERROR_MESSAGE$2);
1812
+ }
1813
+ const value = React__namespace["default"].useReducer(handleAction$1, location
1814
+ ? {
1815
+ current: location,
1816
+ path,
1817
+ key: `${location.prefix ?? ''}${path}`,
1818
+ }
1819
+ : DEFAULT_STATE$1);
1820
+ return (React__namespace["default"].createElement(LocationContext.Provider, { value: value }, children));
1821
+ }
1847
1822
 
1848
- const AddFilesControl = () => {
1849
- const props = useAddFiles();
1850
- const Resolved = useResolvedComposable(AddFiles, 'AddFiles');
1851
- return React__namespace["default"].createElement(Resolved, { ...props });
1823
+ const compareFileItems = (prev, next) => prev.key.localeCompare(next.key);
1824
+ const resolveFiles = (prevItems, files) => {
1825
+ if (!files?.length)
1826
+ return prevItems;
1827
+ // construct `nextItems` and filter out existing `file` entries
1828
+ const nextItems = files.reduce((items, file) => {
1829
+ const { name, webkitRelativePath } = file;
1830
+ return prevItems.some(({ file: existing }) => existing.name === name &&
1831
+ existing.webkitRelativePath === webkitRelativePath)
1832
+ ? items
1833
+ : items.concat({
1834
+ key: ui.isEmpty(webkitRelativePath) ? name : webkitRelativePath,
1835
+ id: crypto.randomUUID(),
1836
+ file,
1837
+ });
1838
+ }, []);
1839
+ if (!nextItems.length)
1840
+ return prevItems;
1841
+ if (!prevItems.length) {
1842
+ return nextItems.sort(compareFileItems);
1843
+ }
1844
+ return prevItems.concat(nextItems).sort(compareFileItems);
1852
1845
  };
1853
-
1854
- const useAddFolder = () => {
1855
- const { data: { addFolderLabel, isAddFolderDisabled }, onAddFolder, } = useControlsContext();
1856
- return {
1857
- isDisabled: isAddFolderDisabled,
1858
- label: addFolderLabel,
1859
- onAddFolder,
1860
- };
1846
+ const filesReducer = (prevItems, input) => {
1847
+ switch (input.type) {
1848
+ case 'ADD_FILE_ITEMS': {
1849
+ return resolveFiles(prevItems, input.files);
1850
+ }
1851
+ case 'REMOVE_FILE_ITEM': {
1852
+ const filteredItems = prevItems.filter(({ id }) => id !== input.id);
1853
+ return filteredItems.length === prevItems.length
1854
+ ? prevItems
1855
+ : filteredItems;
1856
+ }
1857
+ case 'RESET_FILE_ITEMS': {
1858
+ return [];
1859
+ }
1860
+ // TODO: clear message
1861
+ }
1861
1862
  };
1862
-
1863
- const AddFolderControl = () => {
1864
- const props = useAddFolder();
1865
- const Resolved = useResolvedComposable(AddFolder, 'AddFolder');
1866
- return React__namespace["default"].createElement(Resolved, { ...props });
1863
+ const parseFileSelectParams = (value) => {
1864
+ if (ui.isUndefined(value))
1865
+ return ['FILE', undefined];
1866
+ if (ui.isString(value))
1867
+ return [value, undefined];
1868
+ const [selectType, ...rest] = value;
1869
+ return [selectType, !rest?.length ? undefined : { accept: rest.join() }];
1867
1870
  };
1868
1871
 
1869
- const compareContent$3 = ({ label: a }, { label: b }) => {
1870
- if (a === undefined) {
1871
- return b === undefined ? 0 : 1;
1872
+ const defaultValue$6 = [undefined, ui.noop];
1873
+ const { FilesContext, useFiles } = uiReactCore.createContextUtilities({
1874
+ contextName: 'Files',
1875
+ defaultValue: defaultValue$6,
1876
+ });
1877
+ function FilesProvider({ children, }) {
1878
+ const [items, dispatch] = React__namespace["default"].useReducer(filesReducer, []);
1879
+ const [fileInput, handleFileSelect] = internal.useFileSelect((nextFiles) => {
1880
+ dispatch({ type: 'ADD_FILE_ITEMS', files: nextFiles });
1881
+ });
1882
+ const handleFilesAction = React__namespace["default"].useCallback((action) => {
1883
+ if (action.type === 'SELECT_FILES') {
1884
+ handleFileSelect(...parseFileSelectParams(action.selectionType));
1885
+ }
1886
+ else {
1887
+ dispatch(action);
1888
+ }
1889
+ }, [handleFileSelect]);
1890
+ const value = React__namespace["default"].useMemo(() => [items, handleFilesAction], [items, handleFilesAction]);
1891
+ return (React__namespace["default"].createElement(FilesContext.Provider, { value: value },
1892
+ fileInput,
1893
+ children));
1894
+ }
1895
+
1896
+ const handleAction = (event) => {
1897
+ switch (event.type) {
1898
+ case 'SET_ACTION_TYPE': {
1899
+ return event.actionType;
1900
+ }
1901
+ case 'RESET_ACTION_TYPE': {
1902
+ return undefined;
1903
+ }
1872
1904
  }
1873
- return b === undefined ? -1 : a.localeCompare(b);
1874
1905
  };
1875
- const compareButtonData = (a, b, direction) => direction === 'ascending'
1876
- ? compareContent$3(a.content, b.content)
1877
- : compareContent$3(b.content, a.content);
1906
+ function useActionTypeState(initialState) {
1907
+ const [actionType, setActionType] = React__namespace["default"].useState(initialState);
1908
+ const handler = React__namespace["default"].useCallback((action) => setActionType(handleAction(action)), []);
1909
+ return [actionType, handler];
1910
+ }
1911
+
1912
+ const defaultValue$5 = [undefined, ui.noop];
1913
+ const { ActionTypeContext, useActionType } = uiReactCore.createContextUtilities({
1914
+ contextName: 'ActionType',
1915
+ defaultValue: defaultValue$5,
1916
+ });
1917
+ function ActionTypeProvider({ actionType, children, }) {
1918
+ const value = useActionTypeState(actionType);
1919
+ return (React__namespace["default"].createElement(ActionTypeContext.Provider, { value: value }, children));
1920
+ }
1878
1921
 
1879
- const compareContent$2 = ({ value: a }, { value: b }) => {
1880
- if (a === undefined) {
1881
- return b === undefined ? 0 : 1;
1882
- }
1883
- return b === undefined ? -1 : a.getTime() - b.getTime();
1922
+ const DEFAULT_STATE = {
1923
+ fileDataItems: undefined,
1884
1924
  };
1885
- const compareDateData = (a, b, direction) => direction === 'ascending'
1886
- ? compareContent$2(a.content, b.content)
1887
- : compareContent$2(b.content, a.content);
1888
-
1889
- const compareContent$1 = ({ value: a }, { value: b }) => {
1890
- if (a === undefined) {
1891
- return b === undefined ? 0 : 1;
1925
+ const locationItemsReducer = (prevState, event) => {
1926
+ switch (event.type) {
1927
+ case 'SET_LOCATION_ITEMS': {
1928
+ const { items } = event;
1929
+ if (!items?.length)
1930
+ return prevState;
1931
+ if (!prevState.fileDataItems?.length) {
1932
+ return { fileDataItems: items.map(createFileDataItem) };
1933
+ }
1934
+ const nextFileDataItems = items?.reduce((fileDataItems, data) => prevState.fileDataItems?.some(({ id }) => id === data.id)
1935
+ ? fileDataItems
1936
+ : fileDataItems.concat(createFileDataItem(data)), []);
1937
+ if (!nextFileDataItems?.length)
1938
+ return prevState;
1939
+ return {
1940
+ fileDataItems: prevState.fileDataItems.concat(nextFileDataItems),
1941
+ };
1942
+ }
1943
+ case 'REMOVE_LOCATION_ITEM': {
1944
+ const { id } = event;
1945
+ if (!prevState.fileDataItems)
1946
+ return prevState;
1947
+ const fileDataItems = prevState.fileDataItems.filter((item) => item.id !== id);
1948
+ if (fileDataItems.length === prevState.fileDataItems.length) {
1949
+ return prevState;
1950
+ }
1951
+ return { fileDataItems };
1952
+ }
1953
+ case 'RESET_LOCATION_ITEMS': {
1954
+ return DEFAULT_STATE;
1955
+ }
1892
1956
  }
1893
- return b === undefined ? -1 : a - b;
1894
1957
  };
1895
- const compareNumberData = (a, b, direction) => direction === 'ascending'
1896
- ? compareContent$1(a.content, b.content)
1897
- : compareContent$1(b.content, a.content);
1958
+ const defaultValue$4 = [DEFAULT_STATE, ui.noop];
1959
+ const { LocationItemsContext, useLocationItems } = uiReactCore.createContextUtilities({ contextName: 'LocationItems', defaultValue: defaultValue$4 });
1960
+ function LocationItemsProvider({ children, }) {
1961
+ const value = React__namespace["default"].useReducer(locationItemsReducer, DEFAULT_STATE);
1962
+ return (React__namespace["default"].createElement(LocationItemsContext.Provider, { value: value }, children));
1963
+ }
1898
1964
 
1899
- const compareContent = ({ text: a }, { text: b }) => {
1900
- if (a === undefined) {
1901
- return b === undefined ? 0 : 1;
1902
- }
1903
- return b === undefined ? -1 : a.localeCompare(b);
1904
- };
1905
- const compareTextData = (a, b, direction) => direction === 'ascending'
1906
- ? compareContent(a.content, b.content)
1907
- : compareContent(b.content, a.content);
1965
+ function StoreProvider(props) {
1966
+ const { actionType, children, location, path } = props;
1967
+ return (React__namespace["default"].createElement(FilesProvider, null,
1968
+ React__namespace["default"].createElement(LocationProvider, { location: location, path: path },
1969
+ React__namespace["default"].createElement(LocationItemsProvider, null,
1970
+ React__namespace["default"].createElement(ActionTypeProvider, { actionType: actionType }, children)))));
1971
+ }
1908
1972
 
1909
- const GROUP_ORDER = [
1910
- 'checkbox',
1911
- 'button',
1912
- 'date',
1913
- 'number',
1914
- 'text',
1915
- ];
1916
- const UNSORTABLE_GROUPS = ['checkbox'];
1917
- const useDataTable = () => {
1918
- const { data } = useControlsContext();
1919
- const { tableData } = data;
1920
- const defaultSortIndex = React__namespace["default"].useMemo(() => tableData?.headers?.findIndex(({ type }) => type === 'sort') ?? -1, [tableData]);
1921
- const [sortState, setSortState] = React__namespace["default"].useState({
1922
- index: defaultSortIndex,
1923
- direction: 'ascending',
1924
- });
1925
- const mappedHeaders = React__namespace["default"].useMemo(() => tableData?.headers.map((header, index) => {
1926
- const { type } = header;
1927
- switch (type) {
1928
- case 'sort': {
1929
- return {
1930
- ...header,
1931
- content: {
1932
- ...header.content,
1933
- onSort: () => {
1934
- setSortState({
1935
- index,
1936
- direction: sortState.index === index
1937
- ? sortState.direction === 'ascending'
1938
- ? 'descending'
1939
- : 'ascending'
1940
- : 'ascending',
1941
- });
1942
- },
1943
- sortDirection: sortState.index === index ? sortState.direction : undefined,
1944
- },
1945
- };
1973
+ function useStore() {
1974
+ const [actionType, dispatchActionType] = useActionType();
1975
+ const [files, dispatchFilesAction] = useFiles();
1976
+ const [location, dispatchLocationAction] = useLocation();
1977
+ const [locationItems, dispatchLocationItemsAction] = useLocationItems();
1978
+ const dispatchHandler = React__namespace["default"].useCallback((action) => {
1979
+ switch (action.type) {
1980
+ case 'ADD_FILE_ITEMS':
1981
+ case 'REMOVE_FILE_ITEM':
1982
+ case 'SELECT_FILES':
1983
+ case 'RESET_FILE_ITEMS': {
1984
+ dispatchFilesAction(action);
1985
+ break;
1946
1986
  }
1947
- case 'checkbox':
1948
- case 'text':
1949
- default: {
1950
- return header;
1987
+ case 'NAVIGATE':
1988
+ case 'RESET_LOCATION': {
1989
+ dispatchLocationAction(action);
1990
+ break;
1991
+ }
1992
+ case 'SET_LOCATION_ITEMS':
1993
+ case 'REMOVE_LOCATION_ITEM':
1994
+ case 'RESET_LOCATION_ITEMS': {
1995
+ dispatchLocationItemsAction(action);
1996
+ break;
1997
+ }
1998
+ case 'SET_ACTION_TYPE':
1999
+ case 'RESET_ACTION_TYPE': {
2000
+ dispatchActionType(action);
2001
+ break;
1951
2002
  }
1952
2003
  }
1953
- }), [sortState, tableData]);
1954
- const sortedRows = React__namespace["default"].useMemo(() => {
1955
- // Early return if there is no table data
1956
- if (!tableData) {
1957
- return;
1958
- }
1959
- // Return rows as is if there are no sortable columns
1960
- if (sortState.index < 0) {
1961
- return tableData.rows;
1962
- }
1963
- const { index, direction } = sortState;
1964
- const groupedRows = {
1965
- button: [],
1966
- checkbox: [],
1967
- date: [],
1968
- number: [],
1969
- text: [],
2004
+ }, [
2005
+ dispatchActionType,
2006
+ dispatchFilesAction,
2007
+ dispatchLocationAction,
2008
+ dispatchLocationItemsAction,
2009
+ ]);
2010
+ return [{ actionType, files, location, locationItems }, dispatchHandler];
2011
+ }
2012
+
2013
+ const getErrorMessage = (propertyName) => `Unable to resolve credentials due to invalid value of '${propertyName}'`;
2014
+ function useGetActionInputCallback({ accountId, customEndpoint, region, }) {
2015
+ const { getCredentials } = useCredentials();
2016
+ const [{ location }] = useStore();
2017
+ const { current, key } = location;
2018
+ return React__namespace["default"].useCallback((_location) => {
2019
+ // prefer passed in location / prefix over current location in state
2020
+ const location = _location ?? current;
2021
+ // when `location` has been provided as a param, resolve `_prefix` to `location.prefix`.
2022
+ // in the default scenario where `current` is the target `location` use the fully qualified `key`
2023
+ // that includes the default `prefix` and any additional prefixes from navigation
2024
+ const prefix = _location ? _location.prefix : key;
2025
+ assertLocationData(location, getErrorMessage('locationData'));
2026
+ assertPrefix(prefix, getErrorMessage('prefix'));
2027
+ const { bucket, permissions, type } = location;
2028
+ // BUCKET/PREFIX grants end with `*`, but object grants do not.
2029
+ const scope = `s3://${bucket}/${prefix}${type === 'OBJECT' ? '' : '*'}`;
2030
+ return {
2031
+ accountId,
2032
+ bucket,
2033
+ credentials: getCredentials({
2034
+ permissions,
2035
+ scope,
2036
+ }),
2037
+ region,
2038
+ customEndpoint,
1970
2039
  };
1971
- tableData.rows.forEach((row) => {
1972
- const { type } = row.content[index];
1973
- groupedRows[type].push(row);
1974
- });
1975
- const groupOrder = direction === 'ascending' ? GROUP_ORDER : [...GROUP_ORDER].reverse();
1976
- return groupOrder
1977
- .map((groupType) => {
1978
- if (UNSORTABLE_GROUPS.includes(groupType)) {
1979
- return groupedRows[groupType];
1980
- }
1981
- return groupedRows[groupType].sort((rowA, rowB) => {
1982
- switch (groupType) {
1983
- case 'button': {
1984
- return compareButtonData(rowA.content[index], rowB.content[index], direction);
1985
- }
1986
- case 'date': {
1987
- return compareDateData(rowA.content[index], rowB.content[index], direction);
1988
- }
1989
- case 'number': {
1990
- return compareNumberData(rowA.content[index], rowB.content[index], direction);
1991
- }
1992
- case 'text':
1993
- default: {
1994
- return compareTextData(rowA.content[index], rowB.content[index], direction);
1995
- }
1996
- }
1997
- });
1998
- })
1999
- .flat();
2000
- }, [sortState, tableData]);
2040
+ }, [accountId, current, customEndpoint, getCredentials, key, region]);
2041
+ }
2042
+
2043
+ const ERROR_MESSAGE$1 = '`useGetActionInput` must be called from within a `ConfigurationProvider`.';
2044
+ const { useGetActionInput, GetActionInputContext } = uiReactCore.createContextUtilities({
2045
+ contextName: 'GetActionInput',
2046
+ errorMessage: ERROR_MESSAGE$1,
2047
+ });
2048
+ function GetActionInputProvider({ accountId, children, customEndpoint, region, }) {
2049
+ const value = useGetActionInputCallback({
2050
+ accountId,
2051
+ customEndpoint,
2052
+ region,
2053
+ });
2054
+ return (React__namespace["default"].createElement(GetActionInputContext.Provider, { value: value }, children));
2055
+ }
2056
+
2057
+ const Passthrough = ({ children }) => (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, children));
2058
+ function createConfigurationProvider(input) {
2059
+ const { accountId, ChildComponent, displayName, region, customEndpoint, ...rest } = input;
2060
+ const Child = elements.isComponent(ChildComponent) ? ChildComponent : Passthrough;
2061
+ const Provider = (props) => (React__namespace["default"].createElement(CredentialsProvider, { ...rest },
2062
+ React__namespace["default"].createElement(GetActionInputProvider, { accountId: accountId, region: region, customEndpoint: customEndpoint },
2063
+ React__namespace["default"].createElement(Child, { ...props }))));
2064
+ Provider.displayName = displayName;
2065
+ return Provider;
2066
+ }
2067
+
2068
+ const defaultValue$3 = { data: {} };
2069
+ const { useControlsContext, ControlsContextProvider } = uiReactCore.createContextUtilities({
2070
+ contextName: 'ControlsContext',
2071
+ defaultValue: defaultValue$3,
2072
+ });
2073
+
2074
+ const useActionCancel = () => {
2075
+ const { data: { actionCancelLabel, isActionCancelDisabled }, onActionCancel, } = useControlsContext();
2001
2076
  return {
2002
- headers: mappedHeaders ?? [],
2003
- rows: sortedRows ?? [],
2077
+ onCancel: onActionCancel,
2078
+ isDisabled: isActionCancelDisabled,
2079
+ label: actionCancelLabel,
2004
2080
  };
2005
2081
  };
2006
2082
 
2007
- const DataTableControl = () => {
2008
- const props = useDataTable();
2009
- const Resolved = useResolvedComposable(DataTable, 'DataTable');
2083
+ function useResolvedComposable(DefaultComposable, name) {
2084
+ const { composables } = useComposables();
2085
+ const Composable = React__namespace["default"].useMemo(() => {
2086
+ const ResolvedComposable = (props) => {
2087
+ const Resolved = composables?.[name] ?? DefaultComposable;
2088
+ return React__namespace["default"].createElement(Resolved, { ...props });
2089
+ };
2090
+ ResolvedComposable.displayName = name;
2091
+ return ResolvedComposable;
2092
+ }, [composables, DefaultComposable, name]);
2093
+ return Composable;
2094
+ }
2095
+
2096
+ const ActionCancelControl = () => {
2097
+ const props = useActionCancel();
2098
+ const Resolved = useResolvedComposable(ActionCancel, 'ActionCancel');
2010
2099
  return React__namespace["default"].createElement(Resolved, { ...props });
2011
2100
  };
2012
2101
 
2013
- /**
2014
- * This hook, not to be confused with the useDropZone vended from @aws-amplify/ui-react-core, is only intended for use
2015
- * with its corresponding DropZone control.
2016
- */
2017
- const useDropZone = () => {
2018
- const { onDropFiles } = useControlsContext();
2019
- return { onDropFiles };
2102
+ const getNavigationItems = ({ destinationParts, location, onNavigate, }) => {
2103
+ const { bucket, permissions, prefix = '', type } = location;
2104
+ const destinationSubpaths = [];
2105
+ return destinationParts.map((part, index) => {
2106
+ const isCurrent = index === destinationParts.length - 1;
2107
+ if (index !== 0) {
2108
+ destinationSubpaths.push(part);
2109
+ }
2110
+ const destinationPath = `${destinationSubpaths.concat('').join('/')}`;
2111
+ const destination = {
2112
+ id: crypto.randomUUID(),
2113
+ type,
2114
+ permissions,
2115
+ bucket,
2116
+ prefix,
2117
+ };
2118
+ return {
2119
+ name: part,
2120
+ ...(isCurrent && { isCurrent }),
2121
+ onNavigate: () => {
2122
+ onNavigate?.(destination, destinationPath);
2123
+ },
2124
+ };
2125
+ });
2020
2126
  };
2021
2127
 
2022
- const DropZoneControl = ({ children, }) => {
2023
- const props = useDropZone();
2024
- const Resolved = useResolvedComposable(DropZone, 'DropZone');
2025
- return React__namespace["default"].createElement(Resolved, { ...props }, children);
2128
+ const getNavigationParts = ({ location, path, includeBucketInPrefix, }) => {
2129
+ const { bucket, prefix = '', type } = location;
2130
+ const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
2131
+ const trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path;
2132
+ const firstPrefixPart = [];
2133
+ if (type !== 'BUCKET') {
2134
+ if (includeBucketInPrefix) {
2135
+ firstPrefixPart.push(bucket);
2136
+ }
2137
+ if (trimmedPrefix) {
2138
+ if (includeBucketInPrefix) {
2139
+ firstPrefixPart.push('/');
2140
+ }
2141
+ firstPrefixPart.push(trimmedPrefix);
2142
+ }
2143
+ }
2144
+ const prefixParts = type === 'BUCKET' ? [bucket] : [firstPrefixPart.join('')];
2145
+ if (type === 'BUCKET' && trimmedPrefix) {
2146
+ prefixParts.push(trimmedPrefix);
2147
+ }
2148
+ const pathParts = trimmedPath ? trimmedPath.split('/') : [];
2149
+ return prefixParts.concat(pathParts);
2026
2150
  };
2027
2151
 
2028
- const useOverwriteToggle = () => {
2029
- const { data: { isOverwritingEnabled, isOverwriteToggleDisabled, overwriteToggleLabel, }, onToggleOverwrite, } = useControlsContext();
2030
- return {
2031
- isDisabled: isOverwriteToggleDisabled,
2032
- isOverwritingEnabled,
2033
- label: overwriteToggleLabel,
2034
- onToggle: onToggleOverwrite,
2035
- };
2152
+ // import { ActionDestinationProps } from '../../composables/ActionDestination';
2153
+ const useActionDestination = () => {
2154
+ const { data, onSelectDestination } = useControlsContext();
2155
+ const { actionDestinationLabel, isActionDestinationNavigable, destination } = data;
2156
+ return React__namespace["default"].useMemo(() => {
2157
+ if (!destination?.current) {
2158
+ return { items: [] };
2159
+ }
2160
+ const { current, path } = destination;
2161
+ const destinationParts = getNavigationParts({
2162
+ location: current,
2163
+ path,
2164
+ });
2165
+ return {
2166
+ label: actionDestinationLabel,
2167
+ items: getNavigationItems({
2168
+ location: current,
2169
+ destinationParts,
2170
+ onNavigate: onSelectDestination,
2171
+ }),
2172
+ isNavigable: isActionDestinationNavigable,
2173
+ };
2174
+ }, [
2175
+ actionDestinationLabel,
2176
+ isActionDestinationNavigable,
2177
+ destination,
2178
+ onSelectDestination,
2179
+ ]);
2036
2180
  };
2037
2181
 
2038
- const OverwriteToggleControl = () => {
2039
- const props = useOverwriteToggle();
2040
- const Resolved = useResolvedComposable(OverwriteToggle$1, 'OverwriteToggle');
2182
+ const ActionDestinationControl = () => {
2183
+ const props = useActionDestination();
2184
+ const Resolved = useResolvedComposable(ActionDestination$1, 'ActionDestination');
2041
2185
  return React__namespace["default"].createElement(Resolved, { ...props });
2042
2186
  };
2043
2187
 
2044
- const useMessage = () => {
2045
- const { data: { message = {} }, } = useControlsContext();
2046
- return message;
2188
+ const useActionExit = () => {
2189
+ const { data: { actionExitLabel: label, isActionExitDisabled: isDisabled }, onActionExit: onExit, } = useControlsContext();
2190
+ return { label, isDisabled, onExit };
2047
2191
  };
2048
2192
 
2049
- const MessageControl = () => {
2050
- const props = useMessage();
2051
- const Resolved = useResolvedComposable(Message, 'Message');
2193
+ const ActionExitControl = () => {
2194
+ const props = useActionExit();
2195
+ const Resolved = useResolvedComposable(ActionExit, 'ActionExit');
2052
2196
  return React__namespace["default"].createElement(Resolved, { ...props });
2053
2197
  };
2054
2198
 
2055
- const useStatusDisplay = () => {
2056
- const { data } = useControlsContext();
2057
- const { statusCounts, statusDisplayCanceledLabel, statusDisplayCompletedLabel, statusDisplayFailedLabel, statusDisplayQueuedLabel, } = data;
2058
- if (!statusCounts?.TOTAL) {
2059
- return { statuses: [], total: 0 };
2060
- }
2061
- const statuses = [
2062
- { name: statusDisplayCompletedLabel ?? '', count: statusCounts.COMPLETE },
2063
- { name: statusDisplayFailedLabel ?? '', count: statusCounts.FAILED },
2064
- { name: statusDisplayCanceledLabel ?? '', count: statusCounts.CANCELED },
2065
- { name: statusDisplayQueuedLabel ?? '', count: statusCounts.QUEUED },
2066
- ];
2067
- return { statuses, total: statusCounts.TOTAL };
2199
+ const useActionStart = () => {
2200
+ const { data: { actionStartLabel, isActionStartDisabled }, onActionStart, } = useControlsContext();
2201
+ return {
2202
+ label: actionStartLabel,
2203
+ isDisabled: isActionStartDisabled,
2204
+ onStart: onActionStart,
2205
+ };
2068
2206
  };
2069
2207
 
2070
- const StatusDisplayControl = () => {
2071
- const props = useStatusDisplay();
2072
- const Resolved = useResolvedComposable(StatusDisplay$1, 'StatusDisplay');
2208
+ const ActionStartControl = () => {
2209
+ const props = useActionStart();
2210
+ const Resolved = useResolvedComposable(ActionStart, 'ActionStart');
2073
2211
  return React__namespace["default"].createElement(Resolved, { ...props });
2074
2212
  };
2075
2213
 
2076
- const useTitle = () => {
2077
- const { data } = useControlsContext();
2214
+ const useAddFiles = () => {
2215
+ const { data: { addFilesLabel, isAddFilesDisabled }, onAddFiles, } = useControlsContext();
2078
2216
  return {
2079
- title: data?.title,
2217
+ isDisabled: isAddFilesDisabled,
2218
+ label: addFilesLabel,
2219
+ onAddFiles,
2080
2220
  };
2081
2221
  };
2082
2222
 
2083
- const TitleControl = () => {
2084
- const props = useTitle();
2085
- const Resolved = useResolvedComposable(Title$1, 'Title');
2223
+ const AddFilesControl = () => {
2224
+ const props = useAddFiles();
2225
+ const Resolved = useResolvedComposable(AddFiles, 'AddFiles');
2086
2226
  return React__namespace["default"].createElement(Resolved, { ...props });
2087
2227
  };
2088
2228
 
2089
- const DEFAULT_ACTION_VIEW_DISPLAY_TEXT = {
2090
- actionCancelLabel: 'Cancel',
2091
- actionExitLabel: 'Exit',
2092
- actionDestinationLabel: 'Destination',
2093
- statusDisplayCanceledLabel: 'Canceled',
2094
- statusDisplayCompletedLabel: 'Completed',
2095
- statusDisplayFailedLabel: 'Failed',
2096
- statusDisplayInProgressLabel: 'In progress',
2097
- statusDisplayTotalLabel: 'Total',
2098
- statusDisplayQueuedLabel: 'Not started',
2099
- // empty by default
2100
- tableColumnCancelHeader: '',
2101
- tableColumnStatusHeader: 'Status',
2102
- tableColumnFolderHeader: 'Folder',
2103
- tableColumnNameHeader: 'Name',
2104
- tableColumnTypeHeader: 'Type',
2105
- tableColumnSizeHeader: 'Size',
2106
- tableColumnProgressHeader: 'Progress',
2229
+ const useAddFolder = () => {
2230
+ const { data: { addFolderLabel, isAddFolderDisabled }, onAddFolder, } = useControlsContext();
2231
+ return {
2232
+ isDisabled: isAddFolderDisabled,
2233
+ label: addFolderLabel,
2234
+ onAddFolder,
2235
+ };
2107
2236
  };
2108
- const DEFAULT_LIST_VIEW_DISPLAY_TEXT = {
2109
- loadingIndicatorLabel: 'Loading',
2110
- searchSubmitLabel: 'Submit',
2111
- searchClearLabel: 'Clear search',
2112
- getDateDisplayValue: (date) => new Intl.DateTimeFormat('en-US', {
2113
- month: 'short',
2114
- day: 'numeric',
2115
- hour: 'numeric',
2116
- year: 'numeric',
2117
- minute: 'numeric',
2118
- hourCycle: 'h12',
2119
- }).format(date),
2237
+
2238
+ const AddFolderControl = () => {
2239
+ const props = useAddFolder();
2240
+ const Resolved = useResolvedComposable(AddFolder, 'AddFolder');
2241
+ return React__namespace["default"].createElement(Resolved, { ...props });
2120
2242
  };
2121
2243
 
2122
- const DEFAULT_CREATE_FOLDER_VIEW_DISPLAY_TEXT = {
2123
- ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2124
- title: 'Create folder',
2125
- actionStartLabel: 'Create folder',
2126
- folderNameLabel: 'Folder name',
2127
- folderNamePlaceholder: 'Folder name cannot contain "/", nor end or start with "."',
2128
- getValidationMessage: () => 'Folder name cannot contain "/", nor end or start with "."',
2129
- getActionCompleteMessage: (data) => {
2130
- const { counts } = data ?? {};
2131
- const { FAILED, OVERWRITE_PREVENTED } = counts ?? {};
2132
- if (OVERWRITE_PREVENTED) {
2133
- return {
2134
- content: 'A folder already exists with the provided name',
2135
- type: 'warning',
2136
- };
2137
- }
2138
- if (FAILED) {
2139
- return {
2140
- content: 'There was an issue creating the folder.',
2141
- type: 'error',
2142
- };
2143
- }
2144
- return { content: 'Folder created.', type: 'success' };
2145
- },
2244
+ const compareContent$3 = ({ label: a }, { label: b }) => {
2245
+ if (a === undefined) {
2246
+ return b === undefined ? 0 : 1;
2247
+ }
2248
+ return b === undefined ? -1 : a.localeCompare(b);
2249
+ };
2250
+ const compareButtonData = (a, b, direction) => direction === 'ascending'
2251
+ ? compareContent$3(a.content, b.content)
2252
+ : compareContent$3(b.content, a.content);
2253
+
2254
+ const compareContent$2 = ({ value: a }, { value: b }) => {
2255
+ if (a === undefined) {
2256
+ return b === undefined ? 0 : 1;
2257
+ }
2258
+ return b === undefined ? -1 : a.getTime() - b.getTime();
2146
2259
  };
2260
+ const compareDateData = (a, b, direction) => direction === 'ascending'
2261
+ ? compareContent$2(a.content, b.content)
2262
+ : compareContent$2(b.content, a.content);
2147
2263
 
2148
- const DEFAULT_COPY_VIEW_DISPLAY_TEXT = {
2149
- ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2150
- title: 'Copy',
2151
- actionStartLabel: 'Copy',
2152
- actionDestinationLabel: 'Copy destination',
2153
- getListFoldersResultsMessage: ({ folders, query, message, hasError, hasExhaustedSearch, }) => {
2154
- if (!folders?.length) {
2155
- return {
2156
- content: query
2157
- ? `No folders found matching "${query}"`
2158
- : 'No subfolders found within selected folder.',
2159
- type: 'info',
2160
- };
2161
- }
2162
- if (message && !!query) {
2163
- return { content: 'Error loading folders.', type: 'error' };
2164
- }
2165
- if (hasError) {
2166
- return { content: 'Error loading folders.', type: 'error' };
2167
- }
2168
- if (hasExhaustedSearch) {
2169
- return {
2170
- content: 'Showing results for up to the first 10,000 items.',
2171
- type: 'info',
2172
- };
2264
+ const compareContent$1 = ({ value: a }, { value: b }) => {
2265
+ if (a === undefined) {
2266
+ return b === undefined ? 0 : 1;
2267
+ }
2268
+ return b === undefined ? -1 : a - b;
2269
+ };
2270
+ const compareNumberData = (a, b, direction) => direction === 'ascending'
2271
+ ? compareContent$1(a.content, b.content)
2272
+ : compareContent$1(b.content, a.content);
2273
+
2274
+ const compareContent = ({ text: a }, { text: b }) => {
2275
+ if (a === undefined) {
2276
+ return b === undefined ? 0 : 1;
2277
+ }
2278
+ return b === undefined ? -1 : a.localeCompare(b);
2279
+ };
2280
+ const compareTextData = (a, b, direction) => direction === 'ascending'
2281
+ ? compareContent(a.content, b.content)
2282
+ : compareContent(b.content, a.content);
2283
+
2284
+ const GROUP_ORDER = [
2285
+ 'checkbox',
2286
+ 'button',
2287
+ 'date',
2288
+ 'number',
2289
+ 'text',
2290
+ ];
2291
+ const UNSORTABLE_GROUPS = ['checkbox'];
2292
+ const useDataTable = () => {
2293
+ const { data } = useControlsContext();
2294
+ const { tableData } = data;
2295
+ const defaultSortIndex = React__namespace["default"].useMemo(() => tableData?.headers?.findIndex(({ type }) => type === 'sort') ?? -1, [tableData]);
2296
+ const [sortState, setSortState] = React__namespace["default"].useState({
2297
+ index: defaultSortIndex,
2298
+ direction: 'ascending',
2299
+ });
2300
+ const mappedHeaders = React__namespace["default"].useMemo(() => tableData?.headers.map((header, index) => {
2301
+ const { type } = header;
2302
+ switch (type) {
2303
+ case 'sort': {
2304
+ return {
2305
+ ...header,
2306
+ content: {
2307
+ ...header.content,
2308
+ onSort: () => {
2309
+ setSortState({
2310
+ index,
2311
+ direction: sortState.index === index
2312
+ ? sortState.direction === 'ascending'
2313
+ ? 'descending'
2314
+ : 'ascending'
2315
+ : 'ascending',
2316
+ });
2317
+ },
2318
+ sortDirection: sortState.index === index ? sortState.direction : undefined,
2319
+ },
2320
+ };
2321
+ }
2322
+ case 'checkbox':
2323
+ case 'text':
2324
+ default: {
2325
+ return header;
2326
+ }
2173
2327
  }
2174
- },
2175
- loadingIndicatorLabel: 'Loading',
2176
- overwriteWarningMessage: 'Copied files will overwrite existing files at selected destination.',
2177
- searchPlaceholder: 'Search for folders',
2178
- getActionCompleteMessage: (data) => {
2179
- const { counts } = data ?? {};
2180
- const { COMPLETE, FAILED, TOTAL } = counts ?? {};
2181
- if (COMPLETE === TOTAL) {
2182
- return {
2183
- content: 'All files copied.',
2184
- type: 'success',
2185
- };
2328
+ }), [sortState, tableData]);
2329
+ const sortedRows = React__namespace["default"].useMemo(() => {
2330
+ // Early return if there is no table data
2331
+ if (!tableData) {
2332
+ return;
2186
2333
  }
2187
- if (FAILED === TOTAL) {
2188
- return { content: 'All files failed to copy.', type: 'error' };
2334
+ // Return rows as is if there are no sortable columns
2335
+ if (sortState.index < 0) {
2336
+ return tableData.rows;
2189
2337
  }
2190
- return {
2191
- content: `${COMPLETE} files copied, ${FAILED} files failed to copy.`,
2192
- type: 'error',
2338
+ const { index, direction } = sortState;
2339
+ const groupedRows = {
2340
+ button: [],
2341
+ checkbox: [],
2342
+ date: [],
2343
+ number: [],
2344
+ text: [],
2193
2345
  };
2194
- },
2195
- searchSubmitLabel: 'Submit',
2196
- searchClearLabel: 'Clear search',
2346
+ tableData.rows.forEach((row) => {
2347
+ const { type } = row.content[index];
2348
+ groupedRows[type].push(row);
2349
+ });
2350
+ const groupOrder = direction === 'ascending' ? GROUP_ORDER : [...GROUP_ORDER].reverse();
2351
+ return groupOrder
2352
+ .map((groupType) => {
2353
+ if (UNSORTABLE_GROUPS.includes(groupType)) {
2354
+ return groupedRows[groupType];
2355
+ }
2356
+ return groupedRows[groupType].sort((rowA, rowB) => {
2357
+ switch (groupType) {
2358
+ case 'button': {
2359
+ return compareButtonData(rowA.content[index], rowB.content[index], direction);
2360
+ }
2361
+ case 'date': {
2362
+ return compareDateData(rowA.content[index], rowB.content[index], direction);
2363
+ }
2364
+ case 'number': {
2365
+ return compareNumberData(rowA.content[index], rowB.content[index], direction);
2366
+ }
2367
+ case 'text':
2368
+ default: {
2369
+ return compareTextData(rowA.content[index], rowB.content[index], direction);
2370
+ }
2371
+ }
2372
+ });
2373
+ })
2374
+ .flat();
2375
+ }, [sortState, tableData]);
2376
+ return {
2377
+ headers: mappedHeaders ?? [],
2378
+ rows: sortedRows ?? [],
2379
+ };
2197
2380
  };
2198
2381
 
2199
- const DEFAULT_DELETE_VIEW_DISPLAY_TEXT = {
2200
- ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2201
- title: 'Delete',
2202
- actionStartLabel: 'Delete',
2203
- getActionCompleteMessage: (data) => {
2204
- const { counts } = data ?? {};
2205
- const { COMPLETE, FAILED, TOTAL } = counts ?? {};
2206
- if (COMPLETE === TOTAL) {
2207
- return { content: 'All files deleted.', type: 'success' };
2208
- }
2209
- if (FAILED === TOTAL) {
2210
- return { content: 'All files failed to delete.', type: 'error' };
2211
- }
2212
- return {
2213
- content: `${COMPLETE} files deleted, ${FAILED} files failed to delete.`,
2214
- type: 'error',
2215
- };
2216
- },
2382
+ const DataTableControl = () => {
2383
+ const props = useDataTable();
2384
+ const Resolved = useResolvedComposable(DataTable, 'DataTable');
2385
+ return React__namespace["default"].createElement(Resolved, { ...props });
2217
2386
  };
2218
2387
 
2219
- const DEFAULT_ERROR_MESSAGE$1 = 'There was an error loading items.';
2220
- const DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT = {
2221
- ...DEFAULT_LIST_VIEW_DISPLAY_TEXT,
2222
- getListItemsResultMessage: (data) => {
2223
- const { items, hasExhaustedSearch, hasError = false, message, isLoading, } = data ?? {};
2224
- if (isLoading) {
2225
- return undefined;
2226
- }
2227
- if (hasError) {
2228
- return {
2229
- type: 'error',
2230
- content: message ?? DEFAULT_ERROR_MESSAGE$1,
2231
- };
2232
- }
2233
- if (!items?.length && hasExhaustedSearch) {
2234
- return {
2235
- type: 'info',
2236
- content: `No results found in the first 10,000 items.`,
2237
- };
2238
- }
2239
- if (!items?.length) {
2240
- return {
2241
- type: 'info',
2242
- content: 'No files.',
2243
- };
2244
- }
2245
- if (hasExhaustedSearch) {
2246
- return {
2247
- type: 'info',
2248
- content: `Showing results for up to the first 10,000 items.`,
2249
- };
2250
- }
2251
- // TODO: add more cases as needed
2252
- return undefined;
2253
- },
2254
- searchSubfoldersToggleLabel: 'Include subfolders',
2255
- searchPlaceholder: 'Search current folder',
2256
- tableColumnLastModifiedHeader: 'Last modified',
2257
- tableColumnNameHeader: 'Name',
2258
- tableColumnSizeHeader: 'Size',
2259
- tableColumnTypeHeader: 'Type',
2260
- selectFileLabel: 'Select file',
2261
- selectAllFilesLabel: 'Select all files',
2262
- getActionListItemLabel: (key = '') => {
2263
- switch (key) {
2264
- case 'Copy':
2265
- return 'Copy';
2266
- case 'Delete':
2267
- return 'Delete';
2268
- case 'Create folder':
2269
- return 'Create folder';
2270
- case 'Upload':
2271
- return 'Upload';
2272
- default:
2273
- return key;
2274
- }
2275
- },
2276
- getTitle: (location) => {
2277
- const { current, key } = location;
2278
- const { bucket = '' } = current ?? {};
2279
- return key || bucket;
2280
- },
2388
+ /**
2389
+ * This hook, not to be confused with the useDropZone vended from @aws-amplify/ui-react-core, is only intended for use
2390
+ * with its corresponding DropZone control.
2391
+ */
2392
+ const useDropZone = () => {
2393
+ const { onDropFiles } = useControlsContext();
2394
+ return { onDropFiles };
2395
+ };
2396
+
2397
+ const DropZoneControl = ({ children, }) => {
2398
+ const props = useDropZone();
2399
+ const Resolved = useResolvedComposable(DropZone, 'DropZone');
2400
+ return React__namespace["default"].createElement(Resolved, { ...props }, children);
2281
2401
  };
2282
2402
 
2283
- const DEFAULT_ERROR_MESSAGE = 'There was an error loading locations.';
2284
- const DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT = {
2285
- ...DEFAULT_LIST_VIEW_DISPLAY_TEXT,
2286
- title: 'Home',
2287
- searchPlaceholder: 'Filter folders and files',
2288
- getListLocationsResultMessage: (data) => {
2289
- const { isLoading, items, hasExhaustedSearch, hasError = false, message, } = data ?? {};
2290
- if (isLoading) {
2291
- return undefined;
2292
- }
2293
- if (hasError) {
2294
- return {
2295
- type: 'error',
2296
- content: message ?? DEFAULT_ERROR_MESSAGE,
2297
- };
2298
- }
2299
- if (items?.length === 0 && !hasExhaustedSearch) {
2300
- return {
2301
- type: 'info',
2302
- content: 'No folders or files.',
2303
- };
2304
- }
2305
- if (hasExhaustedSearch) {
2306
- return {
2307
- type: 'info',
2308
- content: `Showing results for up to the first 10,000 items.`,
2309
- };
2310
- }
2311
- // TODO: add more cases as needed
2312
- return undefined;
2313
- },
2314
- getPermissionName: (permissions) => {
2315
- let text = '';
2316
- if (permissions.includes('get') || permissions.includes('list')) {
2317
- text = 'Read';
2318
- }
2319
- if (permissions.includes('write') || permissions.includes('delete')) {
2320
- text = text ? 'Read/Write' : 'Write';
2321
- }
2322
- if (!text) {
2323
- text = permissions.join('/');
2324
- }
2325
- return text;
2326
- },
2327
- getDownloadLabel: (fileName) => `Download ${fileName}`,
2328
- tableColumnBucketHeader: 'Bucket',
2329
- tableColumnFolderHeader: 'Folder',
2330
- tableColumnPermissionsHeader: 'Permissions',
2331
- tableColumnActionsHeader: 'Actions',
2403
+ const useOverwriteToggle = () => {
2404
+ const { data: { isOverwritingEnabled, isOverwriteToggleDisabled, overwriteToggleLabel, }, onToggleOverwrite, } = useControlsContext();
2405
+ return {
2406
+ isDisabled: isOverwriteToggleDisabled,
2407
+ isOverwritingEnabled,
2408
+ label: overwriteToggleLabel,
2409
+ onToggle: onToggleOverwrite,
2410
+ };
2332
2411
  };
2333
2412
 
2334
- const DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT = {
2335
- ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2336
- title: 'Upload',
2337
- actionStartLabel: 'Upload',
2338
- addFilesLabel: 'Add files',
2339
- addFolderLabel: 'Add folder',
2340
- getActionCompleteMessage: (data) => {
2341
- const { counts } = data ?? {};
2342
- const { COMPLETE, FAILED, OVERWRITE_PREVENTED, CANCELED, TOTAL } = counts ?? {};
2343
- const hasPreventedOverwrite = !!OVERWRITE_PREVENTED;
2344
- const hasFailure = !!FAILED;
2345
- const hasSuccess = !!COMPLETE;
2346
- const hasCanceled = !!CANCELED;
2347
- const type = hasFailure
2348
- ? 'error'
2349
- : hasPreventedOverwrite || hasCanceled
2350
- ? 'warning'
2351
- : 'success';
2352
- const preventedOverwriteMessage = hasPreventedOverwrite
2353
- ? [
2354
- 'Overwrite prevented for',
2355
- OVERWRITE_PREVENTED === TOTAL ? 'all' : String(OVERWRITE_PREVENTED),
2356
- OVERWRITE_PREVENTED > 1 || OVERWRITE_PREVENTED === TOTAL
2357
- ? `files`
2358
- : 'file',
2359
- ].join(' ')
2360
- : undefined;
2361
- const canceledMessage = hasCanceled
2362
- ? [
2363
- CANCELED === TOTAL ? 'All' : String(CANCELED),
2364
- CANCELED > 1 || CANCELED === TOTAL ? `uploads` : 'upload',
2365
- 'canceled',
2366
- ].join(' ')
2367
- : undefined;
2368
- const failedMessage = hasFailure
2369
- ? [
2370
- FAILED === TOTAL ? 'All' : String(FAILED),
2371
- FAILED > 1 || FAILED === TOTAL ? `files` : 'file',
2372
- 'failed to upload',
2373
- ].join(' ')
2374
- : undefined;
2375
- const completedMessage = hasSuccess
2376
- ? [
2377
- COMPLETE === TOTAL ? 'All' : String(COMPLETE),
2378
- COMPLETE > 1 || COMPLETE === TOTAL ? `files` : 'file',
2379
- 'uploaded',
2380
- ].join(' ')
2381
- : undefined;
2382
- const messages = [
2383
- preventedOverwriteMessage,
2384
- failedMessage,
2385
- canceledMessage,
2386
- completedMessage,
2387
- ].filter(Boolean);
2388
- if (messages.length > 0) {
2389
- return {
2390
- content: messages.join(', ') + '.',
2391
- type,
2392
- };
2393
- }
2394
- return { content: 'All files uploaded.', type };
2395
- },
2396
- getFilesValidationMessage: (data) => {
2397
- if (!data?.invalidFiles) {
2398
- return undefined;
2399
- }
2400
- const tooBigFileNames = data.invalidFiles
2401
- .filter(({ file }) => isFileTooBig(file))
2402
- .map(({ file }) => file.name)
2403
- .join(', ');
2404
- if (tooBigFileNames) {
2405
- return {
2406
- content: `Files larger than 160GB cannot be added to the upload queue: ${tooBigFileNames}`,
2407
- type: 'warning',
2408
- };
2409
- }
2410
- return undefined;
2411
- },
2412
- statusDisplayOverwritePreventedLabel: 'Overwrite prevented',
2413
- overwriteToggleLabel: 'Overwrite existing files',
2413
+ const OverwriteToggleControl = () => {
2414
+ const props = useOverwriteToggle();
2415
+ const Resolved = useResolvedComposable(OverwriteToggle$1, 'OverwriteToggle');
2416
+ return React__namespace["default"].createElement(Resolved, { ...props });
2414
2417
  };
2415
2418
 
2416
- const DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT = {
2417
- CopyView: DEFAULT_COPY_VIEW_DISPLAY_TEXT,
2418
- CreateFolderView: DEFAULT_CREATE_FOLDER_VIEW_DISPLAY_TEXT,
2419
- DeleteView: DEFAULT_DELETE_VIEW_DISPLAY_TEXT,
2420
- LocationDetailView: DEFAULT_LOCATION_DETAIL_VIEW_DISPLAY_TEXT,
2421
- LocationsView: DEFAULT_LOCATIONS_VIEW_DISPLAY_TEXT,
2422
- UploadView: DEFAULT_UPLOAD_VIEW_DISPLAY_TEXT,
2419
+ const useMessage = () => {
2420
+ const { data: { message = {} }, } = useControlsContext();
2421
+ return message;
2423
2422
  };
2424
2423
 
2425
- const { DisplayTextContext, useDisplayText } = uiReactCore.createContextUtilities({
2426
- contextName: 'DisplayText',
2427
- errorMessage: '`useDisplayText` must be called inside `DisplayTextProvider`',
2428
- });
2429
- function resolveDisplayText(displayText) {
2430
- if (!displayText)
2431
- return DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT;
2432
- // override
2433
- const { CopyView, CreateFolderView, DeleteView, LocationDetailView, LocationsView, UploadView, } = displayText;
2424
+ const MessageControl = () => {
2425
+ const props = useMessage();
2426
+ const Resolved = useResolvedComposable(Message, 'Message');
2427
+ return React__namespace["default"].createElement(Resolved, { ...props });
2428
+ };
2429
+
2430
+ const useStatusDisplay = () => {
2431
+ const { data } = useControlsContext();
2432
+ const { statusCounts, statusDisplayCanceledLabel, statusDisplayCompletedLabel, statusDisplayFailedLabel, statusDisplayQueuedLabel, } = data;
2433
+ if (!statusCounts?.TOTAL) {
2434
+ return { statuses: [], total: 0 };
2435
+ }
2436
+ const statuses = [
2437
+ { name: statusDisplayCompletedLabel ?? '', count: statusCounts.COMPLETE },
2438
+ { name: statusDisplayFailedLabel ?? '', count: statusCounts.FAILED },
2439
+ { name: statusDisplayCanceledLabel ?? '', count: statusCounts.CANCELED },
2440
+ { name: statusDisplayQueuedLabel ?? '', count: statusCounts.QUEUED },
2441
+ ];
2442
+ return { statuses, total: statusCounts.TOTAL };
2443
+ };
2444
+
2445
+ const StatusDisplayControl = () => {
2446
+ const props = useStatusDisplay();
2447
+ const Resolved = useResolvedComposable(StatusDisplay$1, 'StatusDisplay');
2448
+ return React__namespace["default"].createElement(Resolved, { ...props });
2449
+ };
2450
+
2451
+ const useTitle = () => {
2452
+ const { data } = useControlsContext();
2434
2453
  return {
2435
- CopyView: { ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.CopyView, ...CopyView },
2436
- CreateFolderView: {
2437
- ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.CreateFolderView,
2438
- ...CreateFolderView,
2439
- },
2440
- DeleteView: {
2441
- ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.DeleteView,
2442
- ...DeleteView,
2443
- },
2444
- LocationDetailView: {
2445
- ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.LocationDetailView,
2446
- ...LocationDetailView,
2447
- },
2448
- LocationsView: {
2449
- ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.LocationsView,
2450
- ...LocationsView,
2451
- },
2452
- UploadView: {
2453
- ...DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT.UploadView,
2454
- ...UploadView,
2455
- },
2454
+ title: data?.title,
2456
2455
  };
2457
- }
2458
- function DisplayTextProvider({ children, displayText: _override, }) {
2459
- // do deep merge here of default and override here
2460
- const resolvedDisplayText = React__namespace["default"].useMemo(() => resolveDisplayText(_override), [_override]);
2461
- return (React__namespace["default"].createElement(DisplayTextContext.Provider, { value: resolvedDisplayText }, children));
2462
- }
2456
+ };
2457
+
2458
+ const TitleControl = () => {
2459
+ const props = useTitle();
2460
+ const Resolved = useResolvedComposable(Title$1, 'Title');
2461
+ return React__namespace["default"].createElement(Resolved, { ...props });
2462
+ };
2463
2463
 
2464
2464
  const getActionIcon = (status) => {
2465
2465
  switch (status) {
@@ -4765,6 +4765,23 @@ function useViews() {
4765
4765
  };
4766
4766
  }
4767
4767
 
4768
+ const DEFAULT_VIEW_HOOKS = {
4769
+ Copy: useCopyView,
4770
+ CreateFolder: useCreateFolderView,
4771
+ Delete: useDeleteView,
4772
+ LocationDetail: useLocationDetailView,
4773
+ Locations: useLocationsView,
4774
+ Upload: useUploadView,
4775
+ };
4776
+ const isUseViewType = (value) => !!DEFAULT_VIEW_HOOKS?.[value];
4777
+ // @ts-expect-error
4778
+ const useView = (type) => {
4779
+ if (!isUseViewType(type)) {
4780
+ throw new Error(`Value of \`${type}\` cannot be used to index \`useView\``);
4781
+ }
4782
+ return DEFAULT_VIEW_HOOKS[type]();
4783
+ };
4784
+
4768
4785
  /**
4769
4786
  * Handles default `StorageBrowser` behavior:
4770
4787
  * - render `LocationsView` on init
@@ -4785,23 +4802,6 @@ function StorageBrowserDefault() {
4785
4802
  return React__namespace["default"].createElement(LocationsView, null);
4786
4803
  }
4787
4804
 
4788
- const USE_VIEW_HOOKS = {
4789
- Copy: useCopyView,
4790
- CreateFolder: useCreateFolderView,
4791
- Delete: useDeleteView,
4792
- LocationDetail: useLocationDetailView,
4793
- Locations: useLocationsView,
4794
- Upload: useUploadView,
4795
- };
4796
- const isUseViewType = (value) => !!USE_VIEW_HOOKS?.[value];
4797
- // @ts-expect-error
4798
- const useView = (type) => {
4799
- if (!isUseViewType(type)) {
4800
- throw new Error(`Value of \`${type}\` cannot be used to index \`useView\``);
4801
- }
4802
- return USE_VIEW_HOOKS[type]();
4803
- };
4804
-
4805
4805
  function createStorageBrowser(input) {
4806
4806
  assertRegisterAuthListener(input.config.registerAuthListener);
4807
4807
  const { accountId, customEndpoint, registerAuthListener, getLocationCredentials, region, } = input.config;