box-ui-elements 24.0.0-beta.4 → 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/content-explorer/Content.js +2 -1
- package/es/elements/content-explorer/Content.js.map +1 -1
- package/es/elements/content-explorer/ContentExplorer.js +19 -5
- 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/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/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/i18n/bn-IN.js +4 -0
- package/i18n/bn-IN.properties +4 -0
- package/i18n/da-DK.js +4 -0
- package/i18n/da-DK.properties +4 -0
- package/i18n/de-DE.js +5 -1
- package/i18n/de-DE.properties +4 -0
- package/i18n/en-AU.js +4 -0
- package/i18n/en-AU.properties +4 -0
- package/i18n/en-CA.js +4 -0
- package/i18n/en-CA.properties +4 -0
- package/i18n/en-GB.js +4 -0
- package/i18n/en-GB.properties +4 -0
- package/i18n/en-US.js +4 -0
- package/i18n/en-US.properties +8 -0
- package/i18n/en-x-pseudo.js +4 -0
- package/i18n/es-419.js +5 -1
- package/i18n/es-419.properties +4 -0
- package/i18n/es-ES.js +5 -1
- package/i18n/es-ES.properties +4 -0
- package/i18n/fi-FI.js +4 -0
- package/i18n/fi-FI.properties +4 -0
- package/i18n/fr-CA.js +4 -0
- package/i18n/fr-CA.properties +4 -0
- package/i18n/fr-FR.js +4 -0
- package/i18n/fr-FR.properties +4 -0
- package/i18n/hi-IN.js +4 -0
- package/i18n/hi-IN.properties +4 -0
- package/i18n/it-IT.js +4 -0
- package/i18n/it-IT.properties +4 -0
- package/i18n/ja-JP.js +6 -2
- package/i18n/ja-JP.properties +6 -2
- package/i18n/ko-KR.js +4 -0
- package/i18n/ko-KR.properties +4 -0
- package/i18n/nb-NO.js +4 -0
- package/i18n/nb-NO.properties +4 -0
- package/i18n/nl-NL.js +4 -0
- package/i18n/nl-NL.properties +4 -0
- package/i18n/pl-PL.js +4 -0
- package/i18n/pl-PL.properties +4 -0
- package/i18n/pt-BR.js +4 -0
- package/i18n/pt-BR.properties +4 -0
- package/i18n/ru-RU.js +5 -1
- package/i18n/ru-RU.properties +4 -0
- package/i18n/sv-SE.js +4 -0
- package/i18n/sv-SE.properties +4 -0
- package/i18n/tr-TR.js +5 -1
- package/i18n/tr-TR.properties +4 -0
- package/i18n/zh-CN.js +4 -0
- package/i18n/zh-CN.properties +4 -0
- package/i18n/zh-TW.js +4 -0
- package/i18n/zh-TW.properties +4 -0
- 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/messages.js +25 -0
- package/src/elements/content-explorer/Content.tsx +1 -0
- package/src/elements/content-explorer/ContentExplorer.tsx +220 -181
- 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/MetadataView-visual.stories.tsx +54 -8
- package/src/elements/content-explorer/utils.ts +150 -13
|
@@ -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
|
});
|
|
@@ -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`, () => {
|