box-ui-elements 24.0.0-beta.3 β†’ 24.0.0-beta.5

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 (159) hide show
  1. package/dist/explorer.js +1 -1
  2. package/dist/openwith.js +1 -1
  3. package/dist/picker.js +1 -1
  4. package/dist/preview.js +1 -1
  5. package/dist/sharing.js +1 -1
  6. package/dist/sidebar.js +1 -1
  7. package/dist/uploader.js +1 -1
  8. package/es/api/Metadata.js +98 -13
  9. package/es/api/Metadata.js.flow +110 -12
  10. package/es/api/Metadata.js.map +1 -1
  11. package/es/elements/common/messages.js +16 -0
  12. package/es/elements/common/messages.js.flow +25 -0
  13. package/es/elements/common/messages.js.map +1 -1
  14. package/es/elements/common/withBlueprintModernization.js +16 -0
  15. package/es/elements/common/withBlueprintModernization.js.map +1 -0
  16. package/es/elements/content-explorer/Content.js +2 -1
  17. package/es/elements/content-explorer/Content.js.map +1 -1
  18. package/es/elements/content-explorer/ContentExplorer.js +21 -6
  19. package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
  20. package/es/elements/content-explorer/MetadataQueryAPIHelper.js +61 -4
  21. package/es/elements/content-explorer/MetadataQueryAPIHelper.js.map +1 -1
  22. package/es/elements/content-explorer/MetadataSidePanel.js +40 -14
  23. package/es/elements/content-explorer/MetadataSidePanel.js.map +1 -1
  24. package/es/elements/content-explorer/MetadataViewContainer.js +55 -4
  25. package/es/elements/content-explorer/MetadataViewContainer.js.map +1 -1
  26. package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +5 -0
  27. package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.flow +6 -0
  28. package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.map +1 -1
  29. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js +61 -13
  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/elements/content-picker/ContentPicker.js +4 -1
  34. package/es/elements/content-picker/ContentPicker.js.flow +4 -1
  35. package/es/elements/content-picker/ContentPicker.js.map +1 -1
  36. package/es/elements/content-picker/stories/tests/ContentPicker-visual.stories.js +5 -0
  37. package/es/elements/content-picker/stories/tests/ContentPicker-visual.stories.js.flow +6 -0
  38. package/es/elements/content-picker/stories/tests/ContentPicker-visual.stories.js.map +1 -1
  39. package/es/elements/content-preview/ContentPreview.js +3 -1
  40. package/es/elements/content-preview/ContentPreview.js.flow +3 -0
  41. package/es/elements/content-preview/ContentPreview.js.map +1 -1
  42. package/es/elements/content-preview/stories/tests/ContentPreview-visual.stories.js +5 -0
  43. package/es/elements/content-preview/stories/tests/ContentPreview-visual.stories.js.flow +7 -1
  44. package/es/elements/content-preview/stories/tests/ContentPreview-visual.stories.js.map +1 -1
  45. package/es/elements/content-sharing/ContentSharing.js +4 -1
  46. package/es/elements/content-sharing/ContentSharing.js.flow +4 -1
  47. package/es/elements/content-sharing/ContentSharing.js.map +1 -1
  48. package/es/elements/content-sidebar/ContentSidebar.js +3 -1
  49. package/es/elements/content-sidebar/ContentSidebar.js.flow +3 -0
  50. package/es/elements/content-sidebar/ContentSidebar.js.map +1 -1
  51. package/es/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.js +5 -0
  52. package/es/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.js.map +1 -1
  53. package/es/elements/content-uploader/ContentUploader.js +3 -1
  54. package/es/elements/content-uploader/ContentUploader.js.map +1 -1
  55. package/es/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js +5 -0
  56. package/es/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js.flow +6 -0
  57. package/es/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js.map +1 -1
  58. package/es/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.js +51 -0
  59. package/es/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.js.map +1 -0
  60. package/es/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.scss +29 -0
  61. package/es/features/classification/applied-by-ai-classification-reason/messages.js +13 -0
  62. package/es/features/classification/applied-by-ai-classification-reason/messages.js.map +1 -0
  63. package/es/features/classification/types.js +2 -0
  64. package/es/features/classification/types.js.map +1 -0
  65. package/es/src/elements/common/__tests__/withBlueprintModernization.test.d.ts +1 -0
  66. package/es/src/elements/common/withBlueprintModernization.d.ts +3 -0
  67. package/es/src/elements/content-explorer/ContentExplorer.d.ts +11 -3
  68. package/es/src/elements/content-explorer/MetadataQueryAPIHelper.d.ts +11 -1
  69. package/es/src/elements/content-explorer/MetadataSidePanel.d.ts +6 -3
  70. package/es/src/elements/content-explorer/MetadataViewContainer.d.ts +3 -1
  71. package/es/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.d.ts +1 -0
  72. package/es/src/elements/content-explorer/utils.d.ts +9 -3
  73. package/es/src/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.d.ts +5 -0
  74. package/es/src/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.d.ts +6 -0
  75. package/es/src/features/classification/applied-by-ai-classification-reason/__tests__/AppliedByAiClassificationReason.test.d.ts +1 -0
  76. package/es/src/features/classification/applied-by-ai-classification-reason/messages.d.ts +13 -0
  77. package/es/src/features/classification/types.d.ts +6 -0
  78. package/i18n/bn-IN.js +8 -2
  79. package/i18n/bn-IN.properties +6 -2
  80. package/i18n/da-DK.js +8 -2
  81. package/i18n/da-DK.properties +6 -2
  82. package/i18n/de-DE.js +8 -2
  83. package/i18n/de-DE.properties +6 -2
  84. package/i18n/en-AU.js +6 -0
  85. package/i18n/en-AU.properties +4 -0
  86. package/i18n/en-CA.js +6 -0
  87. package/i18n/en-CA.properties +4 -0
  88. package/i18n/en-GB.js +6 -0
  89. package/i18n/en-GB.properties +4 -0
  90. package/i18n/en-US.js +6 -0
  91. package/i18n/en-US.properties +12 -0
  92. package/i18n/en-x-pseudo.js +6 -0
  93. package/i18n/es-419.js +8 -2
  94. package/i18n/es-419.properties +6 -2
  95. package/i18n/es-ES.js +8 -2
  96. package/i18n/es-ES.properties +6 -2
  97. package/i18n/fi-FI.js +8 -2
  98. package/i18n/fi-FI.properties +6 -2
  99. package/i18n/fr-CA.js +8 -2
  100. package/i18n/fr-CA.properties +6 -2
  101. package/i18n/fr-FR.js +8 -2
  102. package/i18n/fr-FR.properties +6 -2
  103. package/i18n/hi-IN.js +8 -2
  104. package/i18n/hi-IN.properties +6 -2
  105. package/i18n/it-IT.js +8 -2
  106. package/i18n/it-IT.properties +6 -2
  107. package/i18n/ja-JP.js +8 -2
  108. package/i18n/ja-JP.properties +6 -2
  109. package/i18n/ko-KR.js +8 -2
  110. package/i18n/ko-KR.properties +6 -2
  111. package/i18n/nb-NO.js +8 -2
  112. package/i18n/nb-NO.properties +6 -2
  113. package/i18n/nl-NL.js +8 -2
  114. package/i18n/nl-NL.properties +6 -2
  115. package/i18n/pl-PL.js +8 -2
  116. package/i18n/pl-PL.properties +6 -2
  117. package/i18n/pt-BR.js +8 -2
  118. package/i18n/pt-BR.properties +6 -2
  119. package/i18n/ru-RU.js +8 -2
  120. package/i18n/ru-RU.properties +6 -2
  121. package/i18n/sv-SE.js +8 -2
  122. package/i18n/sv-SE.properties +6 -2
  123. package/i18n/tr-TR.js +8 -2
  124. package/i18n/tr-TR.properties +6 -2
  125. package/i18n/zh-CN.js +8 -2
  126. package/i18n/zh-CN.properties +6 -2
  127. package/i18n/zh-TW.js +8 -2
  128. package/i18n/zh-TW.properties +6 -2
  129. package/package.json +1 -1
  130. package/src/api/Metadata.js +110 -12
  131. package/src/api/__tests__/Metadata.test.js +120 -0
  132. package/src/elements/common/__tests__/withBlueprintModernization.test.tsx +91 -0
  133. package/src/elements/common/messages.js +25 -0
  134. package/src/elements/common/withBlueprintModernization.tsx +24 -0
  135. package/src/elements/content-explorer/Content.tsx +1 -0
  136. package/src/elements/content-explorer/ContentExplorer.tsx +224 -182
  137. package/src/elements/content-explorer/MetadataQueryAPIHelper.ts +89 -4
  138. package/src/elements/content-explorer/MetadataSidePanel.tsx +55 -14
  139. package/src/elements/content-explorer/MetadataViewContainer.tsx +61 -1
  140. package/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +36 -2
  141. package/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +8 -5
  142. package/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx +145 -3
  143. package/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +6 -0
  144. package/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +54 -8
  145. package/src/elements/content-explorer/utils.ts +150 -13
  146. package/src/elements/content-picker/ContentPicker.js +4 -1
  147. package/src/elements/content-picker/stories/tests/ContentPicker-visual.stories.js +6 -0
  148. package/src/elements/content-preview/ContentPreview.js +3 -0
  149. package/src/elements/content-preview/stories/tests/ContentPreview-visual.stories.js +7 -1
  150. package/src/elements/content-sharing/ContentSharing.js +4 -1
  151. package/src/elements/content-sidebar/ContentSidebar.js +3 -0
  152. package/src/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.tsx +6 -0
  153. package/src/elements/content-uploader/ContentUploader.tsx +3 -1
  154. package/src/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js +6 -0
  155. package/src/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.scss +29 -0
  156. package/src/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.tsx +55 -0
  157. package/src/features/classification/applied-by-ai-classification-reason/__tests__/AppliedByAiClassificationReason.test.tsx +105 -0
  158. package/src/features/classification/applied-by-ai-classification-reason/messages.ts +18 -0
  159. package/src/features/classification/types.ts +7 -0
