box-ui-elements 24.0.0-beta.4 → 24.0.0-beta.6

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 (113) 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.js +1 -1
  5. package/dist/preview.js +1 -1
  6. package/dist/sharing.js +1 -1
  7. package/dist/sidebar.js +1 -1
  8. package/dist/uploader.js +1 -1
  9. package/es/api/Metadata.js +98 -13
  10. package/es/api/Metadata.js.flow +110 -12
  11. package/es/api/Metadata.js.map +1 -1
  12. package/es/elements/common/messages.js +16 -0
  13. package/es/elements/common/messages.js.flow +25 -0
  14. package/es/elements/common/messages.js.map +1 -1
  15. package/es/elements/content-explorer/Content.js +5 -2
  16. package/es/elements/content-explorer/Content.js.map +1 -1
  17. package/es/elements/content-explorer/ContentExplorer.js +31 -6
  18. package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
  19. package/es/elements/content-explorer/MetadataQueryAPIHelper.js +164 -10
  20. package/es/elements/content-explorer/MetadataQueryAPIHelper.js.map +1 -1
  21. package/es/elements/content-explorer/MetadataQueryBuilder.js +115 -0
  22. package/es/elements/content-explorer/MetadataQueryBuilder.js.map +1 -0
  23. package/es/elements/content-explorer/MetadataSidePanel.js +40 -14
  24. package/es/elements/content-explorer/MetadataSidePanel.js.map +1 -1
  25. package/es/elements/content-explorer/MetadataViewContainer.js +133 -36
  26. package/es/elements/content-explorer/MetadataViewContainer.js.map +1 -1
  27. package/es/elements/content-explorer/stories/MetadataView.stories.js +3 -25
  28. package/es/elements/content-explorer/stories/MetadataView.stories.js.map +1 -1
  29. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js +65 -29
  30. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js.map +1 -1
  31. package/es/elements/content-explorer/utils.js +140 -12
  32. package/es/elements/content-explorer/utils.js.map +1 -1
  33. package/es/src/elements/common/__mocks__/mockMetadata.d.ts +8 -24
  34. package/es/src/elements/content-explorer/Content.d.ts +4 -3
  35. package/es/src/elements/content-explorer/ContentExplorer.d.ts +19 -6
  36. package/es/src/elements/content-explorer/MetadataQueryAPIHelper.d.ts +22 -3
  37. package/es/src/elements/content-explorer/MetadataQueryBuilder.d.ts +27 -0
  38. package/es/src/elements/content-explorer/MetadataSidePanel.d.ts +6 -3
  39. package/es/src/elements/content-explorer/MetadataViewContainer.d.ts +10 -4
  40. package/es/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.d.ts +1 -0
  41. package/es/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.d.ts +1 -0
  42. package/es/src/elements/content-explorer/utils.d.ts +9 -3
  43. package/i18n/bn-IN.js +4 -0
  44. package/i18n/bn-IN.properties +12 -0
  45. package/i18n/da-DK.js +4 -0
  46. package/i18n/da-DK.properties +12 -0
  47. package/i18n/de-DE.js +5 -1
  48. package/i18n/de-DE.properties +12 -0
  49. package/i18n/en-AU.js +4 -0
  50. package/i18n/en-AU.properties +12 -0
  51. package/i18n/en-CA.js +4 -0
  52. package/i18n/en-CA.properties +12 -0
  53. package/i18n/en-GB.js +4 -0
  54. package/i18n/en-GB.properties +12 -0
  55. package/i18n/en-US.js +4 -0
  56. package/i18n/en-US.properties +8 -0
  57. package/i18n/en-x-pseudo.js +4 -0
  58. package/i18n/es-419.js +5 -1
  59. package/i18n/es-419.properties +12 -0
  60. package/i18n/es-ES.js +5 -1
  61. package/i18n/es-ES.properties +12 -0
  62. package/i18n/fi-FI.js +4 -0
  63. package/i18n/fi-FI.properties +12 -0
  64. package/i18n/fr-CA.js +4 -0
  65. package/i18n/fr-CA.properties +12 -0
  66. package/i18n/fr-FR.js +4 -0
  67. package/i18n/fr-FR.properties +12 -0
  68. package/i18n/hi-IN.js +4 -0
  69. package/i18n/hi-IN.properties +12 -0
  70. package/i18n/it-IT.js +4 -0
  71. package/i18n/it-IT.properties +12 -0
  72. package/i18n/ja-JP.js +6 -2
  73. package/i18n/ja-JP.properties +14 -2
  74. package/i18n/ko-KR.js +4 -0
  75. package/i18n/ko-KR.properties +12 -0
  76. package/i18n/nb-NO.js +4 -0
  77. package/i18n/nb-NO.properties +12 -0
  78. package/i18n/nl-NL.js +4 -0
  79. package/i18n/nl-NL.properties +12 -0
  80. package/i18n/pl-PL.js +4 -0
  81. package/i18n/pl-PL.properties +12 -0
  82. package/i18n/pt-BR.js +4 -0
  83. package/i18n/pt-BR.properties +12 -0
  84. package/i18n/ru-RU.js +5 -1
  85. package/i18n/ru-RU.properties +12 -0
  86. package/i18n/sv-SE.js +4 -0
  87. package/i18n/sv-SE.properties +12 -0
  88. package/i18n/tr-TR.js +5 -1
  89. package/i18n/tr-TR.properties +12 -0
  90. package/i18n/zh-CN.js +4 -0
  91. package/i18n/zh-CN.properties +12 -0
  92. package/i18n/zh-TW.js +4 -0
  93. package/i18n/zh-TW.properties +12 -0
  94. package/package.json +3 -3
  95. package/src/api/Metadata.js +110 -12
  96. package/src/api/__tests__/Metadata.test.js +120 -0
  97. package/src/elements/common/__mocks__/mockMetadata.ts +7 -11
  98. package/src/elements/common/messages.js +25 -0
  99. package/src/elements/content-explorer/Content.tsx +9 -2
  100. package/src/elements/content-explorer/ContentExplorer.tsx +71 -17
  101. package/src/elements/content-explorer/MetadataQueryAPIHelper.ts +199 -8
  102. package/src/elements/content-explorer/MetadataQueryBuilder.ts +159 -0
  103. package/src/elements/content-explorer/MetadataSidePanel.tsx +55 -14
  104. package/src/elements/content-explorer/MetadataViewContainer.tsx +164 -29
  105. package/src/elements/content-explorer/__tests__/Content.test.tsx +1 -0
  106. package/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +38 -7
  107. package/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +428 -12
  108. package/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts +419 -0
  109. package/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx +145 -3
  110. package/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx +413 -9
  111. package/src/elements/content-explorer/stories/MetadataView.stories.tsx +3 -21
  112. package/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +56 -21
  113. package/src/elements/content-explorer/utils.ts +150 -13
