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,8 +3,10 @@ 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
|
|
6
|
+
import type { MetadataTemplateField } from '@box/metadata-editor';
|
|
7
|
+
import type { MetadataFieldType } from '@box/metadata-view';
|
|
7
8
|
import API from '../../api';
|
|
9
|
+
import { areFieldValuesEqual, isEmptyValue, isMultiValuesField } from './utils';
|
|
8
10
|
|
|
9
11
|
import {
|
|
10
12
|
JSON_PATCH_OP_ADD,
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
METADATA_FIELD_TYPE_ENUM,
|
|
15
17
|
METADATA_FIELD_TYPE_MULTISELECT,
|
|
16
18
|
} from '../../common/constants';
|
|
17
|
-
import { FIELD_NAME, FIELD_METADATA, FIELD_EXTENSION } from '../../constants';
|
|
19
|
+
import { FIELD_NAME, FIELD_METADATA, FIELD_EXTENSION, FIELD_PERMISSIONS } from '../../constants';
|
|
18
20
|
|
|
19
21
|
import type { MetadataQuery as MetadataQueryType, MetadataQueryResponseData } from '../../common/types/metadataQueries';
|
|
20
22
|
import type {
|
|
@@ -57,13 +59,18 @@ export default class MetadataQueryAPIHelper {
|
|
|
57
59
|
oldValue: MetadataFieldValue | null,
|
|
58
60
|
newValue: MetadataFieldValue | null,
|
|
59
61
|
): JSONPatchOperations => {
|
|
62
|
+
// check if two values are the same, return empty operations if so
|
|
63
|
+
if (areFieldValuesEqual(oldValue, newValue)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
let operation = JSON_PATCH_OP_REPLACE;
|
|
61
68
|
|
|
62
|
-
if (
|
|
69
|
+
if (isEmptyValue(oldValue) && !isEmptyValue(newValue)) {
|
|
63
70
|
operation = JSON_PATCH_OP_ADD;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
if (oldValue &&
|
|
73
|
+
if (!isEmptyValue(oldValue) && isEmptyValue(newValue)) {
|
|
67
74
|
operation = JSON_PATCH_OP_REMOVE;
|
|
68
75
|
}
|
|
69
76
|
|
|
@@ -170,6 +177,51 @@ export default class MetadataQueryAPIHelper {
|
|
|
170
177
|
return this.api.getMetadataAPI(true).getSchemaByTemplateKey(this.templateKey);
|
|
171
178
|
};
|
|
172
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Generate operations for all fields update in the metadata sidepanel
|
|
182
|
+
*
|
|
183
|
+
* @private
|
|
184
|
+
* @return {JSONPatchOperations}
|
|
185
|
+
*/
|
|
186
|
+
generateOperations = (
|
|
187
|
+
item: BoxItem,
|
|
188
|
+
templateOldFields: MetadataTemplateField[],
|
|
189
|
+
templateNewFields: MetadataTemplateField[],
|
|
190
|
+
): JSONPatchOperations => {
|
|
191
|
+
const { scope, templateKey } = this.metadataTemplate;
|
|
192
|
+
const itemFields = item.metadata[scope][templateKey];
|
|
193
|
+
const operations = templateNewFields.flatMap(newField => {
|
|
194
|
+
let newFieldValue = newField.value;
|
|
195
|
+
const { key, type } = newField;
|
|
196
|
+
// when retrieve value from float type field, it gives a string instead
|
|
197
|
+
if (type === 'float' && newFieldValue !== '') {
|
|
198
|
+
newFieldValue = Number(newFieldValue);
|
|
199
|
+
}
|
|
200
|
+
const oldField = templateOldFields.find(f => f.key === key);
|
|
201
|
+
const oldFieldValue = oldField.value;
|
|
202
|
+
|
|
203
|
+
/*
|
|
204
|
+
Generate operations array based on all the fields' orignal value and the incoming updated value.
|
|
205
|
+
|
|
206
|
+
Edge Case:
|
|
207
|
+
If there are multiple items shared different value for enum or multi-select field, the form will
|
|
208
|
+
return 'Multiple values' as the value. In this case, it needs to generate operation based on the
|
|
209
|
+
actual item's field value.
|
|
210
|
+
*/
|
|
211
|
+
const shouldUseItemFieldValue =
|
|
212
|
+
isMultiValuesField(type as MetadataFieldType, oldFieldValue) &&
|
|
213
|
+
!isMultiValuesField(type as MetadataFieldType, newFieldValue);
|
|
214
|
+
|
|
215
|
+
return this.createJSONPatchOperations(
|
|
216
|
+
key,
|
|
217
|
+
shouldUseItemFieldValue ? itemFields[key] : oldFieldValue,
|
|
218
|
+
newFieldValue,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return operations;
|
|
223
|
+
};
|
|
224
|
+
|
|
173
225
|
queryMetadata = (): Promise<MetadataQueryResponseData> => {
|
|
174
226
|
return new Promise((resolve, reject) => {
|
|
175
227
|
this.api.getMetadataQueryAPI().queryMetadata(this.metadataQuery, resolve, reject, { forceFetch: true });
|
|
@@ -205,6 +257,34 @@ export default class MetadataQueryAPIHelper {
|
|
|
205
257
|
.updateMetadata(file, this.metadataTemplate, operations, successCallback, errorCallback);
|
|
206
258
|
};
|
|
207
259
|
|
|
260
|
+
updateMetadataWithOperations = (
|
|
261
|
+
item: BoxItem,
|
|
262
|
+
operations: JSONPatchOperations,
|
|
263
|
+
successCallback: () => void,
|
|
264
|
+
errorCallback: ErrorCallback,
|
|
265
|
+
): Promise<void> => {
|
|
266
|
+
return this.api
|
|
267
|
+
.getMetadataAPI(true)
|
|
268
|
+
.updateMetadata(item, this.metadataTemplate, operations, successCallback, errorCallback);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
bulkUpdateMetadata = (
|
|
272
|
+
items: BoxItem[],
|
|
273
|
+
templateOldFields: MetadataTemplateField[],
|
|
274
|
+
templateNewFields: MetadataTemplateField[],
|
|
275
|
+
successCallback: () => void,
|
|
276
|
+
errorCallback: ErrorCallback,
|
|
277
|
+
): Promise<void> => {
|
|
278
|
+
const operations: JSONPatchOperations = [];
|
|
279
|
+
items.forEach(item => {
|
|
280
|
+
const operation = this.generateOperations(item, templateOldFields, templateNewFields);
|
|
281
|
+
operations.push(operation);
|
|
282
|
+
});
|
|
283
|
+
return this.api
|
|
284
|
+
.getMetadataAPI(true)
|
|
285
|
+
.bulkUpdateMetadata(items, this.metadataTemplate, operations, successCallback, errorCallback);
|
|
286
|
+
};
|
|
287
|
+
|
|
208
288
|
/**
|
|
209
289
|
* Verify that the metadata query has required fields and update it if necessary
|
|
210
290
|
* For a file item, default fields included in the response are "type", "id", "etag"
|
|
@@ -225,6 +305,11 @@ export default class MetadataQueryAPIHelper {
|
|
|
225
305
|
clonedFields.push(FIELD_EXTENSION);
|
|
226
306
|
}
|
|
227
307
|
|
|
308
|
+
// This field is necessary to check if the user has permission to update metadata
|
|
309
|
+
if (!clonedFields.includes(FIELD_PERMISSIONS)) {
|
|
310
|
+
clonedFields.push(FIELD_PERMISSIONS);
|
|
311
|
+
}
|
|
312
|
+
|
|
228
313
|
clonedQuery.fields = clonedFields;
|
|
229
314
|
|
|
230
315
|
return clonedQuery;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { useIntl } from 'react-intl';
|
|
3
3
|
|
|
4
|
-
import { IconButton, SidePanel, Text } from '@box/blueprint-web';
|
|
4
|
+
import { IconButton, SidePanel, Text, useNotification } from '@box/blueprint-web';
|
|
5
5
|
import { XMark } from '@box/blueprint-web-assets/icons/Fill/index';
|
|
6
6
|
import { FileDefault } from '@box/blueprint-web-assets/icons/Line/index';
|
|
7
7
|
import {
|
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
JSONPatchOperations,
|
|
11
11
|
MetadataInstance,
|
|
12
12
|
MetadataInstanceForm,
|
|
13
|
+
type MetadataTemplateField,
|
|
13
14
|
} from '@box/metadata-editor';
|
|
14
15
|
|
|
15
16
|
import type { Selection } from 'react-aria-components';
|
|
16
|
-
import type { Collection } from '../../common/types/core';
|
|
17
|
+
import type { BoxItem, Collection } from '../../common/types/core';
|
|
17
18
|
import type { MetadataTemplate } from '../../common/types/metadata';
|
|
18
|
-
import {
|
|
19
|
+
import { useTemplateInstance, useSelectedItemText } from './utils';
|
|
19
20
|
|
|
20
21
|
import messages from '../common/messages';
|
|
21
22
|
|
|
@@ -23,17 +24,29 @@ import './MetadataSidePanel.scss';
|
|
|
23
24
|
|
|
24
25
|
export interface MetadataSidePanelProps {
|
|
25
26
|
currentCollection: Collection;
|
|
26
|
-
onClose: () => void;
|
|
27
27
|
metadataTemplate: MetadataTemplate;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
onUpdate: (
|
|
30
|
+
items: BoxItem[],
|
|
31
|
+
operations: JSONPatchOperations,
|
|
32
|
+
templateOldFields: MetadataTemplateField[],
|
|
33
|
+
templateNewFields: MetadataTemplateField[],
|
|
34
|
+
successCallback: () => void,
|
|
35
|
+
errorCallback: ErrorCallback,
|
|
36
|
+
) => Promise<void>;
|
|
37
|
+
refreshCollection: () => void;
|
|
28
38
|
selectedItemIds: Selection;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
const MetadataSidePanel = ({
|
|
32
42
|
currentCollection,
|
|
43
|
+
metadataTemplate,
|
|
33
44
|
onClose,
|
|
45
|
+
onUpdate,
|
|
46
|
+
refreshCollection,
|
|
34
47
|
selectedItemIds,
|
|
35
|
-
metadataTemplate,
|
|
36
48
|
}: MetadataSidePanelProps) => {
|
|
49
|
+
const { addNotification } = useNotification();
|
|
37
50
|
const { formatMessage } = useIntl();
|
|
38
51
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
|
39
52
|
const [isUnsavedChangesModalOpen, setIsUnsavedChangesModalOpen] = useState<boolean>(false);
|
|
@@ -43,7 +56,7 @@ const MetadataSidePanel = ({
|
|
|
43
56
|
selectedItemIds === 'all'
|
|
44
57
|
? currentCollection.items
|
|
45
58
|
: currentCollection.items.filter(item => selectedItemIds.has(item.id));
|
|
46
|
-
const templateInstance =
|
|
59
|
+
const templateInstance = useTemplateInstance(metadataTemplate, selectedItems, isEditing);
|
|
47
60
|
|
|
48
61
|
const handleMetadataInstanceEdit = () => {
|
|
49
62
|
setIsEditing(true);
|
|
@@ -53,19 +66,47 @@ const MetadataSidePanel = ({
|
|
|
53
66
|
setIsEditing(false);
|
|
54
67
|
};
|
|
55
68
|
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
57
|
-
const handleMetadataInstanceFormChange = (values: FormValues) => {
|
|
58
|
-
// TODO: Implement on form change
|
|
59
|
-
};
|
|
60
|
-
|
|
61
69
|
const handleMetadataInstanceFormDiscardUnsavedChanges = () => {
|
|
62
70
|
setIsUnsavedChangesModalOpen(false);
|
|
63
71
|
setIsEditing(false);
|
|
64
72
|
};
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
const handleUpdateMetadataSuccess = () => {
|
|
75
|
+
addNotification({
|
|
76
|
+
closeButtonAriaLabel: formatMessage(messages.close),
|
|
77
|
+
sensitivity: 'foreground',
|
|
78
|
+
styledText: formatMessage(messages.metadataUpdateSuccessNotification, {
|
|
79
|
+
numSelected: selectedItems.length,
|
|
80
|
+
}),
|
|
81
|
+
typeIconAriaLabel: formatMessage(messages.success),
|
|
82
|
+
variant: 'success',
|
|
83
|
+
});
|
|
84
|
+
setIsEditing(false);
|
|
85
|
+
refreshCollection();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleUpdateMetadataError = () => {
|
|
89
|
+
addNotification({
|
|
90
|
+
closeButtonAriaLabel: formatMessage(messages.close),
|
|
91
|
+
sensitivity: 'foreground',
|
|
92
|
+
styledText: formatMessage(messages.metadataUpdateErrorNotification),
|
|
93
|
+
typeIconAriaLabel: formatMessage(messages.error),
|
|
94
|
+
variant: 'error',
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
67
98
|
const handleMetadataInstanceFormSubmit = async (values: FormValues, operations: JSONPatchOperations) => {
|
|
68
|
-
|
|
99
|
+
const { fields: templateNewFields } = values.metadata;
|
|
100
|
+
const { fields: templateOldFields } = templateInstance;
|
|
101
|
+
|
|
102
|
+
await onUpdate(
|
|
103
|
+
selectedItems,
|
|
104
|
+
operations,
|
|
105
|
+
templateOldFields,
|
|
106
|
+
templateNewFields,
|
|
107
|
+
handleUpdateMetadataSuccess,
|
|
108
|
+
handleUpdateMetadataError,
|
|
109
|
+
);
|
|
69
110
|
};
|
|
70
111
|
|
|
71
112
|
return (
|
|
@@ -99,7 +140,7 @@ const MetadataSidePanel = ({
|
|
|
99
140
|
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
|
|
100
141
|
selectedTemplateInstance={templateInstance}
|
|
101
142
|
onCancel={handleMetadataInstanceFormCancel}
|
|
102
|
-
onChange={
|
|
143
|
+
onChange={null}
|
|
103
144
|
onDelete={null}
|
|
104
145
|
onDiscardUnsavedChanges={handleMetadataInstanceFormDiscardUnsavedChanges}
|
|
105
146
|
onSubmit={handleMetadataInstanceFormSubmit}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { EnumType, FloatType, MetadataFormFieldValue, RangeType } from '@box/metadata-filter';
|
|
3
3
|
import { MetadataView, type MetadataViewProps } from '@box/metadata-view';
|
|
4
|
+
import { type Key } from '@react-types/shared';
|
|
4
5
|
|
|
6
|
+
import { SortDescriptor } from 'react-aria-components';
|
|
5
7
|
import type { Collection } from '../../common/types/core';
|
|
6
8
|
import type { MetadataTemplate } from '../../common/types/metadata';
|
|
7
9
|
|
|
@@ -25,6 +27,21 @@ type ActionBarProps = Omit<
|
|
|
25
27
|
onFilterSubmit?: (filterValues: ExternalFilterValues) => void;
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Helper function to trim metadataFieldNamePrefix from column names
|
|
32
|
+
* For example: 'metadata.enterprise_1515946.mdViewTemplate1.industry' -> 'industry'
|
|
33
|
+
*/
|
|
34
|
+
function trimMetadataFieldPrefix(column: string): string {
|
|
35
|
+
// Check if the column starts with 'metadata.' and contains at least 2 dots
|
|
36
|
+
if (column.startsWith('metadata.') && column.split('.').length >= 3) {
|
|
37
|
+
// Split by dots and take everything after the first 3 parts
|
|
38
|
+
// metadata.enterprise_1515946.mdViewTemplate1.industry -> industry
|
|
39
|
+
const parts = column.split('.');
|
|
40
|
+
return parts.slice(3).join('.');
|
|
41
|
+
}
|
|
42
|
+
return column;
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
function transformInitialFilterValuesToInternal(
|
|
29
46
|
publicValues?: ExternalFilterValues,
|
|
30
47
|
): Record<string, { value: MetadataFormFieldValue }> | undefined {
|
|
@@ -55,6 +72,8 @@ export interface MetadataViewContainerProps extends Omit<MetadataViewProps, 'ite
|
|
|
55
72
|
actionBarProps?: ActionBarProps;
|
|
56
73
|
currentCollection: Collection;
|
|
57
74
|
metadataTemplate: MetadataTemplate;
|
|
75
|
+
/* Internally controlled onSortChange prop for the MetadataView component. */
|
|
76
|
+
onSortChange?: (sortBy: Key, sortDirection: string) => void;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
const MetadataViewContainer = ({
|
|
@@ -62,6 +81,7 @@ const MetadataViewContainer = ({
|
|
|
62
81
|
columns,
|
|
63
82
|
currentCollection,
|
|
64
83
|
metadataTemplate,
|
|
84
|
+
onSortChange: onSortChangeInternal,
|
|
65
85
|
...rest
|
|
66
86
|
}: MetadataViewContainerProps) => {
|
|
67
87
|
const { items = [] } = currentCollection;
|
|
@@ -111,7 +131,47 @@ const MetadataViewContainer = ({
|
|
|
111
131
|
};
|
|
112
132
|
}, [actionBarProps, initialFilterValues, onFilterSubmit, filterGroups]);
|
|
113
133
|
|
|
114
|
-
|
|
134
|
+
// Extract the original tableProps.onSortChange from rest
|
|
135
|
+
const { tableProps, ...otherRest } = rest;
|
|
136
|
+
const onSortChangeExternal = tableProps?.onSortChange;
|
|
137
|
+
|
|
138
|
+
// Create a wrapper function that calls both. The wrapper function should follow the signature of onSortChange from RAC
|
|
139
|
+
const handleSortChange = React.useCallback(
|
|
140
|
+
({ column, direction }: SortDescriptor) => {
|
|
141
|
+
// Call the internal onSortChange first
|
|
142
|
+
// API accepts asc/desc "https://developer.box.com/reference/post-metadata-queries-execute-read/"
|
|
143
|
+
if (onSortChangeInternal) {
|
|
144
|
+
const trimmedColumn = trimMetadataFieldPrefix(String(column));
|
|
145
|
+
onSortChangeInternal(trimmedColumn, direction === 'ascending' ? 'ASC' : 'DESC');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Then call the original customer-provided onSortChange if it exists
|
|
149
|
+
// Accepts "ascending" / "descending" (https://react-spectrum.adobe.com/react-aria/Table.html)
|
|
150
|
+
if (onSortChangeExternal) {
|
|
151
|
+
onSortChangeExternal({
|
|
152
|
+
column,
|
|
153
|
+
direction,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[onSortChangeInternal, onSortChangeExternal],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Create new tableProps with our wrapper function
|
|
161
|
+
const newTableProps = {
|
|
162
|
+
...tableProps,
|
|
163
|
+
onSortChange: handleSortChange,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<MetadataView
|
|
168
|
+
actionBarProps={transformedActionBarProps}
|
|
169
|
+
columns={columns}
|
|
170
|
+
items={items}
|
|
171
|
+
tableProps={newTableProps}
|
|
172
|
+
{...otherRest}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
115
175
|
};
|
|
116
176
|
|
|
117
177
|
export default MetadataViewContainer;
|
|
@@ -454,7 +454,7 @@ describe('elements/content-explorer/ContentExplorer', () => {
|
|
|
454
454
|
textValue: 'Name',
|
|
455
455
|
id: 'name',
|
|
456
456
|
type: 'string' as const,
|
|
457
|
-
|
|
457
|
+
allowsSorting: true,
|
|
458
458
|
minWidth: 150,
|
|
459
459
|
maxWidth: 150,
|
|
460
460
|
},
|
|
@@ -462,7 +462,7 @@ describe('elements/content-explorer/ContentExplorer', () => {
|
|
|
462
462
|
textValue: field.displayName,
|
|
463
463
|
id: `${metadataFieldNamePrefix}.${field.key}`,
|
|
464
464
|
type: field.type as MetadataFieldType,
|
|
465
|
-
|
|
465
|
+
allowsSorting: true,
|
|
466
466
|
minWidth: 150,
|
|
467
467
|
maxWidth: 150,
|
|
468
468
|
})),
|
|
@@ -506,6 +506,40 @@ describe('elements/content-explorer/ContentExplorer', () => {
|
|
|
506
506
|
expect(screen.getByRole('button', { name: 'Metadata' })).toBeInTheDocument();
|
|
507
507
|
});
|
|
508
508
|
|
|
509
|
+
test('should call both internal and user onSortChange callbacks when sorting by a metadata field', async () => {
|
|
510
|
+
const mockOnSortChangeInternal = jest.fn();
|
|
511
|
+
const mockOnSortChangeExternal = jest.fn();
|
|
512
|
+
|
|
513
|
+
renderComponent({
|
|
514
|
+
...metadataViewV2ElementProps,
|
|
515
|
+
metadataViewProps: {
|
|
516
|
+
...metadataViewV2ElementProps.metadataViewProps,
|
|
517
|
+
onSortChange: mockOnSortChangeInternal, // Internal callback - receives trimmed column name
|
|
518
|
+
tableProps: {
|
|
519
|
+
...metadataViewV2ElementProps.metadataViewProps.tableProps,
|
|
520
|
+
onSortChange: mockOnSortChangeExternal, // User callback - receives full column ID
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const industryHeader = await screen.findByRole('columnheader', { name: 'Industry' });
|
|
526
|
+
expect(industryHeader).toBeInTheDocument();
|
|
527
|
+
|
|
528
|
+
const firstRow = await screen.findByRole('row', { name: /Child 2/i });
|
|
529
|
+
expect(firstRow).toBeInTheDocument();
|
|
530
|
+
|
|
531
|
+
await userEvent.click(industryHeader);
|
|
532
|
+
|
|
533
|
+
// Internal callback gets trimmed version for API calls
|
|
534
|
+
expect(mockOnSortChangeInternal).toHaveBeenCalledWith('industry', 'ASC');
|
|
535
|
+
|
|
536
|
+
// User callback gets full column ID with direction
|
|
537
|
+
expect(mockOnSortChangeExternal).toHaveBeenCalledWith({
|
|
538
|
+
column: 'metadata.enterprise_0.templateName.industry',
|
|
539
|
+
direction: 'ascending',
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
509
543
|
test('should call onClick when bulk item action is clicked', async () => {
|
|
510
544
|
let mockOnClickArg;
|
|
511
545
|
const mockOnClick = jest.fn(arg => {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
JSON_PATCH_OP_REPLACE,
|
|
9
9
|
JSON_PATCH_OP_TEST,
|
|
10
10
|
} from '../../../common/constants';
|
|
11
|
-
import { FIELD_METADATA, FIELD_NAME, FIELD_EXTENSION } from '../../../constants';
|
|
11
|
+
import { FIELD_METADATA, FIELD_NAME, FIELD_EXTENSION, FIELD_PERMISSIONS } from '../../../constants';
|
|
12
12
|
|
|
13
13
|
describe('features/metadata-based-view/MetadataQueryAPIHelper', () => {
|
|
14
14
|
let metadataQueryAPIHelper;
|
|
@@ -426,27 +426,30 @@ describe('features/metadata-based-view/MetadataQueryAPIHelper', () => {
|
|
|
426
426
|
expect(isArray(updatedMetadataQuery.fields)).toBe(true);
|
|
427
427
|
expect(includes(updatedMetadataQuery.fields, FIELD_NAME)).toBe(true);
|
|
428
428
|
expect(includes(updatedMetadataQuery.fields, FIELD_EXTENSION)).toBe(true);
|
|
429
|
+
expect(includes(updatedMetadataQuery.fields, FIELD_PERMISSIONS)).toBe(true);
|
|
429
430
|
|
|
430
431
|
if (index === 2) {
|
|
431
|
-
// Verify "name" and "
|
|
432
|
+
// Verify "name", "extension" and "permission" are added to pre-existing fields
|
|
432
433
|
expect(updatedMetadataQuery.fields).toEqual([
|
|
433
434
|
...mdQueryWithoutNameField.fields,
|
|
434
435
|
FIELD_NAME,
|
|
435
436
|
FIELD_EXTENSION,
|
|
437
|
+
FIELD_PERMISSIONS,
|
|
436
438
|
]);
|
|
437
439
|
}
|
|
438
440
|
|
|
439
441
|
if (index === 4) {
|
|
440
|
-
// Verify "extension"
|
|
442
|
+
// Verify "extension" and "permission" are added when "name" exists but "extension" and "permission" don't
|
|
441
443
|
expect(updatedMetadataQuery.fields).toEqual([
|
|
442
444
|
...mdQueryWithoutExtensionField.fields,
|
|
443
445
|
FIELD_EXTENSION,
|
|
446
|
+
FIELD_PERMISSIONS,
|
|
444
447
|
]);
|
|
445
448
|
}
|
|
446
449
|
|
|
447
450
|
if (index === 5) {
|
|
448
|
-
//
|
|
449
|
-
expect(updatedMetadataQuery.fields).toEqual(mdQueryWithBothFields.fields);
|
|
451
|
+
// Verify "permission" is added
|
|
452
|
+
expect(updatedMetadataQuery.fields).toEqual([...mdQueryWithBothFields.fields, FIELD_PERMISSIONS]);
|
|
450
453
|
}
|
|
451
454
|
},
|
|
452
455
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import {
|
|
3
|
+
import { Notification } from '@box/blueprint-web';
|
|
4
|
+
import { render, screen, waitFor } from '../../../test-utils/testing-library';
|
|
4
5
|
import MetadataSidePanel, { type MetadataSidePanelProps } from '../MetadataSidePanel';
|
|
5
6
|
|
|
6
7
|
// Mock scrollTo method
|
|
@@ -65,13 +66,20 @@ const mockOnClose = jest.fn();
|
|
|
65
66
|
describe('elements/content-explorer/MetadataSidePanel', () => {
|
|
66
67
|
const defaultProps: MetadataSidePanelProps = {
|
|
67
68
|
currentCollection: mockCollection,
|
|
68
|
-
onClose: mockOnClose,
|
|
69
69
|
metadataTemplate: mockMetadataTemplate,
|
|
70
|
+
onClose: mockOnClose,
|
|
71
|
+
onUpdate: jest.fn(),
|
|
72
|
+
refreshCollection: jest.fn(),
|
|
70
73
|
selectedItemIds: new Set(['1']),
|
|
71
74
|
};
|
|
72
75
|
|
|
73
76
|
const renderComponent = (props: Partial<MetadataSidePanelProps> = {}) =>
|
|
74
|
-
render(
|
|
77
|
+
render(
|
|
78
|
+
<Notification.Provider>
|
|
79
|
+
<Notification.Viewport />
|
|
80
|
+
<MetadataSidePanel {...defaultProps} {...props} />
|
|
81
|
+
</Notification.Provider>,
|
|
82
|
+
);
|
|
75
83
|
|
|
76
84
|
test('renders the metadata title', () => {
|
|
77
85
|
renderComponent();
|
|
@@ -124,4 +132,138 @@ describe('elements/content-explorer/MetadataSidePanel', () => {
|
|
|
124
132
|
const submitButton = screen.getByRole('button', { name: 'Save' });
|
|
125
133
|
expect(submitButton).toBeInTheDocument();
|
|
126
134
|
});
|
|
135
|
+
|
|
136
|
+
test('switches back to view mode when cancel button is clicked', async () => {
|
|
137
|
+
renderComponent();
|
|
138
|
+
const editTemplateButton = screen.getByLabelText('Edit Mock Template');
|
|
139
|
+
await userEvent.click(editTemplateButton);
|
|
140
|
+
|
|
141
|
+
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
|
142
|
+
await userEvent.click(cancelButton);
|
|
143
|
+
|
|
144
|
+
// Should be back in view mode
|
|
145
|
+
expect(screen.getByLabelText('Edit Mock Template')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('calls onUpdate when form is submitted for single item', async () => {
|
|
149
|
+
const mockUpdateMetadata = jest.fn().mockResolvedValue(undefined);
|
|
150
|
+
renderComponent({ onUpdate: mockUpdateMetadata });
|
|
151
|
+
|
|
152
|
+
const editTemplateButton = screen.getByLabelText('Edit Mock Template');
|
|
153
|
+
await userEvent.click(editTemplateButton);
|
|
154
|
+
|
|
155
|
+
const submitButton = screen.getByRole('button', { name: 'Save' });
|
|
156
|
+
await userEvent.click(submitButton);
|
|
157
|
+
|
|
158
|
+
expect(mockUpdateMetadata).toHaveBeenCalledWith(
|
|
159
|
+
[mockCollection.items[0]],
|
|
160
|
+
expect.any(Array),
|
|
161
|
+
expect.any(Array),
|
|
162
|
+
expect.any(Array),
|
|
163
|
+
expect.any(Function),
|
|
164
|
+
expect.any(Function),
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('calls onUpdate when multiple items are selected', async () => {
|
|
169
|
+
const mockUpdateMetadata = jest.fn().mockResolvedValue(undefined);
|
|
170
|
+
|
|
171
|
+
renderComponent({
|
|
172
|
+
selectedItemIds: new Set(['1', '2']),
|
|
173
|
+
onUpdate: mockUpdateMetadata,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const editTemplateButton = screen.getByLabelText('Edit Mock Template');
|
|
177
|
+
await userEvent.click(editTemplateButton);
|
|
178
|
+
|
|
179
|
+
const submitButton = screen.getByRole('button', { name: 'Save' });
|
|
180
|
+
await userEvent.click(submitButton);
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(mockUpdateMetadata).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('displays success notification when metadata update succeeds', async () => {
|
|
188
|
+
const mockUpdateMetadata = jest.fn().mockImplementation((_, __, ___, ____, successCallback) => {
|
|
189
|
+
successCallback();
|
|
190
|
+
return Promise.resolve();
|
|
191
|
+
});
|
|
192
|
+
const mockRefreshCollection = jest.fn();
|
|
193
|
+
|
|
194
|
+
renderComponent({
|
|
195
|
+
onUpdate: mockUpdateMetadata,
|
|
196
|
+
refreshCollection: mockRefreshCollection,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const editTemplateButton = screen.getByLabelText('Edit Mock Template');
|
|
200
|
+
await userEvent.click(editTemplateButton);
|
|
201
|
+
|
|
202
|
+
const submitButton = screen.getByRole('button', { name: 'Save' });
|
|
203
|
+
await userEvent.click(submitButton);
|
|
204
|
+
|
|
205
|
+
expect(screen.getByText('1 document updated')).toBeInTheDocument();
|
|
206
|
+
expect(mockRefreshCollection).toHaveBeenCalledTimes(1);
|
|
207
|
+
expect(screen.getByLabelText('Edit Mock Template')).toBeInTheDocument(); // Back to view mode
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('displays error notification when metadata update fails', async () => {
|
|
211
|
+
const mockUpdateMetadata = jest.fn().mockImplementation((_, __, ___, ____, _____, errorCallback) => {
|
|
212
|
+
errorCallback();
|
|
213
|
+
return Promise.resolve();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
renderComponent({ onUpdate: mockUpdateMetadata });
|
|
217
|
+
|
|
218
|
+
const editTemplateButton = screen.getByLabelText('Edit Mock Template');
|
|
219
|
+
await userEvent.click(editTemplateButton);
|
|
220
|
+
|
|
221
|
+
const submitButton = screen.getByRole('button', { name: 'Save' });
|
|
222
|
+
await userEvent.click(submitButton);
|
|
223
|
+
|
|
224
|
+
await waitFor(() => {
|
|
225
|
+
expect(screen.getByText('Unable to save changes. Please try again.')).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('handles "all" selection correctly', () => {
|
|
230
|
+
renderComponent({ selectedItemIds: 'all' });
|
|
231
|
+
const subtitle = screen.getByText('2 files selected');
|
|
232
|
+
expect(subtitle).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('displays "Multiple Values" for items with different field values', () => {
|
|
236
|
+
const collectionWithDifferentValues = {
|
|
237
|
+
...mockCollection,
|
|
238
|
+
items: [
|
|
239
|
+
{
|
|
240
|
+
...mockCollection.items[0],
|
|
241
|
+
metadata: {
|
|
242
|
+
enterprise_123: {
|
|
243
|
+
mockTemplate: {
|
|
244
|
+
alias: 'value-1',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
...mockCollection.items[1],
|
|
251
|
+
metadata: {
|
|
252
|
+
enterprise_123: {
|
|
253
|
+
mockTemplate: {
|
|
254
|
+
alias: 'value-2',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
renderComponent({
|
|
263
|
+
currentCollection: collectionWithDifferentValues,
|
|
264
|
+
selectedItemIds: new Set(['1', '2']),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(screen.getByText('Multiple Values')).toBeInTheDocument();
|
|
268
|
+
});
|
|
127
269
|
});
|
|
@@ -28,6 +28,12 @@ export const basic = {
|
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
export const Modernization = {
|
|
32
|
+
args: {
|
|
33
|
+
enableModernizedComponents: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
31
37
|
export const openExistingFolder = {
|
|
32
38
|
play: async ({ canvasElement }) => {
|
|
33
39
|
const canvas = within(canvasElement);
|