@@ -3,7 +3,9 @@ import { http, HttpResponse } from 'msw';
3
3
  import { Download, SignMeOthers } from '@box/blueprint-web-assets/icons/Fill/index';
4
4
  import { Sign } from '@box/blueprint-web-assets/icons/Line';
5
5
  import { expect, fn, userEvent, waitFor, within, screen } from 'storybook/test';
6
+
6
7
  import noop from 'lodash/noop';
8
+ import orderBy from 'lodash/orderBy';
7
9
 
8
10
  import ContentExplorer from '../../ContentExplorer';
9
11
  import { DEFAULT_HOSTNAME_API } from '../../../../constants';
@@ -138,17 +140,16 @@ export const metadataViewV2: Story = {
138
140
  args: metadataViewV2ElementProps,
139
141
  };
140
142
 
141
- // @TODO Assert that rows are actually sorted in a different order, once handleSortChange is implemented
142
143
  export const metadataViewV2SortsFromHeader: Story = {
143
144
  args: metadataViewV2ElementProps,
144
145
  play: async ({ canvas }) => {
145
- await waitFor(() => {
146
- expect(canvas.getByRole('row', { name: /Industry/i })).toBeInTheDocument();
147
- });
146
+ const industryHeader = await canvas.findByRole('columnheader', { name: 'Industry' });
147
+ expect(industryHeader).toBeInTheDocument();
148
+
149
+ const firstRow = await canvas.findByRole('row', { name: /Child 2/i });
150
+ expect(firstRow).toBeInTheDocument();
148
151
 
149
- const firstRow = canvas.getByRole('row', { name: /Industry/i });
150
- const industryHeader = within(firstRow).getByRole('columnheader', { name: 'Industry' });
151
- userEvent.click(industryHeader);
152
+ await userEvent.click(industryHeader);
152
153
  },
153
154
  };