@@ -9,7 +9,6 @@ const mockMetadata = {
9
9
  role: ['Business Owner', 'Marketing'],
10
10
  $template: 'templateName',
11
11
  $parent: 'file_1188899160835',
12
- name: 'something',
13
12
  industry: 'Technology',
14
13
  last_contacted_at: '2023-11-16T00:00:00.000Z',
15
14
  $version: 9,
@@ -31,7 +30,6 @@ const mockMetadata = {
31
30
  role: ['Developer'],
32
31
  $template: 'templateName',
33
32
  $parent: 'file_1318276254035',
34
- name: '1',
35
33
  industry: 'Technology',
36
34
  last_contacted_at: '2023-11-01T00:00:00.000Z',
37
35
  $version: 3,
@@ -94,7 +92,6 @@ const mockMetadata = {
94
92
  $scope: 'enterprise_0',
95
93
  $template: 'templateName',
96
94
  $parent: 'file_1812508470016',
97
- name: 'in folder 3 that doesnt have metadata',
98
95
  $version: 0,
99
96
  },
100
97
  },
@@ -154,14 +151,6 @@ const mockSchema = {
154
151
  hidden: false,
155
152
  copyInstanceOnItemCopy: false,
156
153
  fields: [
157
- {
158
- id: '56b6f00e-5db3-4875-a31d-14b20f63c0ea',
159
- type: 'string',
160
- key: 'name',
161
- displayName: 'Name',
162
- hidden: false,
163
- description: 'The customer name',
164
- },
165
154
  {
166
155
  id: '07d3c06c-5db4-4f3f-821e-19219ba70ed3',
167
156
  type: 'date',
@@ -220,6 +209,13 @@ const mockSchema = {
220
209
  },
221
210
  ],
222
211
  },
212
+ {
213
+ id: 'c3f87bb0-44df-4689-aafe-b9ed4aecbb01',
214
+ type: 'float',
215
+ key: 'number',
216
+ displayName: 'Merit Count',
217
+ hidden: false,
218
+ },
223
219
  ],
224
220
  };
225
221
 
