box-ui-elements 23.4.0-beta.35 → 23.4.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/explorer.css +1 -1
  2. package/dist/explorer.js +1 -1
  3. package/dist/openwith.js +1 -1
  4. package/dist/picker.css +1 -1
  5. package/dist/picker.js +1 -1
  6. package/dist/preview.js +1 -1
  7. package/dist/sharing.js +1 -1
  8. package/dist/sidebar.js +1 -1
  9. package/dist/uploader.js +1 -1
  10. package/es/api/APIFactory.js +4 -2
  11. package/es/api/APIFactory.js.flow +4 -2
  12. package/es/api/APIFactory.js.map +1 -1
  13. package/es/elements/common/messages.js +8 -0
  14. package/es/elements/common/messages.js.flow +16 -0
  15. package/es/elements/common/messages.js.map +1 -1
  16. package/es/elements/common/sub-header/SubHeader.js +14 -1
  17. package/es/elements/common/sub-header/SubHeader.js.map +1 -1
  18. package/es/elements/common/sub-header/SubHeaderLeftV2.js +63 -0
  19. package/es/elements/common/sub-header/SubHeaderLeftV2.js.map +1 -0
  20. package/es/elements/common/sub-header/SubHeaderLeftV2.scss +3 -0
  21. package/es/elements/content-explorer/ContentExplorer.js +60 -3
  22. package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
  23. package/es/src/elements/common/sub-header/SubHeader.d.ts +5 -1
  24. package/es/src/elements/common/sub-header/SubHeaderLeftV2.d.ts +13 -0
  25. package/es/src/elements/common/sub-header/__tests__/SubHeaderLeftV2.test.d.ts +1 -0
  26. package/es/src/elements/content-explorer/ContentExplorer.d.ts +12 -0
  27. package/i18n/bn-IN.js +2 -0
  28. package/i18n/da-DK.js +2 -0
  29. package/i18n/de-DE.js +2 -0
  30. package/i18n/en-AU.js +2 -0
  31. package/i18n/en-CA.js +2 -0
  32. package/i18n/en-GB.js +2 -0
  33. package/i18n/en-US.js +2 -0
  34. package/i18n/en-US.properties +4 -0
  35. package/i18n/en-x-pseudo.js +2 -0
  36. package/i18n/es-419.js +2 -0
  37. package/i18n/es-ES.js +2 -0
  38. package/i18n/fi-FI.js +2 -0
  39. package/i18n/fr-CA.js +2 -0
  40. package/i18n/fr-FR.js +2 -0
  41. package/i18n/hi-IN.js +2 -0
  42. package/i18n/it-IT.js +2 -0
  43. package/i18n/ja-JP.js +6 -4
  44. package/i18n/ja-JP.properties +4 -4
  45. package/i18n/ko-KR.js +2 -0
  46. package/i18n/nb-NO.js +2 -0
  47. package/i18n/nl-NL.js +2 -0
  48. package/i18n/pl-PL.js +2 -0
  49. package/i18n/pt-BR.js +2 -0
  50. package/i18n/ru-RU.js +2 -0
  51. package/i18n/sv-SE.js +2 -0
  52. package/i18n/tr-TR.js +2 -0
  53. package/i18n/zh-CN.js +2 -0
  54. package/i18n/zh-TW.js +2 -0
  55. package/package.json +1 -1
  56. package/src/api/APIFactory.js +4 -2
  57. package/src/elements/common/messages.js +16 -0
  58. package/src/elements/common/sub-header/SubHeader.tsx +24 -1
  59. package/src/elements/common/sub-header/SubHeaderLeftV2.scss +3 -0
  60. package/src/elements/common/sub-header/SubHeaderLeftV2.tsx +73 -0
  61. package/src/elements/common/sub-header/__tests__/SubHeaderLeftV2.test.tsx +109 -0
  62. package/src/elements/content-explorer/ContentExplorer.tsx +57 -2
@@ -1,7 +1,11 @@
1
1
  import * as React from 'react';
2
2
  import noop from 'lodash/noop';
3
+ import classNames from 'classnames';
3
4
  import { PageHeader } from '@box/blueprint-web';