154
155
 
@@ -237,6 +238,37 @@ export const metadataViewV2WithBulkItemActionMenuShowsItemActionMenu: Story = {
237
238
  },
238
239
  };
239
240
 
241
+ export const sidePanelOpenWithMultipleItemsSelected: Story = {
242
+ args: {
243
+ ...metadataViewV2ElementProps,
244
+ metadataViewProps: {
245
+ columns,
246
+ tableProps: {
247
+ isSelectAllEnabled: true,
248
+ },
249
+ },
250
+ },
251
+
252
+ play: async ({ canvas }) => {
253
+ await waitFor(() => {
254
+ expect(canvas.getByRole('row', { name: /Child 2/i })).toBeInTheDocument();
255
+ });
256
+
257
+ // Select the first row by clicking its checkbox
258
+ const firstItem = canvas.getByRole('row', { name: /Child 2/i });
259
+ const checkbox = within(firstItem).getByRole('checkbox');
260
+ await userEvent.click(checkbox);
261
+
262
+ // Select the second row by clicking its checkbox
263
+ const secondItem = canvas.getAllByRole('row', { name: /Child 1/i })[0];
264
+ const secondCheckbox = within(secondItem).getByRole('checkbox');
265
+ await userEvent.click(secondCheckbox);
266
+
267
+ const metadataButton = canvas.getByRole('button', { name: 'Metadata' });
268
+ await userEvent.click(metadataButton);
269
+ },
270
+ };
271
+
240
272
  const meta: Meta<typeof ContentExplorer> = {
241
273
  title: 'Elements/ContentExplorer/tests/MetadataView/visual',
242
274
  component: ContentExplorer,
@@ -248,7 +280,21 @@ const meta: Meta<typeof ContentExplorer> = {
248
280
  parameters: {
249
281
  msw: {
250
282
  handlers: [
251
- http.post(`${DEFAULT_HOSTNAME_API}/2.0/metadata_queries/execute_read`, () => {
283
+ // Note that the Metadata API backend normally handles the sorting. The mocks below simulate the sorting for specific cases, but may not 100% accurately reflect the backend behavior.
284
+ http.post(`${DEFAULT_HOSTNAME_API}/2.0/metadata_queries/execute_read`, async ({ request }) => {
285
+ const body = await request.clone().json();
286
+ const orderByDirection = body.order_by[0].direction;
287
+ const orderByFieldKey = body.order_by[0].field_key;
288
+
289
+ // Hardcoded case for sorting by industry
290
+ if (orderByFieldKey === `industry` && orderByDirection === 'ASC') {
291
+ const sortedMetadata = orderBy(
292
+ mockMetadata.entries,
293
+ 'metadata.enterprise_0.templateName.industry',
294
+ 'asc',
295
+ );
296
+ return HttpResponse.json({ ...mockMetadata, entries: sortedMetadata });
297
+ }
252
298
  return HttpResponse.json(mockMetadata);
253
299
  }),
254
300
  http.get(`${DEFAULT_HOSTNAME_API}/2.0/metadata_templates/enterprise/templateName/schema`, () => {
@@ -1,12 +1,24 @@
1
- import { useMemo } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
+ import isNil from 'lodash/isNil';
4
+ import xor from 'lodash/xor';
3
5
 
4
- import type { MetadataTemplate } from '@box/metadata-editor';
6
+ import {
7
+ MULTI_VALUE_DEFAULT_OPTION,
8
+ MULTI_VALUE_DEFAULT_VALUE,
9
+ type MetadataTemplate,
10
+ type MetadataFormFieldValue,
11
+ } from '@box/metadata-editor';
12
+ import type { MetadataFieldType } from '@box/metadata-view';
5
13
  import type { Selection } from 'react-aria-components';
6
14
  import type { BoxItem, Collection } from '../../common/types/core';
7
15
 
8
16
  import messages from '../common/messages';
9
17
 
18
+ // Specific type for metadata field value in the item
19
+ // Note: Item doesn't have field value in metadata object if that field is not set, so the value will be undefined in this case
20
+ type ItemMetadataFieldValue = string | number | Array<string> | null | undefined;
21
+
10
22
  // Get selected item text
11
23
  export function useSelectedItemText(currentCollection: Collection, selectedItemIds: Selection): string {
12
24
  const { formatMessage } = useIntl();
@@ -28,21 +40,146 @@ export function useSelectedItemText(currentCollection: Collection, selectedItemI
28
40
  }, [currentCollection.items, formatMessage, selectedItemIds]);
29
41
  }
30
42
 
43
+ // Check if the field value is empty.
44
+ // Note: 0 doesn't represent empty here because of float type field
45
+ export function isEmptyValue(value: ItemMetadataFieldValue) {
46
+ if (isNil(value)) {
47
+ return true;
48
+ }
49
+
50
+ // date, string, enum
51
+ if (value === '') {
52
+ return true;
53
+ }
54
+
55
+ // multiSelect
56
+ if (Array.isArray(value) && value.length === 0) {
57
+ return true;
58
+ }
59
+
60
+ // float
61
+ if (Number.isNaN(value)) {
62
+ return true;
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ // Check if the field values are equal based on the field types
69
+ export function areFieldValuesEqual(value1: ItemMetadataFieldValue, value2: ItemMetadataFieldValue) {
70
+ if (isEmptyValue(value1) && isEmptyValue(value2)) {
71
+ return true;
72
+ }
73
+
74
+ // Handle multiSelect arrays comparison
75
+ if (Array.isArray(value1) && Array.isArray(value2)) {
76
+ return xor(value1, value2).length === 0;
77
+ }
78
+
79
+ return value1 === value2;
80
+ }
81
+
82
+ // Return default form value by field type
83
+ function getDefaultValueByFieldType(fieldType: MetadataFieldType) {
84
+ if (fieldType === 'date' || fieldType === 'enum' || fieldType === 'float' || fieldType === 'string') {
85
+ return '';
86
+ }
87
+ if (fieldType === 'multiSelect') {
88
+ return [];
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ // Set the field value in Metadata Form based on the field type
94
+ function getFieldValue(fieldType: MetadataFieldType, fieldValue: ItemMetadataFieldValue) {
95
+ if (isNil(fieldValue)) {
96
+ return getDefaultValueByFieldType(fieldType);
97
+ }
98
+ return fieldValue;
99
+ }
100
+
101
+ // Check if the field value in Metadata Form is multi-values such as "Multiple values"
102
+ export function isMultiValuesField(fieldType: MetadataFieldType, fieldValue: MetadataFormFieldValue) {
103
+ if (fieldType === 'multiSelect') {
104
+ return Array.isArray(fieldValue) && fieldValue.length === 1 && fieldValue[0] === MULTI_VALUE_DEFAULT_VALUE;
105
+ }
106
+ if (fieldType === 'enum') {
107
+ return fieldValue === MULTI_VALUE_DEFAULT_VALUE;
108
+ }
109
+ return false;
110
+ }
111
+
31
112
  // Get template instance based on metadata template and selected items
32
- export function getTemplateInstance(metadataTemplate: MetadataTemplate, selectedItems: BoxItem[]) {
113
+ export function useTemplateInstance(metadataTemplate: MetadataTemplate, selectedItems: BoxItem[], isEditing: boolean) {
114
+ const { formatMessage } = useIntl();
33
115
  const { displayName, fields, hidden, id, scope, templateKey, type } = metadataTemplate;
34
116
 
35
117
  const selectedItemsFields = fields.map(
36
- ({ displayName: fieldDisplayName, hidden: fieldHidden, id: fieldId, key, options, type: fieldType }) => ({
37
- displayName: fieldDisplayName,
38
- hidden: fieldHidden,
39
- id: fieldId,
40
- key,
41
- options,
42
- type: fieldType,
43
- // TODO: Add support for multiple selected items
44
- value: selectedItems[0].metadata[scope][templateKey][key],
45
- }),
118
+ ({ displayName: fieldDisplayName, hidden: fieldHidden, id: fieldId, key, options, type: fieldType }) => {
119
+ const defaultItemField = {
120
+ displayName: fieldDisplayName,
121
+ hidden: fieldHidden,
122
+ id: fieldId,
123
+ key,
124
+ options,
125
+ type: fieldType,
126
+ value: getFieldValue(fieldType as MetadataFieldType, undefined),
127
+ };
128
+
129
+ const firstSelectedItem = selectedItems[0];
130
+ const firstSelectedItemFieldValue = firstSelectedItem.metadata[scope][templateKey][key];
131
+
132
+ // Case 1: Single selected item
133
+ if (selectedItems.length <= 1) {
134
+ return {
135
+ ...defaultItemField,
136
+ value: firstSelectedItemFieldValue,
137
+ };
138
+ }
139
+
140
+ // Case 2.1: Multiple selected items, but all have the same initial value
141
+ const allItemsHaveSameInitialValue = selectedItems.every(selectedItem =>
142
+ areFieldValuesEqual(selectedItem.metadata[scope][templateKey][key], firstSelectedItemFieldValue),
143
+ );
144
+
145
+ if (allItemsHaveSameInitialValue) {
146
+ return {
147
+ ...defaultItemField,
148
+ value: getFieldValue(fieldType as MetadataFieldType, firstSelectedItemFieldValue),
149
+ };
150
+ }
151
+
152
+ // Case 2.2: Multiple selected items, but some have different initial values
153
+ // Case 2.2.1: Edit Mode
154
+ if (isEditing) {
155
+ let fieldValue = getFieldValue(fieldType as MetadataFieldType, undefined);
156
+ // Add MultiValue Option if the field is multiSelect or enum
157
+ if (fieldType === 'multiSelect' || fieldType === 'enum') {
158
+ fieldValue = fieldType === 'enum' ? MULTI_VALUE_DEFAULT_VALUE : [MULTI_VALUE_DEFAULT_VALUE];
159
+ const multiValueOption = options?.find(option => option.key === MULTI_VALUE_DEFAULT_VALUE);
160
+ if (!multiValueOption) {
161
+ options?.push(MULTI_VALUE_DEFAULT_OPTION);
162
+ }
163
+ }
164
+ return {
165
+ ...defaultItemField,
166
+ value: fieldValue,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Case: 2.2.2 View Mode
172
+ *
173
+ * We want to show "Multiple values" label for multiple dates across files selection.
174
+ * We use fragment here to bypass check in shared feature.
175
+ * This feature tries to parse string as date if the string is passed as value.
176
+ */
177
+ const multipleValuesText = formatMessage(messages.multipleValues);
178
+ return {
179
+ ...defaultItemField,
180
+ value: React.createElement(React.Fragment, null, multipleValuesText),
181
+ };
182
+ },
46
183
  );
47
184
 
48
185
  return {
@@ -9,6 +9,7 @@ import React, { Component } from 'react';
9
9
  import type { Node } from 'react';
10
10
  import classNames from 'classnames';
11
11
  import debounce from 'lodash/debounce';
12
+ import flow from 'lodash/flow';
12
13
  import getProp from 'lodash/get';
13
14
  import uniqueid from 'lodash/uniqueId';
14
15
  import noop from 'lodash/noop';
@@ -18,6 +19,8 @@ import UploadDialog from '../common/upload-dialog';
18
19
  import CreateFolderDialog from '../common/create-folder-dialog';
19
20
  import Internationalize from '../common/Internationalize';
20
21
  import makeResponsive from '../common/makeResponsive';
22
+ // $FlowFixMe
23
+ import { withBlueprintModernization } from '../common/withBlueprintModernization';
21
24
  // $FlowFixMe TypeScript file
22
25
  import ThemingStyles from '../common/theming';
23
26
  import Pagination from '../../features/pagination';
@@ -1345,4 +1348,4 @@ class ContentPicker extends Component<Props, State> {
1345
1348
  }
1346
1349
 
1347
1350
  export { ContentPicker as ContentPickerComponent };
1348
- export default makeResponsive(ContentPicker);
1351
+ export default flow([makeResponsive, withBlueprintModernization])(ContentPicker);
@@ -8,6 +8,12 @@ import { DEFAULT_HOSTNAME_API } from '../../../../constants';
8
8
 
9
9
  export const basic = {};
10
10
 
11
+ export const Modernization = {
12
+ args: {
13
+ enableModernizedComponents: true,
14
+ },
15
+ };
16
+
11
17
  export const withPagination = {
12
18
  args: {
13
19
  initialPageSize: 1,
@@ -35,6 +35,8 @@ import { withLogger } from '../common/logger';
35
35
  import { PREVIEW_FIELDS_TO_FETCH } from '../../utils/fields';
36
36
  import { mark } from '../../utils/performance';
37
37
  import { withFeatureConsumer, withFeatureProvider } from '../common/feature-checking';
38
+ // $FlowFixMe
39
+ import { withBlueprintModernization } from '../common/withBlueprintModernization';
38
40
  import { EVENT_JS_READY } from '../common/logger/constants';
39
41
  import ReloadNotification from './ReloadNotification';
40
42
  import API from '../../api';
@@ -1418,6 +1420,7 @@ export default flow([
1418
1420
  withNavRouter,
1419
1421
  withFeatureConsumer,
1420
1422
  withFeatureProvider,
1423
+ withBlueprintModernization,
1421
1424
  withLogger(ORIGIN_CONTENT_PREVIEW),
1422
1425
  withErrorBoundary(ORIGIN_CONTENT_PREVIEW),
1423
1426
  ])(ContentPreview);
@@ -30,6 +30,12 @@ export const basic = {
30
30
  },
31
31
  };
32
32
 
33
+ export const Modernization = {
34
+ args: {
35
+ enableModernizedComponents: true,
36
+ },
37
+ };
38
+
33
39
  export const closeModal = {
34
40
  play: async ({ canvasElement }) => {
35
41
  const canvas = within(canvasElement);
@@ -92,7 +98,7 @@ export const hoverOverCitation = {
92
98
 
93
99
  expect(modal.getByText('Based on:')).toBeInTheDocument();
94
100
 
95
- const citations = await modal.getAllByTestId('content-answers-citation-status')
101
+ const citations = await modal.getAllByTestId('content-answers-citation-status');
96
102
  const citation = citations[0];
97
103
  expect(citation).toBeInTheDocument();
98
104
  await userEvent.hover(citation);
@@ -9,6 +9,8 @@
9
9
  import 'regenerator-runtime/runtime';
10
10
  import * as React from 'react';
11
11
  import API from '../../api';
12
+ // $FlowFixMe
13
+ import { withBlueprintModernization } from '../common/withBlueprintModernization';
12
14
  import SharingModal from './SharingModal';
13
15
  import { CLIENT_NAME_CONTENT_SHARING, DEFAULT_HOSTNAME_API } from '../../constants';
14
16
  import type { ItemType, StringMap } from '../../common/types/core';
@@ -118,4 +120,5 @@ function ContentSharing({
118
120
  );
119
121
  }
120
122
 
121
- export default ContentSharing;
123
+ export { ContentSharing as ContentSharingComponent };
124
+ export default withBlueprintModernization(ContentSharing);
@@ -21,6 +21,8 @@ import { EVENT_JS_READY } from '../common/logger/constants';
21
21
  import { mark } from '../../utils/performance';
22
22
  import { SIDEBAR_FIELDS_TO_FETCH, SIDEBAR_FIELDS_TO_FETCH_ARCHIVE } from '../../utils/fields';
23
23
  import { withErrorBoundary } from '../common/error-boundary';
24
+ // $FlowFixMe
25
+ import { withBlueprintModernization } from '../common/withBlueprintModernization';
24
26
  import {
25
27
  isFeatureEnabled as isFeatureEnabledInContext,
26
28
  withFeatureConsumer,
@@ -432,6 +434,7 @@ export { ContentSidebar as ContentSidebarComponent };
432
434
  export default flow([
433
435
  withFeatureConsumer,
434
436
  withFeatureProvider,
437
+ withBlueprintModernization,
435
438
  withLogger(ORIGIN_CONTENT_SIDEBAR),
436
439
  withErrorBoundary(ORIGIN_CONTENT_SIDEBAR),
437
440
  ])(ContentSidebar);
@@ -23,6 +23,12 @@ export default {
23
23
  },
24
24
  };
25
25
 
26
+ export const Modernization = {
27
+ args: {
28
+ enableModernizedComponents: true,
29
+ },
30
+ };
31
+
26
32
  export const ContentSidebarWithBoxAIDisabled: StoryObj<typeof BoxAISidebar> = {
27
33
  args: {
28
34
  features: {
@@ -2,6 +2,7 @@ import 'regenerator-runtime/runtime';
2
2
  import React, { Component } from 'react';
3
3
  import classNames from 'classnames';
4
4
  import cloneDeep from 'lodash/cloneDeep';
5
+ import flow from 'lodash/flow';
5
6
  import getProp from 'lodash/get';
6
7
  import noop from 'lodash/noop';
7
8
  import uniqueid from 'lodash/uniqueId';
@@ -14,6 +15,7 @@ import API from '../../api';
14
15
  import Browser from '../../utils/Browser';
15
16
  import Internationalize from '../common/Internationalize';
16
17
  import makeResponsive from '../common/makeResponsive';
18
+ import { withBlueprintModernization } from '../common/withBlueprintModernization';
17
19
  import ThemingStyles, { Theme } from '../common/theming';
18
20
  import FolderUpload from '../../api/uploads/FolderUpload';
19
21
  import { getTypedFileId, getTypedFolderId } from '../../utils/file';
@@ -1323,5 +1325,5 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
1323
1325
  }
1324
1326
  }
1325
1327
 
1326
- export default makeResponsive(ContentUploader);
1328
+ export default flow([makeResponsive, withBlueprintModernization])(ContentUploader);
1327
1329
  export { ContentUploader as ContentUploaderComponent, CHUNKED_UPLOAD_MIN_SIZE_BYTES };
@@ -23,6 +23,12 @@ export const basic = {
23
23
  },
24
24
  };
25
25
 
26
+ export const Modernization = {
27
+ args: {
28
+ enableModernizedComponents: true,
29
+ },
30
+ };
31
+
26
32
  export const singleUpload = {
27
33
  play: async ({ canvasElement }) => {
28
34
  const canvas = within(canvasElement);
@@ -0,0 +1,29 @@
1
+ $reason-font-size: 13px; // Overriding Blueprint to match current sidebar styles
2
+
3
+ .AppliedByAiClassificationReason {
4
+ .AppliedByAiClassificationReason-headerText {
5
+ font-size: $reason-font-size;
6
+ }
7
+
8
+ .AppliedByAiClassificationReason-answer {
9
+ font-size: $reason-font-size;
10
+ line-height: var(--body-default-line-height);
11
+ }
12
+
13
+ .AppliedByAiClassificationReason-references {
14
+ > * > * {
15
+ font-size: $reason-font-size;
16
+ }
17
+ }
18
+ }
19
+
20
+ .AppliedByAiClassificationReason-header {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--space-2);
24
+ margin: 0 0 var(--space-2);
25
+ }
26
+
27
+ .AppliedByAiClassificationReason-references {
28
+ margin-top: var(--space-2);
29
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { AnswerContent, References } from '@box/box-ai-content-answers';
3
+ import { Card, Text } from '@box/blueprint-web';
4
+ import BoxAIIconColor from '@box/blueprint-web-assets/icons/Logo/BoxAiLogo';
5
+ import { Size5 } from '@box/blueprint-web-assets/tokens/tokens';
6
+ import { FormattedDate, FormattedMessage } from 'react-intl';
7
+
8
+ import { isValidDate } from '../../../utils/datetime';
9
+ import type { AiClassificationReason } from '../types';
10
+
11
+ import messages from './messages';
12
+
13
+ import './AppliedByAiClassificationReason.scss';
14
+
15
+ export type AppliedByAiClassificationReasonProps = AiClassificationReason;
16
+
17
+ const AppliedByAiClassificationReason = ({ answer, modifiedAt, citations }: AppliedByAiClassificationReasonProps) => {
18
+ const modifiedDate = new Date(modifiedAt);
19
+ const isModifiedDateAvailable = Boolean(modifiedAt) && isValidDate(modifiedDate);
20
+
21
+ const formattedModifiedAt = isModifiedDateAvailable && (
22
+ <FormattedDate value={modifiedDate} month="long" year="numeric" day="numeric" />
23
+ );
24
+
25
+ return (
26
+ <Card className="AppliedByAiClassificationReason">
27
+ <h3 className="AppliedByAiClassificationReason-header">
28
+ <BoxAIIconColor data-testid="box-ai-icon" height={Size5} width={Size5} />
29
+ <Text
30
+ className="AppliedByAiClassificationReason-headerText"
31
+ as="span"
32
+ color="textOnLightSecondary"
33
+ variant="bodyDefaultSemibold"
34
+ >
35
+ {isModifiedDateAvailable ? (
36
+ <FormattedMessage
37
+ {...messages.appliedByBoxAiOnDate}
38
+ values={{ modifiedAt: formattedModifiedAt }}
39
+ />
40
+ ) : (
41
+ <FormattedMessage {...messages.appliedByBoxAi} />
42
+ )}
43
+ </Text>
44
+ </h3>
45
+ <AnswerContent className="AppliedByAiClassificationReason-answer" answer={answer} />
46
+ {citations && (
47
+ <div className="AppliedByAiClassificationReason-references">
48
+ <References citations={citations} />
49
+ </div>
50
+ )}
51
+ </Card>
52
+ );
53
+ };
54
+
55
+ export default AppliedByAiClassificationReason;
@@ -0,0 +1,105 @@
1
+ import * as React from 'react';
2
+
3
+ import { render, screen } from '../../../../test-utils/testing-library';
4
+ import AppliedByAiClassificationReason from '../AppliedByAiClassificationReason';
5
+
6
+ import messages from '../messages';
7
+
8
+ describe('AppliedByAiClassificationReason', () => {
9
+ let defaultProps;
10
+ let modifiedAtDisplayDate;
11
+
12
+ beforeEach(() => {
13
+ defaultProps = {
14
+ answer: 'This file is marked as Internal Only because it contains non-public financial results.',
15
+ modifiedAt: '2024-01-15T10:30:00Z',
16
+ };
17
+ modifiedAtDisplayDate = 'January 15, 2024';
18
+ });
19
+
20
+ const renderComponent = (props = {}) => {
21
+ return render(<AppliedByAiClassificationReason {...defaultProps} {...props} />);
22
+ };
23
+
24
+ test('should render AI classification reason with icon, applied date, and reasoning', () => {
25
+ const expectedIconSize = '1.25rem';
26
+
27
+ renderComponent();
28
+
29
+ const boxAiIcon = screen.getByTestId('box-ai-icon');
30
+ const appliedByWithDate = screen.getByRole('heading', {
31
+ level: 3,
32
+ name: messages.appliedByBoxAiOnDate.defaultMessage.replace('{modifiedAt}', modifiedAtDisplayDate),
33
+ });
34
+ const reasonText = screen.getByText(defaultProps.answer);
35
+ const citationsLabel = screen.queryByTestId('content-answers-references-label');
36
+ const noReferencesIconContainer = screen.queryByTestId('content-answers-references-no-references');
37
+
38
+ expect(boxAiIcon).toBeVisible();
39
+ expect(boxAiIcon).toHaveAttribute('height', expectedIconSize);
40
+ expect(boxAiIcon).toHaveAttribute('width', expectedIconSize);
41
+ expect(appliedByWithDate).toBeVisible();
42
+ expect(reasonText).toBeVisible();
43
+ // Assert none of the Reference components are rendered
44
+ expect(citationsLabel).toBeNull();
45
+ expect(noReferencesIconContainer).toBeNull();
46
+ });
47
+
48
+ test('should render no references icon when an empty citations array provided', () => {
49
+ renderComponent({ citations: [] });
50
+
51
+ const noReferencesIconContainer = screen.queryByTestId('content-answers-references-no-references');
52
+ const noReferencesIcon = noReferencesIconContainer.querySelector('svg');
53
+
54
+ expect(noReferencesIcon).toBeVisible();
55
+ });
56
+
57
+ test('should render references when non-empty citations are provided', () => {
58
+ const expectedCitationsCount = 5;
59
+ const expectedCitations = Array.from({ length: expectedCitationsCount }, () => ({
60
+ content: 'file content for citation',
61
+ fileId: 'fileId',
62
+ location: 'cited location',
63
+ title: 'file title',
64
+ }));
65
+
66
+ renderComponent({ citations: expectedCitations });
67
+
68
+ const citationsLabel = screen.queryByTestId('content-answers-references-label');
69
+ const citationElements = screen.getAllByTestId('content-answers-citation-status');
70
+
71
+ expect(citationsLabel).toBeVisible();
72
+ expect(citationElements).toHaveLength(expectedCitationsCount);
73
+ });
74
+
75
+ test.each([null, undefined, 'invalid date str'])(
76
+ 'should render applied by without date when modifiedAt is invalid: %s',
77
+ invalidModifiedAt => {
78
+ renderComponent({ modifiedAt: invalidModifiedAt });
79
+
80
+ const appliedByWithoutDate = screen.getByRole('heading', {
81
+ level: 3,
82
+ name: messages.appliedByBoxAi.defaultMessage,
83
+ });
84
+
85
+ expect(appliedByWithoutDate).toBeVisible();
86
+ },
87
+ );
88
+
89
+ test('should render long answer text correctly', () => {
90
+ const longAnswer = 'A'.repeat(1000);
91
+ renderComponent({ answer: longAnswer });
92
+
93
+ expect(screen.getByText(longAnswer)).toBeVisible();
94
+ });
95
+
96
+ test('should render answer with special and unicode characters correctly', () => {
97
+ const nonPlainAnswer = 'Answer with special characters and unicode !@#$%^&*()_+-=[]{}|;:,.<>? πŸš€πŸŒŸπŸŽ‰δΈ­ζ–‡ζ—₯本θͺž';
98
+
99
+ renderComponent({ answer: nonPlainAnswer });
100
+
101
+ const reasonText = screen.getByText(nonPlainAnswer);
102
+
103
+ expect(reasonText).toBeVisible();
104
+ });
105
+ });
@@ -0,0 +1,18 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ const messages = defineMessages({
4
+ appliedByBoxAi: {
5
+ defaultMessage: 'Box AI',
6
+ description:
7
+ 'Title of the card that shows the reason why the AI classification was applied when no date is available.',
8
+ id: 'boxui.classification.appliedByBoxAi',
9
+ },
10
+ appliedByBoxAiOnDate: {
11
+ defaultMessage: 'Box AI on {modifiedAt}',
12
+ description:
13
+ 'Title of the card that shows the reason why the AI classification was applied on a specific date.',
14
+ id: 'boxui.classification.appliedByBoxAiOnDate',
15
+ },
16
+ });
17
+
18
+ export default messages;
@@ -0,0 +1,7 @@
1
+ import type { CitationType } from '@box/box-ai-content-answers';
2
+
3
+ export type AiClassificationReason = {
4
+ answer: string;
5
+ modifiedAt?: string;
6
+ citations?: CitationType[];
7
+ };