@@ -27,6 +27,11 @@ const messages = defineMessages({
27
27
  description: 'Generic error label.',
28
28
  defaultMessage: 'Error',
29
29
  },
30
+ success: {
31
+ id: 'be.success',
32
+ description: 'Generic success label.',
33
+ defaultMessage: 'Success',
34
+ },
30
35
  preview: {
31
36
  id: 'be.preview',
32
37
  description: 'Label for preview action.',
@@ -1105,6 +1110,26 @@ const messages = defineMessages({
1105
1110
  }
1106
1111
  `,
1107
1112
  },
1113
+ multipleValues: {
1114
+ id: 'be.multipleValues',
1115
+ description: 'Display text for field when there are multiple items selected and have different value',
1116
+ defaultMessage: 'Multiple Values',
1117
+ },
1118
+ metadataUpdateErrorNotification: {
1119
+ id: 'be.metadataUpdateErrorNotification',
1120
+ description: 'Text shown in error notification banner',
1121
+ defaultMessage: 'Unable to save changes. Please try again.',
1122
+ },
1123
+ metadataUpdateSuccessNotification: {
1124
+ id: 'be.metadataUpdateSuccessNotification',
1125
+ description: 'Text shown in success notification banner',
1126
+ defaultMessage: `
1127
+ {numSelected, plural,
1128
+ =1 {1 document updated}
1129
+ other {# documents updated}
1130
+ }
1131
+ `,
1132
+ },
1108
1133
  });
1109
1134
 
1110
1135
  export default messages;
@@ -4,7 +4,7 @@ import ItemGrid from '../common/item-grid';
4
4
  import ItemList from '../common/item-list';
5
5
  import ProgressBar from '../common/progress-bar';
6
6
  import MetadataBasedItemList from '../../features/metadata-based-view';
7
- import MetadataViewContainer, { MetadataViewContainerProps } from './MetadataViewContainer';
7
+ import MetadataViewContainer, { ExternalFilterValues, MetadataViewContainerProps } from './MetadataViewContainer';
8
8
  import { isFeatureEnabled, type FeatureConfig } from '../common/feature-checking';
9
9
  import { VIEW_ERROR, VIEW_METADATA, VIEW_MODE_LIST, VIEW_MODE_GRID, VIEW_SELECTED } from '../../constants';
10
10
  import type { ViewMode } from '../common/flowTypes';
@@ -37,7 +37,11 @@ export interface ContentProps extends Required<ItemEventHandlers>, Required<Item
37
37
  isTouch: boolean;
38
38
  itemActions?: ItemAction[];
39
39
  metadataTemplate?: MetadataTemplate;
40
- metadataViewProps?: Omit<MetadataViewContainerProps, 'currentCollection'>;
40
+ metadataViewProps?: Omit<
41
+ MetadataViewContainerProps,
42
+ 'hasError' | 'currentCollection' | 'metadataTemplate' | 'onMetadataFilter'
43
+ >;
44
+ onMetadataFilter?: (fields: ExternalFilterValues) => void;
41
45
  onMetadataUpdate: (
42
46
  item: BoxItem,
43
47
  field: string,
@@ -57,6 +61,7 @@ const Content = ({
57
61
  gridColumnCount,
58
62
  metadataTemplate,
59
63
  metadataViewProps,
64
+ onMetadataFilter,
60
65
  onMetadataUpdate,
61
66
  onSortChange,
62
67
  view,
@@ -89,6 +94,8 @@ const Content = ({
89
94
  isLoading={percentLoaded !== 100}
90
95
  hasError={view === VIEW_ERROR}
91
96
  metadataTemplate={metadataTemplate}
97
+ onMetadataFilter={onMetadataFilter}
98
+ onSortChange={onSortChange}
92
99
  {...metadataViewProps}
93
100
  />
94
101
  )}
@@ -8,9 +8,9 @@ import getProp from 'lodash/get';
8
8
  import noop from 'lodash/noop';
9
9
  import throttle from 'lodash/throttle';
10
10
  import uniqueid from 'lodash/uniqueId';
11
- import { TooltipProvider } from '@box/blueprint-web';
12
11
  import { AxiosRequestConfig, AxiosResponse } from 'axios';
13
- import type { Selection } from 'react-aria-components';
12
+ import type { Key, Selection } from 'react-aria-components';
13
+ import type { MetadataTemplateField } from '@box/metadata-editor';
14
14
 
15
15
  import CreateFolderDialog from '../common/create-folder-dialog';
16
16
  import UploadDialog from '../common/upload-dialog';
@@ -77,6 +77,7 @@ import {
77
77
  import type { ViewMode } from '../common/flowTypes';
78
78
  import type { ItemAction } from '../common/item';
79
79
  import type { Theme } from '../common/theming';
80
+ import type { JSONPatchOperations } from '../../common/types/api';
80
81
  import type { MetadataQuery, FieldsToShow } from '../../common/types/metadataQueries';
81
82
  import type { MetadataFieldValue, MetadataTemplate } from '../../common/types/metadata';
82
83
  import type {
@@ -94,13 +95,14 @@ import type {
94
95
  import type { BulkItemAction } from '../common/sub-header/BulkItemActionMenu';
95
96
  import type { ContentPreviewProps } from '../content-preview';
96
97
  import type { ContentUploaderProps } from '../content-uploader';
97
- import type { MetadataViewContainerProps } from './MetadataViewContainer';
98
+ import type { ExternalFilterValues, MetadataViewContainerProps } from './MetadataViewContainer';
98
99
 
99
100
  import '../common/fonts.scss';
100
101
  import '../common/base.scss';
101
102
  import '../common/modal.scss';
102
103
  import './ContentExplorer.scss';
103
104
  import { withBlueprintModernization } from '../common/withBlueprintModernization';
105
+ import Providers from '../common/Providers';
104
106
 
105
107
  const GRID_VIEW_MAX_COLUMNS = 7;
106
108
  const GRID_VIEW_MIN_COLUMNS = 1;
@@ -125,6 +127,7 @@ export interface ContentExplorerProps {
125
127
  defaultView?: DefaultView;
126
128
  features?: FeatureConfig;
127
129
  fieldsToShow?: FieldsToShow;
130
+ hasProviders?: boolean;
128
131
  initialPage?: number;
129
132
  initialPageSize?: number;
130
133
  isLarge?: boolean;
@@ -138,7 +141,10 @@ export interface ContentExplorerProps {
138
141
  measureRef?: (ref: Element | null) => void;
139
142
  messages?: StringMap;
140
143
  metadataQuery?: MetadataQuery;
141
- metadataViewProps?: Omit<MetadataViewContainerProps, 'hasError' | 'currentCollection'>;
144
+ metadataViewProps?: Omit<
145
+ MetadataViewContainerProps,
146
+ 'hasError' | 'currentCollection' | 'metadataTemplate' | 'onMetadataFilter'
147
+ >;
142
148
  onCreate?: (item: BoxItem) => void;
143
149
  onDelete?: (item: BoxItem) => void;
144
150
  onDownload?: (item: BoxItem) => void;
@@ -153,7 +159,7 @@ export interface ContentExplorerProps {
153
159
  rootFolderId?: string;
154
160
  sharedLink?: string;
155
161
  sharedLinkPassword?: string;
156
- sortBy?: SortBy;
162
+ sortBy?: SortBy | Key;
157
163
  sortDirection?: SortDirection;
158
164
  staticHost?: string;
159
165
  staticPath?: string;
@@ -181,6 +187,7 @@ type State = {
181
187
  isUploadModalOpen: boolean;
182
188
  markers: Array<string | null | undefined>;
183
189
  metadataTemplate: MetadataTemplate;
190
+ metadataFilters: ExternalFilterValues;
184
191
  rootName: string;
185
192
  searchQuery: string;
186
193
  selected?: BoxItem;
@@ -211,7 +218,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
211
218
 
212
219
  store: LocalStore = new LocalStore();
213
220
 
214
- metadataQueryAPIHelper: MetadataQueryAPIHelper;
221
+ metadataQueryAPIHelper: MetadataQueryAPIHelper | MetadataQueryAPIHelperV2;
215
222
 
216
223
  static defaultProps = {
217
224
  rootFolderId: DEFAULT_ROOT,
@@ -306,6 +313,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
306
313
  isShareModalOpen: false,
307
314
  isUploadModalOpen: false,
308
315
  markers: [],
316
+ metadataFilters: {},
309
317
  metadataTemplate: {},
310
318
  rootName: '',
311
319
  selectedItemIds: new Set(),
@@ -389,6 +397,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
389
397
  *
390
398
  * @private
391
399
  * @param {Object} metadataQueryCollection - Metadata query response collection
400
+ * @param {Object} metadataTemplate - Metadata template object
392
401
  * @return {void}
393
402
  */
394
403
  showMetadataQueryResultsSuccessCallback = (
@@ -440,7 +449,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
440
449
  */
441
450
  showMetadataQueryResults() {
442
451
  const { features, metadataQuery = {} }: ContentExplorerProps = this.props;
443
- const { currentPageNumber, markers, sortBy, sortDirection }: State = this.state;
452
+ const { currentPageNumber, markers, metadataFilters, sortBy, sortDirection }: State = this.state;
444
453
  const metadataQueryClone = cloneDeep(metadataQuery);
445
454
 
446
455
  if (currentPageNumber === 0) {
@@ -475,6 +484,12 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
475
484
  ];
476
485
 
477
486
  this.metadataQueryAPIHelper = new MetadataQueryAPIHelperV2(this.api);
487
+ this.metadataQueryAPIHelper.fetchMetadataQueryResults(
488
+ metadataQueryClone,
489
+ this.showMetadataQueryResultsSuccessCallback,
490
+ this.errorCallback,
491
+ metadataFilters,
492
+ );
478
493
  } else {
479
494
  metadataQueryClone.order_by = [
480
495
  {
@@ -483,15 +498,46 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
483
498
  },
484
499
  ];
485
500
  this.metadataQueryAPIHelper = new MetadataQueryAPIHelper(this.api);
501
+ this.metadataQueryAPIHelper.fetchMetadataQueryResults(
502
+ metadataQueryClone,
503
+ this.showMetadataQueryResultsSuccessCallback,
504
+ this.errorCallback,
505
+ );
486
506
  }
487
-
488
- this.metadataQueryAPIHelper.fetchMetadataQueryResults(
489
- metadataQueryClone,
490
- this.showMetadataQueryResultsSuccessCallback,
491
- this.errorCallback,
492
- );
493
507
  }
494
508
 
509
+ /**
510
+ * Update selected items' metadata instances based on original and new field values in the metadata instance form
511
+ *
512
+ * @private
513
+ * @return {void}
514
+ */
515
+ updateMetadataV2 = async (
516
+ items: BoxItem[],
517
+ operations: JSONPatchOperations,
518
+ templateOldFields: MetadataTemplateField[],
519
+ templateNewFields: MetadataTemplateField[],
520
+ successCallback: () => void,
521
+ errorCallback: ErrorCallback,
522
+ ) => {
523
+ if (items.length === 1) {
524
+ await this.metadataQueryAPIHelper.updateMetadataWithOperations(
525
+ items[0],
526
+ operations,
527
+ successCallback,
528
+ errorCallback,
529
+ );
530
+ } else {
531
+ await this.metadataQueryAPIHelper.bulkUpdateMetadata(
532
+ items,
533
+ templateOldFields,
534
+ templateNewFields,
535
+ successCallback,
536
+ errorCallback,
537
+ );
538
+ }
539
+ };
540
+
495
541
  /**
496
542
  * Resets the collection so that the loading bar starts showing
497
543
  *
@@ -896,7 +942,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
896
942
  * @param {string} sortDirection - sort direction
897
943
  * @return {void}
898
944
  */
899
- sort = (sortBy: SortBy, sortDirection: SortDirection) => {
945
+ sort = (sortBy: SortBy | Key, sortDirection: SortDirection) => {
900
946
  const {
901
947
  currentCollection: { id },
902
948
  view,
@@ -1691,6 +1737,10 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1691
1737
  this.setState({ isMetadataSidePanelOpen: false });
1692
1738
  };
1693
1739
 
1740
+ filterMetadata = (fields: ExternalFilterValues) => {
1741
+ this.setState({ metadataFilters: fields }, this.refreshCollection);
1742
+ };
1743
+
1694
1744
  /**
1695
1745
  * Renders the file picker
1696
1746
  *
@@ -1716,6 +1766,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1716
1766
  contentUploaderProps,
1717
1767
  defaultView,
1718
1768
  features,
1769
+ hasProviders,
1719
1770
  isMedium,
1720
1771
  isSmall,
1721
1772
  isTouch,
@@ -1785,7 +1836,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1785
1836
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex */
1786
1837
  return (
1787
1838
  <Internationalize language={language} messages={messages}>
1788
- <TooltipProvider container={this.rootElement}>
1839
+ <Providers hasProviders={hasProviders}>
1789
1840
  <div id={this.id} className={styleClassName} ref={measureRef} data-testid="content-explorer">
1790
1841
  <ThemingStyles selector={`#${this.id}`} theme={theme} />
1791
1842
  <div className="be-app-element" onKeyDown={this.onKeyDown} tabIndex={0}>
@@ -1844,6 +1895,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1844
1895
  onItemRename={this.rename}
1845
1896
  onItemSelect={this.select}
1846
1897
  onItemShare={this.share}
1898
+ onMetadataFilter={this.filterMetadata}
1847
1899
  onMetadataUpdate={this.updateMetadata}
1848
1900
  onSortChange={this.sort}
1849
1901
  portalElement={this.rootElement}
@@ -1869,8 +1921,10 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1869
1921
  {isDefaultViewMetadata && isMetadataViewV2Feature && isMetadataSidePanelOpen && (
1870
1922
  <MetadataSidePanel
1871
1923
  currentCollection={currentCollection}
1872
- onClose={this.closeMetadataSidePanel}
1873
1924
  metadataTemplate={metadataTemplate}
1925
+ onClose={this.closeMetadataSidePanel}
1926
+ onUpdate={this.updateMetadataV2}
1927
+ refreshCollection={this.refreshCollection}
1874
1928
  selectedItemIds={selectedItemIds}
1875
1929
  />
1876
1930
  )}
@@ -1966,7 +2020,7 @@ class ContentExplorer extends Component<ContentExplorerProps, State> {
1966
2020
  />
1967
2021
  ) : null}
1968
2022
  </div>
1969
- </TooltipProvider>
2023
+ </Providers>
1970
2024
  </Internationalize>
1971
2025
  );
1972
2026
  /* eslint-enable jsx-a11y/no-static-element-interactions */
@@ -3,8 +3,11 @@ import find from 'lodash/find';
3
3
  import getProp from 'lodash/get';
4
4
  import includes from 'lodash/includes';
5
5
  import isArray from 'lodash/isArray';
6
- import isNil from 'lodash/isNil';
6
+ import type { MetadataTemplateField } from '@box/metadata-editor';
7
+ import type { MetadataFieldType } from '@box/metadata-view';
8
+
7
9
  import API from '../../api';
10
+ import { areFieldValuesEqual, isEmptyValue, isMultiValuesField } from './utils';
8
11
 
9
12
  import {
10
13
  JSON_PATCH_OP_ADD,
@@ -14,7 +17,7 @@ import {
14
17
  METADATA_FIELD_TYPE_ENUM,
15
18
  METADATA_FIELD_TYPE_MULTISELECT,
16
19
  } from '../../common/constants';
17
- import { FIELD_NAME, FIELD_METADATA, FIELD_EXTENSION } from '../../constants';
20
+ import { FIELD_ITEM_NAME, FIELD_METADATA, FIELD_EXTENSION, FIELD_PERMISSIONS } from '../../constants';
18
21
 
19
22
  import type { MetadataQuery as MetadataQueryType, MetadataQueryResponseData } from '../../common/types/metadataQueries';
20
23
  import type {
@@ -26,6 +29,15 @@ import type {
26
29
  } from '../../common/types/metadata';
27
30
  import type { ElementsXhrError, JSONPatchOperations } from '../../common/types/api';
28
31
  import type { Collection, BoxItem } from '../../common/types/core';
32
+ import {
33
+ getMimeTypeFilter,
34
+ getRangeFilter,
35
+ getSelectFilter,
36
+ getStringFilter,
37
+ mergeQueries,
38
+ mergeQueryParams,
39
+ } from './MetadataQueryBuilder';
40
+ import type { ExternalFilterValues } from './MetadataViewContainer';
29
41
 
30
42
  type SuccessCallback = (metadataQueryCollection: Collection, metadataTemplate: MetadataTemplate) => void;
31
43
  type ErrorCallback = (e: ElementsXhrError) => void;
@@ -57,13 +69,18 @@ export default class MetadataQueryAPIHelper {
57
69
  oldValue: MetadataFieldValue | null,
58
70
  newValue: MetadataFieldValue | null,
59
71
  ): JSONPatchOperations => {
72
+ // check if two values are the same, return empty operations if so
73
+ if (areFieldValuesEqual(oldValue, newValue)) {
74
+ return [];
75
+ }
76
+
60
77
  let operation = JSON_PATCH_OP_REPLACE;
61
78
 
62
- if (isNil(oldValue) && newValue) {
79
+ if (isEmptyValue(oldValue) && !isEmptyValue(newValue)) {
63
80
  operation = JSON_PATCH_OP_ADD;
64
81
  }
65
82
 
66
- if (oldValue && isNil(newValue)) {
83
+ if (!isEmptyValue(oldValue) && isEmptyValue(newValue)) {
67
84
  operation = JSON_PATCH_OP_REMOVE;
68
85
  }
69
86
 
@@ -170,6 +187,51 @@ export default class MetadataQueryAPIHelper {
170
187
  return this.api.getMetadataAPI(true).getSchemaByTemplateKey(this.templateKey);
171
188
  };
172
189
 
190
+ /**
191
+ * Generate operations for all fields update in the metadata sidepanel
192
+ *
193
+ * @private
194
+ * @return {JSONPatchOperations}
195
+ */
196
+ generateOperations = (
197
+ item: BoxItem,
198
+ templateOldFields: MetadataTemplateField[],
199
+ templateNewFields: MetadataTemplateField[],
200
+ ): JSONPatchOperations => {
201
+ const { scope, templateKey } = this.metadataTemplate;
202
+ const itemFields = item.metadata[scope][templateKey];
203
+ const operations = templateNewFields.flatMap(newField => {
204
+ let newFieldValue = newField.value;
205
+ const { key, type } = newField;
206
+ // when retrieve value from float type field, it gives a string instead
207
+ if (type === 'float' && newFieldValue !== '') {
208
+ newFieldValue = Number(newFieldValue);
209
+ }
210
+ const oldField = templateOldFields.find(f => f.key === key);
211
+ const oldFieldValue = oldField.value;
212
+
213
+ /*
214
+ Generate operations array based on all the fields' orignal value and the incoming updated value.
215
+
216
+ Edge Case:
217
+ If there are multiple items shared different value for enum or multi-select field, the form will
218
+ return 'Multiple values' as the value. In this case, it needs to generate operation based on the
219
+ actual item's field value.
220
+ */
221
+ const shouldUseItemFieldValue =
222
+ isMultiValuesField(type as MetadataFieldType, oldFieldValue) &&
223
+ !isMultiValuesField(type as MetadataFieldType, newFieldValue);
224
+
225
+ return this.createJSONPatchOperations(
226
+ key,
227
+ shouldUseItemFieldValue ? itemFields[key] : oldFieldValue,
228
+ newFieldValue,
229
+ );
230
+ });
231
+
232
+ return operations;
233
+ };
234
+
173
235
  queryMetadata = (): Promise<MetadataQueryResponseData> => {
174
236
  return new Promise((resolve, reject) => {
175
237
  this.api.getMetadataQueryAPI().queryMetadata(this.metadataQuery, resolve, reject, { forceFetch: true });
@@ -180,8 +242,10 @@ export default class MetadataQueryAPIHelper {
180
242
  metadataQuery: MetadataQueryType,
181
243
  successCallback: SuccessCallback,
182
244
  errorCallback: ErrorCallback,
245
+ fields?: ExternalFilterValues,
183
246
  ): Promise<void> => {
184
- this.metadataQuery = this.verifyQueryFields(metadataQuery);
247
+ this.metadataQuery = this.verifyQueryFields(metadataQuery, fields);
248
+
185
249
  return this.queryMetadata()
186
250
  .then(this.getTemplateSchemaInfo)
187
251
  .then(this.getDataWithTypes)
@@ -205,26 +269,153 @@ export default class MetadataQueryAPIHelper {
205
269
  .updateMetadata(file, this.metadataTemplate, operations, successCallback, errorCallback);
206
270
  };
207
271
 
272
+ updateMetadataWithOperations = (
273
+ item: BoxItem,
274
+ operations: JSONPatchOperations,
275
+ successCallback: () => void,
276
+ errorCallback: ErrorCallback,
277
+ ): Promise<void> => {
278
+ return this.api
279
+ .getMetadataAPI(true)
280
+ .updateMetadata(item, this.metadataTemplate, operations, successCallback, errorCallback);
281
+ };
282
+
283
+ bulkUpdateMetadata = (
284
+ items: BoxItem[],
285
+ templateOldFields: MetadataTemplateField[],
286
+ templateNewFields: MetadataTemplateField[],
287
+ successCallback: () => void,
288
+ errorCallback: ErrorCallback,
289
+ ): Promise<void> => {
290
+ const operations: JSONPatchOperations = [];
291
+ items.forEach(item => {
292
+ const operation = this.generateOperations(item, templateOldFields, templateNewFields);
293
+ operations.push(operation);
294
+ });
295
+ return this.api
296
+ .getMetadataAPI(true)
297
+ .bulkUpdateMetadata(items, this.metadataTemplate, operations, successCallback, errorCallback);
298
+ };
299
+
300
+ buildMetadataQueryParams = (filters: ExternalFilterValues) => {
301
+ let argIndex = 0;
302
+ let queries: string[] = [];
303
+ let queryParams: { [key: string]: number | Date | string } = {};
304
+
305
+ if (filters) {
306
+ Object.keys(filters).forEach(key => {
307
+ const filter = filters[key];
308
+ if (!filter) {
309
+ return;
310
+ }
311
+
312
+ const { fieldType, value } = filter;
313
+
314
+ switch (fieldType) {
315
+ case 'date':
316
+ case 'float': {
317
+ if (typeof value === 'object' && value !== null && 'range' in value) {
318
+ const result = getRangeFilter(value, key, argIndex);
319
+ queryParams = mergeQueryParams(queryParams, result.queryParams);
320
+ queries = mergeQueries(queries, result.queries);
321
+ argIndex += result.keysGenerated;
322
+ break;
323
+ }
324
+ break;
325
+ }
326
+ case 'enum':
327
+ case 'multiSelect': {
328
+ const arrayValue = Array.isArray(value) ? value.map(v => String(v)) : [String(value)];
329
+ let result;
330
+ if (key === 'mimetype-filter') {
331
+ result = getMimeTypeFilter(arrayValue, key, argIndex);
332
+ } else {
333
+ result = getSelectFilter(arrayValue, key, argIndex);
334
+ }
335
+ queryParams = mergeQueryParams(queryParams, result.queryParams);
336
+ queries = mergeQueries(queries, result.queries);
337
+ argIndex += result.keysGenerated;
338
+ break;
339
+ }
340
+
341
+ case 'string': {
342
+ if (value && value[0]) {
343
+ const result = getStringFilter(value[0], key, argIndex);
344
+ queryParams = mergeQueryParams(queryParams, result.queryParams);
345
+ queries = mergeQueries(queries, result.queries);
346
+ argIndex += result.keysGenerated;
347
+ }
348
+ break;
349
+ }
350
+
351
+ default:
352
+ break;
353
+ }
354
+ });
355
+ }
356
+
357
+ const query = queries.reduce((acc, curr, index) => {
358
+ if (index > 0) {
359
+ acc += ` AND ${curr}`;
360
+ } else {
361
+ acc = curr;
362
+ }
363
+ return acc;
364
+ }, '');
365
+
366
+ return {
367
+ queryParams,
368
+ query,
369
+ };
370
+ };
371
+
372
+ mergeQuery = (customQuery: string, filterQuery: string): string => {
373
+ if (!customQuery) {
374
+ return filterQuery;
375
+ }
376
+ if (!filterQuery) {
377
+ return customQuery;
378
+ }
379
+ // Merge queries with AND operator
380
+ return `${customQuery} AND ${filterQuery}`;
381
+ };
382
+
208
383
  /**
209
384
  * Verify that the metadata query has required fields and update it if necessary
210
385
  * For a file item, default fields included in the response are "type", "id", "etag"
211
386
  *
212
387
  * @param {MetadataQueryType} metadataQuery metadata query object
388
+ * @param {ExternalFilterValues} [fields] optional filter values to apply to the metadata query
213
389
  * @return {MetadataQueryType} updated metadata query object with required fields
214
390
  */
215
- verifyQueryFields = (metadataQuery: MetadataQueryType): MetadataQueryType => {
391
+ verifyQueryFields = (metadataQuery: MetadataQueryType, fields?: ExternalFilterValues): MetadataQueryType => {
216
392
  const clonedQuery = cloneDeep(metadataQuery);
217
393
  const clonedFields = isArray(clonedQuery.fields) ? clonedQuery.fields : [];
218
394
 
395
+ if (fields) {
396
+ const { query: filterQuery, queryParams: filteredQueryParams } = this.buildMetadataQueryParams(fields);
397
+ const { query: customQuery, query_params: customQueryParams } = clonedQuery;
398
+ const query = this.mergeQuery(customQuery, filterQuery);
399
+ const queryParams = mergeQueryParams(filteredQueryParams, customQueryParams);
400
+ if (query) {
401
+ clonedQuery.query = query;
402
+ clonedQuery.query_params = queryParams;
403
+ }
404
+ }
219
405
  // Make sure the query fields array has "name" field which is necessary to display info.
220
- if (!clonedFields.includes(FIELD_NAME)) {
221
- clonedFields.push(FIELD_NAME);
406
+ if (!clonedFields.includes(FIELD_ITEM_NAME)) {
407
+ clonedFields.push(FIELD_ITEM_NAME);
222
408
  }
223
409
 
224
410
  if (!clonedFields.includes(FIELD_EXTENSION)) {
225
411
  clonedFields.push(FIELD_EXTENSION);
226
412
  }
227
413
 
414
+ // This field is necessary to check if the user has permission to update metadata
415
+ if (!clonedFields.includes(FIELD_PERMISSIONS)) {
416
+ clonedFields.push(FIELD_PERMISSIONS);
417
+ }
418
+
228
419
  clonedQuery.fields = clonedFields;
229
420
 
230
421
  return clonedQuery;