5
+ import type { Selection } from 'react-aria-components';
6
+
4
7
  import SubHeaderLeft from './SubHeaderLeft';
8
+ import SubHeaderLeftV2 from './SubHeaderLeftV2';
5
9
  import SubHeaderRight from './SubHeaderRight';
6
10
  import type { ViewMode } from '../flowTypes';
7
11
  import type { View, Collection } from '../../../common/types/core';
@@ -19,6 +23,7 @@ export interface SubHeaderProps {
19
23
  gridMinColumns?: number;
20
24
  isSmall: boolean;
21
25
  maxGridColumnCountForWidth?: number;
26
+ onClearSelectedItemIds: () => void;
22
27
  onCreate: () => void;
23
28
  onGridViewSliderChange?: (newSliderValue: number) => void;
24
29
  onItemClick: (id: string | null, triggerNavigationEvent: boolean | null) => void;
@@ -28,6 +33,8 @@ export interface SubHeaderProps {
28
33
  portalElement?: HTMLElement;
29
34
  rootId: string;
30
35
  rootName?: string;
36
+ selectedItemIds: Selection;
37
+ title?: string;
31
38
  view: View;
32
39
  viewMode?: ViewMode;
33
40
  }
@@ -42,6 +49,7 @@ const SubHeader = ({
42
49
  maxGridColumnCountForWidth = 0,
43
50
  onGridViewSliderChange = noop,
44
51
  isSmall,
52
+ onClearSelectedItemIds,
45
53
  onCreate,
46
54
  onItemClick,
47
55
  onSortChange,
@@ -50,6 +58,8 @@ const SubHeader = ({
50
58
  portalElement,
51
59
  rootId,
52
60
  rootName,
61
+ selectedItemIds,
62
+ title,
53
63
  view,
54
64
  viewMode = VIEW_MODE_LIST,
55
65
  }: SubHeaderProps) => {
@@ -60,7 +70,11 @@ const SubHeader = ({
60
70
  }
61
71
 
62
72
  return (
63
- <PageHeader.Root className="be-sub-header" data-testid="be-sub-header" variant="inline">
73
+ <PageHeader.Root
74
+ className={classNames({ 'be-sub-header': !isMetadataViewV2Feature })}
75
+ data-testid="be-sub-header"
76
+ variant="inline"
77
+ >
64
78
  <PageHeader.StartElements>
65
79
  {view !== VIEW_METADATA && !isMetadataViewV2Feature && (
66
80
  <SubHeaderLeft
@@ -73,6 +87,15 @@ const SubHeader = ({
73
87
  view={view}
74
88
  />
75
89
  )}
90
+ {isMetadataViewV2Feature && (
91
+ <SubHeaderLeftV2
92
+ currentCollection={currentCollection}
93
+ onClearSelectedItemIds={onClearSelectedItemIds}
94
+ rootName={rootName}
95
+ selectedItemIds={selectedItemIds}
96
+ title={title}
97
+ />
98
+ )}
76
99
  </PageHeader.StartElements>
77
100
  <PageHeader.EndElements>
78
101
  <SubHeaderRight
@@ -0,0 +1,3 @@
1
+ .be-SubHeaderLeftV2--selection {
2
+ gap: var(--space-3);
3
+ }
@@ -0,0 +1,73 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import { XMark } from '@box/blueprint-web-assets/icons/Fill/index';
4
+ import { IconButton, PageHeader, Text } from '@box/blueprint-web';
5
+ import type { Selection } from 'react-aria-components';
6
+ import type { Collection } from '../../../common/types/core';
7
+ import messages from '../messages';
8
+
9
+ import './SubHeaderLeftV2.scss';
10
+
11
+ export interface SubHeaderLeftV2Props {
12
+ currentCollection: Collection;
13
+ onClearSelectedItemIds?: () => void;
14
+ rootName?: string;
15
+ selectedItemIds: Selection;
16
+ title?: string;
17
+ }
18
+
19
+ const SubHeaderLeftV2 = (props: SubHeaderLeftV2Props) => {
20
+ const { currentCollection, onClearSelectedItemIds, rootName, selectedItemIds, title } = props;
21
+ const { formatMessage } = useIntl();
22
+
23
+ // Generate selected item text based on selected keys
24
+ const selectedItemText: string = useMemo(() => {
25
+ const selectedCount = selectedItemIds === 'all' ? currentCollection.items.length : selectedItemIds.size;
26
+
27
+ if (selectedCount === 0) {
28
+ return '';
29
+ }
30
+
31
+ // Case 1: Single selected item - show item name
32
+ if (selectedCount === 1) {
33
+ const selectedKey =
34
+ selectedItemIds === 'all' ? currentCollection.items[0].id : selectedItemIds.values().next().value;
35
+ const selectedItem = currentCollection.items.find(item => item.id === selectedKey);
36
+ return selectedItem?.name ?? '';
37
+ }
38
+ // Case 2: Multiple selected items - show count
39
+ if (selectedCount > 1) {
40
+ return formatMessage(messages.numFilesSelected, { numSelected: selectedCount });
41
+ }
42
+ return '';
43
+ }, [currentCollection.items, formatMessage, selectedItemIds]);
44
+
45
+ // Case 1 and 2: selected item text with X button
46
+ if (selectedItemText) {
47
+ return (
48
+ <PageHeader.Root className="be-SubHeaderLeftV2--selection" variant="default">
49
+ <PageHeader.Corner>
50
+ <IconButton
51
+ aria-label={formatMessage(messages.clearSelection)}
52
+ icon={XMark}
53
+ onClick={onClearSelectedItemIds}
54
+ variant="small-utility"
55
+ />
56
+ </PageHeader.Corner>
57
+
58
+ <PageHeader.StartElements>
59
+ <Text as="span">{selectedItemText}</Text>
60
+ </PageHeader.StartElements>
61
+ </PageHeader.Root>
62
+ );
63
+ }
64
+
65
+ // Case 3: No selected items - show title if provided, otherwise show root name
66
+ return (
67
+ <Text as="h1" variant="titleXLarge">
68
+ {title ?? rootName}
69
+ </Text>
70
+ );
71
+ };
72
+
73
+ export default SubHeaderLeftV2;
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import { render, screen } from '../../../../test-utils/testing-library';
3
+ import SubHeaderLeftV2 from '../SubHeaderLeftV2';
4
+ import type { Collection } from '../../../../common/types/core';
5
+ import type { SubHeaderLeftV2Props } from '../SubHeaderLeftV2';
6
+
7
+ const mockCollection: Collection = {
8
+ items: [
9
+ { id: '1', name: 'file1.txt' },
10
+ { id: '2', name: 'file2.txt' },
11
+ { id: '3', name: 'file3.txt' },
12
+ ],
13
+ };
14
+
15
+ const defaultProps: SubHeaderLeftV2Props = {
16
+ currentCollection: mockCollection,
17
+ selectedItemIds: new Set(),
18
+ };
19
+
20
+ const renderComponent = (props: Partial<SubHeaderLeftV2Props> = {}) =>
21
+ render(<SubHeaderLeftV2 {...defaultProps} {...props} />);
22
+
23
+ describe('elements/common/sub-header/SubHeaderLeftV2', () => {
24
+ describe('when no items are selected', () => {
25
+ test('should render title if provided', () => {
26
+ renderComponent({
27
+ rootName: 'Custom Folder',
28
+ title: 'Custom Title',
29
+ selectedItemIds: new Set(),
30
+ });
31
+
32
+ expect(screen.getByText('Custom Title')).toBeInTheDocument();
33
+ });
34
+
35
+ test('should render root name if no title is provided', () => {
36
+ renderComponent({
37
+ rootName: 'Custom Folder',
38
+ title: undefined,
39
+ selectedItemIds: new Set(),
40
+ });
41
+
42
+ expect(screen.getByText('Custom Folder')).toBeInTheDocument();
43
+ });
44
+ });
45
+
46
+ describe('when items are selected', () => {
47
+ test('should render single selected item name', () => {
48
+ renderComponent({
49
+ selectedItemIds: new Set(['1']),
50
+ });
51
+
52
+ expect(screen.getByText('file1.txt')).toBeInTheDocument();
53
+ expect(screen.getByRole('button')).toBeInTheDocument(); // Close button
54
+ });
55
+
56
+ test('should render multiple selected items count', () => {
57
+ renderComponent({
58
+ selectedItemIds: new Set(['1', '2']),
59
+ });
60
+
61
+ expect(screen.getByText('2 files selected')).toBeInTheDocument();
62
+ expect(screen.getByRole('button')).toBeInTheDocument(); // Close button
63
+ });
64
+
65
+ test('should render all items selected count', () => {
66
+ renderComponent({
67
+ selectedItemIds: 'all',
68
+ });
69
+
70
+ expect(screen.getByText('3 files selected')).toBeInTheDocument();
71
+ expect(screen.getByRole('button')).toBeInTheDocument(); // Close button
72
+ });
73
+
74
+ test('should call onClearSelectedItemIds when close button is clicked', () => {
75
+ const mockOnClearSelectedItemIds = jest.fn();
76
+
77
+ renderComponent({
78
+ selectedItemIds: new Set(['1']),
79
+ onClearSelectedItemIds: mockOnClearSelectedItemIds,
80
+ });
81
+
82
+ const closeButton = screen.getByRole('button');
83
+ closeButton.click();
84
+
85
+ expect(mockOnClearSelectedItemIds).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ test('should handle selected item not found in collection', () => {
89
+ renderComponent({
90
+ selectedItemIds: new Set(['999']), // Non-existent ID
91
+ });
92
+
93
+ // Should not crash and should not render any selected item text
94
+ expect(screen.queryByText('file1.txt')).not.toBeInTheDocument();
95
+ expect(screen.queryByText('file2.txt')).not.toBeInTheDocument();
96
+ expect(screen.queryByText('file3.txt')).not.toBeInTheDocument();
97
+ });
98
+
99
+ test('should handle empty collection with selected items', () => {
100
+ renderComponent({
101
+ currentCollection: { items: [] },
102
+ selectedItemIds: new Set(['1']),
103
+ });
104
+
105
+ // Should not crash and should not render any selected item text
106
+ expect(screen.queryByText('file1.txt')).not.toBeInTheDocument();
107
+ });
108
+ });
109
+ });
@@ -10,6 +10,8 @@ import throttle from 'lodash/throttle';
10
10
  import uniqueid from 'lodash/uniqueId';
11
11
  import { TooltipProvider } from '@box/blueprint-web';
12
12
  import { AxiosRequestConfig, AxiosResponse } from 'axios';
13
+ import type { Selection } from 'react-aria-components';
14
+
13
15
  import CreateFolderDialog from '../common/create-folder-dialog';
14
16
  import UploadDialog from '../common/upload-dialog';
15
17
  import Header from '../common/header';
@@ -151,6 +153,7 @@ export interface ContentExplorerProps {
151
153
  staticHost?: string;
152
154
  staticPath?: string;
153
155
  theme?: Theme;
156
+ title?: string;
154
157
  token: Token;
155
158
  uploadHost?: string;
156
159
  }
@@ -175,6 +178,7 @@ type State = {
175
178
  rootName: string;
176
179
  searchQuery: string;
177
180
  selected?: BoxItem;
181
+ selectedItemIds: Selection;
178
182
  sortBy: SortBy | string;
179
183
  sortDirection: SortDirection;
180
184
  view: View;
@@ -297,6 +301,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
297
301
  markers: [],
298
302
  metadataTemplate: {},
299
303
  rootName: '',
304
+ selectedItemIds: new Set(),
300
305
  searchQuery: '',
301
306
  sortBy,
302
307
  sortDirection,
@@ -333,7 +338,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
333
338
  * @return {void}
334
339
  */
335
340
  componentDidMount() {
336
- const { currentFolderId, defaultView }: ContentExplorerProps = this.props;
341
+ const { currentFolderId, defaultView, metadataQuery }: ContentExplorerProps = this.props;
337
342
  this.rootElement = document.getElementById(this.id) as HTMLElement;
338
343
  this.appElement = this.rootElement.firstElementChild as HTMLElement;
339
344
 
@@ -343,6 +348,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
343
348
  break;
344
349
  case DEFAULT_VIEW_METADATA:
345
350
  this.showMetadataQueryResults();
351
+ this.fetchFolderName(metadataQuery?.ancestor_folder_id);
346
352
  break;
347
353
  default:
348
354
  this.fetchFolder(currentFolderId);
@@ -1524,6 +1530,25 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1524
1530
  return maxWidthColumns;
1525
1531
  };
1526
1532
 
1533
+ getMetadataViewProps = (): ContentExplorerProps['metadataViewProps'] => {
1534
+ const { metadataViewProps } = this.props;
1535
+ const { tableProps } = metadataViewProps ?? {};
1536
+ const { onSelectionChange } = tableProps ?? {};
1537
+ const { selectedItemIds } = this.state;
1538
+
1539
+ return {
1540
+ ...metadataViewProps,
1541
+ tableProps: {
1542
+ ...tableProps,
1543
+ selectedKeys: selectedItemIds,
1544
+ onSelectionChange: (ids: Selection) => {
1545
+ onSelectionChange?.(ids);
1546
+ this.setState({ selectedItemIds: ids });
1547
+ },
1548
+ },
1549
+ };
1550
+ };
1551
+
1527
1552
  /**
1528
1553
  * Change the current view mode
1529
1554
  *
@@ -1599,6 +1624,31 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1599
1624
  });
1600
1625
  };
1601
1626
 
1627
+ clearSelectedItemIds = () => {
1628
+ this.setState({ selectedItemIds: new Set() });
1629
+ };
1630
+
1631
+ /**
1632
+ * Fetches the folder name and stores it in state rootName if successful
1633
+ *
1634
+ * @private
1635
+ * @return {void}
1636
+ */
1637
+ fetchFolderName = (folderId?: string) => {
1638
+ if (!folderId) {
1639
+ return;
1640
+ }
1641
+
1642
+ this.api.getFolderAPI(false).getFolderFields(
1643
+ folderId,
1644
+ ({ name }) => {
1645
+ this.setState({ rootName: name });
1646
+ },
1647
+ this.errorCallback,
1648
+ { fields: [FIELD_NAME] },
1649
+ );
1650
+ };
1651
+
1602
1652
  /**
1603
1653
  * Renders the file picker
1604
1654
  *
@@ -1632,7 +1682,6 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1632
1682
  measureRef,
1633
1683
  messages,
1634
1684
  fieldsToShow,
1635
- metadataViewProps,
1636
1685
  onDownload,
1637
1686
  onPreview,
1638
1687
  onUpload,
@@ -1645,6 +1694,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1645
1694
  staticPath,
1646
1695
  previewLibraryVersion,
1647
1696
  theme,
1697
+ title,
1648
1698
  token,
1649
1699
  uploadHost,
1650
1700
  }: ContentExplorerProps = this.props;
@@ -1683,6 +1733,8 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1683
1733
  const hasNextMarker: boolean = !!markers[currentPageNumber + 1];
1684
1734
  const hasPreviousMarker: boolean = currentPageNumber === 1 || !!markers[currentPageNumber - 1];
1685
1735
 
1736
+ const metadataViewProps = this.getMetadataViewProps();
1737
+
1686
1738
  /* eslint-disable jsx-a11y/no-static-element-interactions */
1687
1739
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex */
1688
1740
  return (
@@ -1707,12 +1759,15 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1707
1759
  gridMinColumns={GRID_VIEW_MIN_COLUMNS}
1708
1760
  maxGridColumnCountForWidth={maxGridColumnCount}
1709
1761
  onUpload={this.upload}
1762
+ onClearSelectedItemIds={this.clearSelectedItemIds}
1710
1763
  onCreate={this.createFolder}
1711
1764
  onGridViewSliderChange={this.onGridViewSliderChange}
1712
1765
  onItemClick={this.fetchFolder}
1713
1766
  onSortChange={this.sort}
1714
1767
  onViewModeChange={this.changeViewMode}
1715
1768
  portalElement={this.rootElement}
1769
+ selectedItemIds={this.state.selectedItemIds}
1770
+ title={title}
1716
1771
  />
1717
1772
 
1718
1773
  <Content