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.
- package/dist/explorer.js +1 -1
- package/dist/openwith.js +1 -1
- package/dist/picker.js +1 -1
- package/dist/preview.js +1 -1
- package/dist/sharing.js +1 -1
- package/dist/sidebar.js +1 -1
- package/dist/uploader.js +1 -1
- package/es/api/Metadata.js +98 -13
- package/es/api/Metadata.js.flow +110 -12
- package/es/api/Metadata.js.map +1 -1
- package/es/elements/common/messages.js +16 -0
- package/es/elements/common/messages.js.flow +25 -0
- package/es/elements/common/messages.js.map +1 -1
- package/es/elements/common/withBlueprintModernization.js +16 -0
- package/es/elements/common/withBlueprintModernization.js.map +1 -0
- package/es/elements/content-explorer/Content.js +2 -1
- package/es/elements/content-explorer/Content.js.map +1 -1
- package/es/elements/content-explorer/ContentExplorer.js +21 -6
- package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
- package/es/elements/content-explorer/MetadataQueryAPIHelper.js +61 -4
- package/es/elements/content-explorer/MetadataQueryAPIHelper.js.map +1 -1
- package/es/elements/content-explorer/MetadataSidePanel.js +40 -14
- package/es/elements/content-explorer/MetadataSidePanel.js.map +1 -1
- package/es/elements/content-explorer/MetadataViewContainer.js +55 -4
- package/es/elements/content-explorer/MetadataViewContainer.js.map +1 -1
- package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +5 -0
- package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.flow +6 -0
- package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.map +1 -1
- package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js +61 -13
- package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js.map +1 -1
- package/es/elements/content-explorer/utils.js +140 -12
- package/es/elements/content-explorer/utils.js.map +1 -1
- package/es/elements/content-picker/ContentPicker.js +4 -1
- package/es/elements/content-picker/ContentPicker.js.flow +4 -1
- package/es/elements/content-picker/ContentPicker.js.map +1 -1
- package/es/elements/content-picker/stories/tests/ContentPicker-visual.stories.js +5 -0
- package/es/elements/content-picker/stories/tests/ContentPicker-visual.stories.js.flow +6 -0
- package/es/elements/content-picker/stories/tests/ContentPicker-visual.stories.js.map +1 -1
- package/es/elements/content-preview/ContentPreview.js +3 -1
- package/es/elements/content-preview/ContentPreview.js.flow +3 -0
- package/es/elements/content-preview/ContentPreview.js.map +1 -1
- package/es/elements/content-preview/stories/tests/ContentPreview-visual.stories.js +5 -0
- package/es/elements/content-preview/stories/tests/ContentPreview-visual.stories.js.flow +7 -1
- package/es/elements/content-preview/stories/tests/ContentPreview-visual.stories.js.map +1 -1
- package/es/elements/content-sharing/ContentSharing.js +4 -1
- package/es/elements/content-sharing/ContentSharing.js.flow +4 -1
- package/es/elements/content-sharing/ContentSharing.js.map +1 -1
- package/es/elements/content-sidebar/ContentSidebar.js +3 -1
- package/es/elements/content-sidebar/ContentSidebar.js.flow +3 -0
- package/es/elements/content-sidebar/ContentSidebar.js.map +1 -1
- package/es/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.js +5 -0
- package/es/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.js.map +1 -1
- package/es/elements/content-uploader/ContentUploader.js +3 -1
- package/es/elements/content-uploader/ContentUploader.js.map +1 -1
- package/es/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js +5 -0
- package/es/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js.flow +6 -0
- package/es/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js.map +1 -1
- package/es/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.js +51 -0
- package/es/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.js.map +1 -0
- package/es/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.scss +29 -0
- package/es/features/classification/applied-by-ai-classification-reason/messages.js +13 -0
- package/es/features/classification/applied-by-ai-classification-reason/messages.js.map +1 -0
- package/es/features/classification/types.js +2 -0
- package/es/features/classification/types.js.map +1 -0
- package/es/src/elements/common/__tests__/withBlueprintModernization.test.d.ts +1 -0
- package/es/src/elements/common/withBlueprintModernization.d.ts +3 -0
- package/es/src/elements/content-explorer/ContentExplorer.d.ts +11 -3
- package/es/src/elements/content-explorer/MetadataQueryAPIHelper.d.ts +11 -1
- package/es/src/elements/content-explorer/MetadataSidePanel.d.ts +6 -3
- package/es/src/elements/content-explorer/MetadataViewContainer.d.ts +3 -1
- package/es/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.d.ts +1 -0
- package/es/src/elements/content-explorer/utils.d.ts +9 -3
- package/es/src/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.d.ts +5 -0
- package/es/src/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.d.ts +6 -0
- package/es/src/features/classification/applied-by-ai-classification-reason/__tests__/AppliedByAiClassificationReason.test.d.ts +1 -0
- package/es/src/features/classification/applied-by-ai-classification-reason/messages.d.ts +13 -0
- package/es/src/features/classification/types.d.ts +6 -0
- package/i18n/bn-IN.js +8 -2
- package/i18n/bn-IN.properties +6 -2
- package/i18n/da-DK.js +8 -2
- package/i18n/da-DK.properties +6 -2
- package/i18n/de-DE.js +8 -2
- package/i18n/de-DE.properties +6 -2
- package/i18n/en-AU.js +6 -0
- package/i18n/en-AU.properties +4 -0
- package/i18n/en-CA.js +6 -0
- package/i18n/en-CA.properties +4 -0
- package/i18n/en-GB.js +6 -0
- package/i18n/en-GB.properties +4 -0
- package/i18n/en-US.js +6 -0
- package/i18n/en-US.properties +12 -0
- package/i18n/en-x-pseudo.js +6 -0
- package/i18n/es-419.js +8 -2
- package/i18n/es-419.properties +6 -2
- package/i18n/es-ES.js +8 -2
- package/i18n/es-ES.properties +6 -2
- package/i18n/fi-FI.js +8 -2
- package/i18n/fi-FI.properties +6 -2
- package/i18n/fr-CA.js +8 -2
- package/i18n/fr-CA.properties +6 -2
- package/i18n/fr-FR.js +8 -2
- package/i18n/fr-FR.properties +6 -2
- package/i18n/hi-IN.js +8 -2
- package/i18n/hi-IN.properties +6 -2
- package/i18n/it-IT.js +8 -2
- package/i18n/it-IT.properties +6 -2
- package/i18n/ja-JP.js +8 -2
- package/i18n/ja-JP.properties +6 -2
- package/i18n/ko-KR.js +8 -2
- package/i18n/ko-KR.properties +6 -2
- package/i18n/nb-NO.js +8 -2
- package/i18n/nb-NO.properties +6 -2
- package/i18n/nl-NL.js +8 -2
- package/i18n/nl-NL.properties +6 -2
- package/i18n/pl-PL.js +8 -2
- package/i18n/pl-PL.properties +6 -2
- package/i18n/pt-BR.js +8 -2
- package/i18n/pt-BR.properties +6 -2
- package/i18n/ru-RU.js +8 -2
- package/i18n/ru-RU.properties +6 -2
- package/i18n/sv-SE.js +8 -2
- package/i18n/sv-SE.properties +6 -2
- package/i18n/tr-TR.js +8 -2
- package/i18n/tr-TR.properties +6 -2
- package/i18n/zh-CN.js +8 -2
- package/i18n/zh-CN.properties +6 -2
- package/i18n/zh-TW.js +8 -2
- package/i18n/zh-TW.properties +6 -2
- package/package.json +1 -1
- package/src/api/Metadata.js +110 -12
- package/src/api/__tests__/Metadata.test.js +120 -0
- package/src/elements/common/__tests__/withBlueprintModernization.test.tsx +91 -0
- package/src/elements/common/messages.js +25 -0
- package/src/elements/common/withBlueprintModernization.tsx +24 -0
- package/src/elements/content-explorer/Content.tsx +1 -0
- package/src/elements/content-explorer/ContentExplorer.tsx +224 -182
- package/src/elements/content-explorer/MetadataQueryAPIHelper.ts +89 -4
- package/src/elements/content-explorer/MetadataSidePanel.tsx +55 -14
- package/src/elements/content-explorer/MetadataViewContainer.tsx +61 -1
- package/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +36 -2
- package/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +8 -5
- package/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx +145 -3
- package/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +6 -0
- package/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +54 -8
- package/src/elements/content-explorer/utils.ts +150 -13
- package/src/elements/content-picker/ContentPicker.js +4 -1
- package/src/elements/content-picker/stories/tests/ContentPicker-visual.stories.js +6 -0
- package/src/elements/content-preview/ContentPreview.js +3 -0
- package/src/elements/content-preview/stories/tests/ContentPreview-visual.stories.js +7 -1
- package/src/elements/content-sharing/ContentSharing.js +4 -1
- package/src/elements/content-sidebar/ContentSidebar.js +3 -0
- package/src/elements/content-sidebar/stories/tests/ContentSidebar-visual.stories.tsx +6 -0
- package/src/elements/content-uploader/ContentUploader.tsx +3 -1
- package/src/elements/content-uploader/stories/tests/ContentUploader-visual.stories.js +6 -0
- package/src/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.scss +29 -0
- package/src/features/classification/applied-by-ai-classification-reason/AppliedByAiClassificationReason.tsx +55 -0
- package/src/features/classification/applied-by-ai-classification-reason/__tests__/AppliedByAiClassificationReason.test.tsx +105 -0
- package/src/features/classification/applied-by-ai-classification-reason/messages.ts +18 -0
- 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
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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);
|
|
@@ -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 };
|
|
@@ -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;
|