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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/explorer.css +1 -1
  2. package/dist/explorer.js +1 -1
  3. package/dist/openwith.js +1 -1
  4. package/dist/picker.js +1 -1
  5. package/dist/preview.js +1 -1
  6. package/dist/sharing.js +1 -1
  7. package/dist/sidebar.js +1 -1
  8. package/dist/uploader.js +1 -1
  9. package/es/api/Metadata.js +98 -13
  10. package/es/api/Metadata.js.flow +110 -12
  11. package/es/api/Metadata.js.map +1 -1
  12. package/es/elements/common/messages.js +16 -0
  13. package/es/elements/common/messages.js.flow +25 -0
  14. package/es/elements/common/messages.js.map +1 -1
  15. package/es/elements/content-explorer/Content.js +5 -2
  16. package/es/elements/content-explorer/Content.js.map +1 -1
  17. package/es/elements/content-explorer/ContentExplorer.js +31 -6
  18. package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
  19. package/es/elements/content-explorer/MetadataQueryAPIHelper.js +164 -10
  20. package/es/elements/content-explorer/MetadataQueryAPIHelper.js.map +1 -1
  21. package/es/elements/content-explorer/MetadataQueryBuilder.js +115 -0
  22. package/es/elements/content-explorer/MetadataQueryBuilder.js.map +1 -0
  23. package/es/elements/content-explorer/MetadataSidePanel.js +40 -14
  24. package/es/elements/content-explorer/MetadataSidePanel.js.map +1 -1
  25. package/es/elements/content-explorer/MetadataViewContainer.js +133 -36
  26. package/es/elements/content-explorer/MetadataViewContainer.js.map +1 -1
  27. package/es/elements/content-explorer/stories/MetadataView.stories.js +3 -25
  28. package/es/elements/content-explorer/stories/MetadataView.stories.js.map +1 -1
  29. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js +65 -29
  30. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js.map +1 -1
  31. package/es/elements/content-explorer/utils.js +140 -12
  32. package/es/elements/content-explorer/utils.js.map +1 -1
  33. package/es/src/elements/common/__mocks__/mockMetadata.d.ts +8 -24
  34. package/es/src/elements/content-explorer/Content.d.ts +4 -3
  35. package/es/src/elements/content-explorer/ContentExplorer.d.ts +19 -6
  36. package/es/src/elements/content-explorer/MetadataQueryAPIHelper.d.ts +22 -3
  37. package/es/src/elements/content-explorer/MetadataQueryBuilder.d.ts +27 -0
  38. package/es/src/elements/content-explorer/MetadataSidePanel.d.ts +6 -3
  39. package/es/src/elements/content-explorer/MetadataViewContainer.d.ts +10 -4
  40. package/es/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.d.ts +1 -0
  41. package/es/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.d.ts +1 -0
  42. package/es/src/elements/content-explorer/utils.d.ts +9 -3
  43. package/i18n/bn-IN.js +4 -0
  44. package/i18n/bn-IN.properties +12 -0
  45. package/i18n/da-DK.js +4 -0
  46. package/i18n/da-DK.properties +12 -0
  47. package/i18n/de-DE.js +5 -1
  48. package/i18n/de-DE.properties +12 -0
  49. package/i18n/en-AU.js +4 -0
  50. package/i18n/en-AU.properties +12 -0
  51. package/i18n/en-CA.js +4 -0
  52. package/i18n/en-CA.properties +12 -0
  53. package/i18n/en-GB.js +4 -0
  54. package/i18n/en-GB.properties +12 -0
  55. package/i18n/en-US.js +4 -0
  56. package/i18n/en-US.properties +8 -0
  57. package/i18n/en-x-pseudo.js +4 -0
  58. package/i18n/es-419.js +5 -1
  59. package/i18n/es-419.properties +12 -0
  60. package/i18n/es-ES.js +5 -1
  61. package/i18n/es-ES.properties +12 -0
  62. package/i18n/fi-FI.js +4 -0
  63. package/i18n/fi-FI.properties +12 -0
  64. package/i18n/fr-CA.js +4 -0
  65. package/i18n/fr-CA.properties +12 -0
  66. package/i18n/fr-FR.js +4 -0
  67. package/i18n/fr-FR.properties +12 -0
  68. package/i18n/hi-IN.js +4 -0
  69. package/i18n/hi-IN.properties +12 -0
  70. package/i18n/it-IT.js +4 -0
  71. package/i18n/it-IT.properties +12 -0
  72. package/i18n/ja-JP.js +6 -2
  73. package/i18n/ja-JP.properties +14 -2
  74. package/i18n/ko-KR.js +4 -0
  75. package/i18n/ko-KR.properties +12 -0
  76. package/i18n/nb-NO.js +4 -0
  77. package/i18n/nb-NO.properties +12 -0
  78. package/i18n/nl-NL.js +4 -0
  79. package/i18n/nl-NL.properties +12 -0
  80. package/i18n/pl-PL.js +4 -0
  81. package/i18n/pl-PL.properties +12 -0
  82. package/i18n/pt-BR.js +4 -0
  83. package/i18n/pt-BR.properties +12 -0
  84. package/i18n/ru-RU.js +5 -1
  85. package/i18n/ru-RU.properties +12 -0
  86. package/i18n/sv-SE.js +4 -0
  87. package/i18n/sv-SE.properties +12 -0
  88. package/i18n/tr-TR.js +5 -1
  89. package/i18n/tr-TR.properties +12 -0
  90. package/i18n/zh-CN.js +4 -0
  91. package/i18n/zh-CN.properties +12 -0
  92. package/i18n/zh-TW.js +4 -0
  93. package/i18n/zh-TW.properties +12 -0
  94. package/package.json +3 -3
  95. package/src/api/Metadata.js +110 -12
  96. package/src/api/__tests__/Metadata.test.js +120 -0
  97. package/src/elements/common/__mocks__/mockMetadata.ts +7 -11
  98. package/src/elements/common/messages.js +25 -0
  99. package/src/elements/content-explorer/Content.tsx +9 -2
  100. package/src/elements/content-explorer/ContentExplorer.tsx +71 -17
  101. package/src/elements/content-explorer/MetadataQueryAPIHelper.ts +199 -8
  102. package/src/elements/content-explorer/MetadataQueryBuilder.ts +159 -0
  103. package/src/elements/content-explorer/MetadataSidePanel.tsx +55 -14
  104. package/src/elements/content-explorer/MetadataViewContainer.tsx +164 -29
  105. package/src/elements/content-explorer/__tests__/Content.test.tsx +1 -0
  106. package/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +38 -7
  107. package/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +428 -12
  108. package/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts +419 -0
  109. package/src/elements/content-explorer/__tests__/MetadataSidePanel.test.tsx +145 -3
  110. package/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx +413 -9
  111. package/src/elements/content-explorer/stories/MetadataView.stories.tsx +3 -21
  112. package/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +56 -21
  113. package/src/elements/content-explorer/utils.ts +150 -13
@@ -3,18 +3,34 @@ import * as React from 'react';
3
3
  import type { Collection } from '../../../common/types/core';
4
4
  import type { MetadataTemplate, MetadataTemplateField } from '../../../common/types/metadata';
5
5
  import { render, screen, userEvent, waitFor, within } from '../../../test-utils/testing-library';
6
- import MetadataViewContainer, { type MetadataViewContainerProps } from '../MetadataViewContainer';
6
+ import MetadataViewContainer, {
7
+ type MetadataViewContainerProps,
8
+ convertFilterValuesToExternal,
9
+ type ExternalFilterValues,
10
+ } from '../MetadataViewContainer';
7
11
 
8
12
  describe('elements/content-explorer/MetadataViewContainer', () => {
9
13
  const mockItems = [
10
- { id: '1', name: 'File 1.txt', type: 'file' },
11
- { id: '2', name: 'File 2.pdf', type: 'file' },
14
+ {
15
+ id: '1',
16
+ name: 'File 1.txt',
17
+ type: 'file',
18
+ 'item.name': 'File 1.txt',
19
+ industry: 'tech',
20
+ },
21
+ {
22
+ id: '2',
23
+ name: 'File 2.pdf',
24
+ type: 'file',
25
+ 'item.name': 'File 2.pdf',
26
+ industry: 'finance',
27
+ },
12
28
  ];
13
29
 
14
30
  const mockMetadataTemplateFields: MetadataTemplateField[] = [
15
31
  {
16
32
  id: 'field1',
17
- key: ' name',
33
+ key: 'item.name',
18
34
  displayName: 'Name',
19
35
  type: 'string',
20
36
  },
@@ -28,6 +44,22 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
28
44
  { key: 'finance', id: 'finance1' },
29
45
  ],
30
46
  },
47
+ {
48
+ id: 'field3',
49
+ key: 'price',
50
+ displayName: 'Price',
51
+ type: 'float',
52
+ },
53
+ {
54
+ id: 'field4',
55
+ key: 'category',
56
+ displayName: 'Category',
57
+ type: 'multiSelect',
58
+ options: [
59
+ { key: 'category1', id: 'cat1' },
60
+ { key: 'category2', id: 'cat2' },
61
+ ],
62
+ },
31
63
  ];
32
64
 
33
65
  const mockMetadataTemplate: MetadataTemplate = {
@@ -49,7 +81,7 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
49
81
  columns: [
50
82
  {
51
83
  textValue: 'Name',
52
- id: 'name',
84
+ id: 'item.name',
53
85
  type: 'string',
54
86
  allowsSorting: true,
55
87
  minWidth: 250,
@@ -66,17 +98,22 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
66
98
  },
67
99
  ],
68
100
  metadataTemplate: mockMetadataTemplate,
101
+ onMetadataFilter: jest.fn(),
69
102
  };
70
103
 
71
104
  const renderComponent = (props: Partial<MetadataViewContainerProps> = {}) => {
72
105
  return render(<MetadataViewContainer {...defaultProps} {...props} />);
73
106
  };
74
107
 
108
+ beforeEach(() => {
109
+ jest.clearAllMocks();
110
+ });
111
+
75
112
  test('should render MetadataView component', () => {
76
113
  renderComponent();
77
114
 
78
115
  expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
79
- expect(screen.getByRole('button', { name: 'Name' })).toBeInTheDocument();
116
+ expect(screen.getAllByRole('button', { name: 'Name' })).toHaveLength(2); // One in filter bar, one in table header
80
117
  expect(screen.getByRole('button', { name: 'Industry' })).toBeInTheDocument();
81
118
  expect(screen.getByText('File 1.txt')).toBeInTheDocument();
82
119
  expect(screen.getByText('File 2.pdf')).toBeInTheDocument();
@@ -101,7 +138,11 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
101
138
  ],
102
139
  };
103
140
 
104
- renderComponent({ metadataTemplate: template, actionBarProps: { onFilterSubmit } });
141
+ renderComponent({
142
+ metadataTemplate: template,
143
+ actionBarProps: { onFilterSubmit },
144
+ onMetadataFilter: jest.fn(),
145
+ });
105
146
 
106
147
  await userEvent().click(screen.getByRole('button', { name: /Contact Role/ }));
107
148
  await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Developer' }));
@@ -112,7 +153,370 @@ describe('elements/content-explorer/MetadataViewContainer', () => {
112
153
  await waitFor(() => expect(onFilterSubmit).toHaveBeenCalledTimes(2));
113
154
  const firstCall = onFilterSubmit.mock.calls[0][0];
114
155
  const secondCall = onFilterSubmit.mock.calls[1][0];
115
- expect(firstCall['role-filter'].value).toEqual(['Developer']);
116
- expect(secondCall['role-filter'].value).toEqual(['Developer', 'Marketing']);
156
+
157
+ expect(firstCall.role.value).toEqual(['Developer']);
158
+ expect(secondCall.role.value).toEqual(['Developer', 'Marketing']);
159
+ });
160
+
161
+ test('should call onMetadataFilter and onFilterSubmit when filter is submitted', async () => {
162
+ const onFilterSubmit = jest.fn();
163
+ const onMetadataFilter = jest.fn();
164
+ const template: MetadataTemplate = {
165
+ ...mockMetadataTemplate,
166
+ fields: [
167
+ {
168
+ id: 'field1',
169
+ key: 'status',
170
+ displayName: 'Status',
171
+ type: 'enum',
172
+ options: [
173
+ { id: 's1', key: 'Active' },
174
+ { id: 's2', key: 'Inactive' },
175
+ ],
176
+ },
177
+ ],
178
+ };
179
+
180
+ renderComponent({
181
+ metadataTemplate: template,
182
+ actionBarProps: { onFilterSubmit },
183
+ onMetadataFilter,
184
+ });
185
+
186
+ await userEvent().click(screen.getByRole('button', { name: /Status/ }));
187
+ await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Active' }));
188
+
189
+ await waitFor(() => {
190
+ expect(onMetadataFilter).toHaveBeenCalledTimes(1);
191
+ expect(onFilterSubmit).toHaveBeenCalledTimes(1);
192
+ });
193
+
194
+ const filterCall = onMetadataFilter.mock.calls[0][0];
195
+ const submitCall = onFilterSubmit.mock.calls[0][0];
196
+
197
+ expect(filterCall.status.value).toEqual(['Active']);
198
+ expect(submitCall.status.value).toEqual(['Active']);
199
+ });
200
+
201
+ test('should only call onMetadataFilter when onFilterSubmit is not provided', async () => {
202
+ const onMetadataFilter = jest.fn();
203
+ const template: MetadataTemplate = {
204
+ ...mockMetadataTemplate,
205
+ fields: [
206
+ {
207
+ id: 'field1',
208
+ key: 'status',
209
+ displayName: 'Status',
210
+ type: 'enum',
211
+ options: [
212
+ { id: 's1', key: 'Active' },
213
+ { id: 's2', key: 'Inactive' },
214
+ ],
215
+ },
216
+ ],
217
+ };
218
+
219
+ renderComponent({
220
+ metadataTemplate: template,
221
+ onMetadataFilter,
222
+ });
223
+
224
+ await userEvent().click(screen.getByRole('button', { name: /Status/ }));
225
+ await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Active' }));
226
+
227
+ await waitFor(() => {
228
+ expect(onMetadataFilter).toHaveBeenCalledTimes(1);
229
+ });
230
+
231
+ const filterCall = onMetadataFilter.mock.calls[0][0];
232
+ expect(filterCall.status.value).toEqual(['Active']);
233
+ });
234
+
235
+ test('should handle initial filter values transformation', () => {
236
+ const initialFilterValues = {
237
+ industry: {
238
+ fieldType: 'enum' as const,
239
+ value: ['tech'],
240
+ },
241
+ price: {
242
+ fieldType: 'float' as const,
243
+ value: { range: { gt: 10, lt: 100 } },
244
+ },
245
+ name: {
246
+ fieldType: 'string' as const,
247
+ value: ['search term'],
248
+ },
249
+ category: {
250
+ fieldType: 'multiSelect' as const,
251
+ value: ['category1', 'category2'],
252
+ },
253
+ } as unknown as ExternalFilterValues;
254
+
255
+ renderComponent({
256
+ actionBarProps: { initialFilterValues },
257
+ });
258
+
259
+ expect(screen.getByRole('button', { name: 'All Filters 3' })).toBeInTheDocument();
260
+ expect(screen.getByRole('button', { name: /Industry/i })).toHaveTextContent(/\(1\)/);
261
+ expect(screen.getByRole('button', { name: /Category/i })).toHaveTextContent(/\(2\)/);
262
+ });
263
+
264
+ test('should handle empty metadata template fields', () => {
265
+ const emptyTemplate: MetadataTemplate = {
266
+ ...mockMetadataTemplate,
267
+ fields: [],
268
+ };
269
+
270
+ renderComponent({ metadataTemplate: emptyTemplate });
271
+
272
+ expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
273
+ expect(screen.getByText('File 1.txt')).toBeInTheDocument();
274
+ expect(screen.getByText('File 2.pdf')).toBeInTheDocument();
275
+ });
276
+
277
+ test('should handle undefined metadata template', () => {
278
+ renderComponent({ metadataTemplate: undefined as unknown as MetadataTemplate });
279
+
280
+ expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
281
+ expect(screen.getByText('File 1.txt')).toBeInTheDocument();
282
+ expect(screen.getByText('File 2.pdf')).toBeInTheDocument();
283
+ });
284
+
285
+ test('should handle empty collection items', () => {
286
+ const emptyCollection: Collection = {
287
+ id: '0',
288
+ items: [],
289
+ percentLoaded: 100,
290
+ };
291
+
292
+ renderComponent({ currentCollection: emptyCollection });
293
+
294
+ expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
295
+ });
296
+
297
+ test('should handle undefined collection items', () => {
298
+ const collectionWithoutItems: Collection = {
299
+ id: '0',
300
+ percentLoaded: 100,
301
+ };
302
+
303
+ renderComponent({ currentCollection: collectionWithoutItems });
304
+
305
+ expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
306
+ });
307
+
308
+ test('should memoize filterGroups when metadataTemplate changes', () => {
309
+ const { rerender } = renderComponent();
310
+
311
+ // Re-render with same template
312
+ rerender(<MetadataViewContainer {...defaultProps} />);
313
+
314
+ // Re-render with different template
315
+ const newTemplate: MetadataTemplate = {
316
+ ...mockMetadataTemplate,
317
+ id: 'template2',
318
+ displayName: 'New Template',
319
+ };
320
+ rerender(<MetadataViewContainer {...defaultProps} metadataTemplate={newTemplate} />);
321
+
322
+ expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
323
+ });
324
+
325
+ test('should handle fields with no options', () => {
326
+ const templateWithoutOptions: MetadataTemplate = {
327
+ ...mockMetadataTemplate,
328
+ fields: [
329
+ {
330
+ id: 'field1',
331
+ key: 'name',
332
+ displayName: 'File Name',
333
+ type: 'string',
334
+ },
335
+ {
336
+ id: 'field2',
337
+ key: 'industry',
338
+ displayName: 'Industry',
339
+ type: 'enum',
340
+ // No options defined
341
+ },
342
+ ],
343
+ };
344
+
345
+ renderComponent({ metadataTemplate: templateWithoutOptions });
346
+
347
+ expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument();
348
+ expect(screen.getAllByRole('button', { name: 'Name' })).toHaveLength(1); // Only the one added by component
349
+ expect(screen.getByRole('button', { name: 'File Name' })).toBeInTheDocument();
350
+ expect(screen.getByRole('button', { name: 'Industry' })).toBeInTheDocument();
351
+ });
352
+
353
+ test('should handle multiple field types in filter submission', async () => {
354
+ const onFilterSubmit = jest.fn();
355
+ const onMetadataFilter = jest.fn();
356
+ const template: MetadataTemplate = {
357
+ ...mockMetadataTemplate,
358
+ fields: [
359
+ {
360
+ id: 'field1',
361
+ key: 'status',
362
+ displayName: 'Status',
363
+ type: 'enum',
364
+ options: [
365
+ { id: 's1', key: 'Active' },
366
+ { id: 's2', key: 'Inactive' },
367
+ ],
368
+ },
369
+ {
370
+ id: 'field2',
371
+ key: 'price',
372
+ displayName: 'Price',
373
+ type: 'float',
374
+ },
375
+ ],
376
+ };
377
+
378
+ renderComponent({
379
+ metadataTemplate: template,
380
+ actionBarProps: { onFilterSubmit },
381
+ onMetadataFilter,
382
+ });
383
+
384
+ // Test enum filter
385
+ await userEvent().click(screen.getByRole('button', { name: /Status/ }));
386
+ await userEvent().click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Active' }));
387
+
388
+ await waitFor(() => {
389
+ expect(onMetadataFilter).toHaveBeenCalledTimes(1);
390
+ expect(onFilterSubmit).toHaveBeenCalledTimes(1);
391
+ });
392
+
393
+ const filterCall = onMetadataFilter.mock.calls[0][0];
394
+ expect(filterCall.status.value).toEqual(['Active']);
395
+ expect(filterCall.status.fieldType).toBe('enum');
396
+ });
397
+
398
+ describe('convertFilterValuesToExternal', () => {
399
+ test('should convert enum values to string arrays', () => {
400
+ const internalFilters = {
401
+ 'status-filter': {
402
+ fieldType: 'enum' as const,
403
+ options: [
404
+ { key: 'active', id: 'active1' },
405
+ { key: 'inactive', id: 'inactive1' },
406
+ ],
407
+ value: { enum: ['active', 'inactive'] },
408
+ },
409
+ };
410
+
411
+ const result = convertFilterValuesToExternal(internalFilters);
412
+
413
+ expect(result['status-filter'].value).toEqual(['active', 'inactive']);
414
+ expect(result['status-filter'].fieldType).toBe('enum');
415
+ expect(result['status-filter'].options).toEqual([
416
+ { key: 'active', id: 'active1' },
417
+ { key: 'inactive', id: 'inactive1' },
418
+ ]);
419
+ });
420
+
421
+ test('should keep range values unchanged', () => {
422
+ const internalFilters = {
423
+ 'price-filter': {
424
+ fieldType: 'float' as const,
425
+ value: { range: { gt: 10, lt: 100 }, advancedFilterOption: 'range' },
426
+ },
427
+ };
428
+
429
+ const result = convertFilterValuesToExternal(internalFilters);
430
+
431
+ expect(result['price-filter'].value).toEqual({ range: { gt: 10, lt: 100 }, advancedFilterOption: 'range' });
432
+ expect(result['price-filter'].fieldType).toBe('float');
433
+ });
434
+
435
+ test('should keep float values unchanged', () => {
436
+ const internalFilters = {
437
+ 'rating-filter': {
438
+ fieldType: 'float' as const,
439
+ value: { range: { gt: 4.5, lt: 5.0 }, advancedFilterOption: 'range' },
440
+ },
441
+ };
442
+
443
+ const result = convertFilterValuesToExternal(internalFilters);
444
+
445
+ expect(result['rating-filter'].value).toEqual({
446
+ range: { gt: 4.5, lt: 5.0 },
447
+ advancedFilterOption: 'range',
448
+ });
449
+ expect(result['rating-filter'].fieldType).toBe('float');
450
+ });
451
+
452
+ test('should handle mixed field types', () => {
453
+ const internalFilters = {
454
+ 'status-filter': {
455
+ fieldType: 'enum' as const,
456
+ options: [
457
+ { key: 'active', id: 'active1' },
458
+ { key: 'inactive', id: 'inactive1' },
459
+ ],
460
+ value: { enum: ['active'] },
461
+ },
462
+ 'price-filter': {
463
+ fieldType: 'float' as const,
464
+ value: { range: { gt: 0, lt: 50 }, advancedFilterOption: 'range' },
465
+ },
466
+ 'category-filter': {
467
+ fieldType: 'multiSelect' as const,
468
+ options: [
469
+ { key: 'tech', id: 'tech1' },
470
+ { key: 'finance', id: 'finance1' },
471
+ { key: 'healthcare', id: 'healthcare1' },
472
+ ],
473
+ value: { enum: ['tech', 'finance'] },
474
+ },
475
+ };
476
+
477
+ const result = convertFilterValuesToExternal(internalFilters);
478
+
479
+ expect(result['status-filter'].value).toEqual(['active']);
480
+ expect(result['price-filter'].value).toEqual({ range: { gt: 0, lt: 50 }, advancedFilterOption: 'range' });
481
+ expect(result['category-filter'].value).toEqual(['tech', 'finance']);
482
+ });
483
+
484
+ test('should handle empty filter object', () => {
485
+ const result = convertFilterValuesToExternal({});
486
+ expect(result).toEqual({});
487
+ });
488
+
489
+ test('should handle enum values with empty array', () => {
490
+ const internalFilters = {
491
+ 'status-filter': {
492
+ fieldType: 'enum' as const,
493
+ options: [{ key: 'active', id: 'active1' }],
494
+ value: { enum: [] },
495
+ },
496
+ };
497
+
498
+ const result = convertFilterValuesToExternal(internalFilters);
499
+
500
+ expect(result['status-filter'].value).toEqual([]);
501
+ expect(result['status-filter'].fieldType).toBe('enum');
502
+ });
503
+
504
+ test('should handle multiSelect values', () => {
505
+ const internalFilters = {
506
+ 'category-filter': {
507
+ fieldType: 'multiSelect' as const,
508
+ options: [
509
+ { key: 'tech', id: 'tech1' },
510
+ { key: 'finance', id: 'finance1' },
511
+ ],
512
+ value: { enum: ['tech', 'finance'] },
513
+ },
514
+ };
515
+
516
+ const result = convertFilterValuesToExternal(internalFilters);
517
+
518
+ expect(result['category-filter'].value).toEqual(['tech', 'finance']);
519
+ expect(result['category-filter'].fieldType).toBe('multiSelect');
520
+ });
117
521
  });
118
522
  });
@@ -23,37 +23,19 @@ const metadataQuery = {
23
23
 
24
24
  ancestor_folder_id: '0',
25
25
  fields: [
26
- `name`,
27
26
  `${metadataSourceFieldName}.industry`,
28
27
  `${metadataSourceFieldName}.last_contacted_at`,
29
28
  `${metadataSourceFieldName}.role`,
29
+ `${metadataSourceFieldName}.number`,
30
30
  ],
31
31
  };
32
32
 
33
- const fieldsToShow = [
34
- { key: `name` },
35
- { key: `${metadataSourceFieldName}.industry`, canEdit: true },
36
- { key: `${metadataSourceFieldName}.last_contacted_at`, canEdit: true },
37
- { key: `${metadataSourceFieldName}.role`, canEdit: true },
38
- ];
39
-
40
33
  const columns = mockSchema.fields.map(field => {
41
- if (field.key === 'name') {
42
- return {
43
- textValue: field.displayName,
44
- id: 'name',
45
- type: 'string',
46
- allowsSorting: true,
47
- minWidth: 250,
48
- maxWidth: 250,
49
- isRowHeader: true,
50
- };
51
- }
52
-
53
34
  if (field.type === 'date') {
54
35
  return {
55
36
  textValue: field.displayName,
56
37
  id: `${metadataSourceFieldName}.${field.key}`,
38
+ key: `${metadataSourceFieldName}.${field.key}`,
57
39
  type: field.type,
58
40
  allowsSorting: true,
59
41
  minWidth: 200,
@@ -68,6 +50,7 @@ const columns = mockSchema.fields.map(field => {
68
50
  return {
69
51
  textValue: field.displayName,
70
52
  id: `${metadataSourceFieldName}.${field.key}`,
53
+ key: `${metadataSourceFieldName}.${field.key}`,
71
54
  type: field.type,
72
55
  allowsSorting: true,
73
56
  minWidth: 200,
@@ -87,7 +70,6 @@ export const metadataView: Story = {
87
70
  },
88
71
  },
89
72
  metadataQuery,
90
- fieldsToShow,
91
73
  defaultView,
92
74
  features: {
93
75
  contentExplorer: {
@@ -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';
@@ -30,13 +32,11 @@ const metadataQuery = {
30
32
  fields: [
31
33
  // Default to returning all fields in the metadata template schema, and name as a standalone (non-metadata) field
32
34
  ...mockSchema.fields.map(field => `${metadataFieldNamePrefix}.${field.key}`),
33
- 'name',
34
35
  ],
35
36
  };
36
37
 
37
38
  // Used for metadata view v1
38
39
  const fieldsToShow = [
39
- { key: `${metadataFieldNamePrefix}.name`, canEdit: false, displayName: 'Alias' },
40
40
  { key: `${metadataFieldNamePrefix}.industry`, canEdit: true },
41
41
  { key: `${metadataFieldNamePrefix}.last_contacted_at`, canEdit: true },
42
42
  { key: `${metadataFieldNamePrefix}.role`, canEdit: true },
@@ -44,15 +44,6 @@ const fieldsToShow = [
44
44
 
45
45
  // Used for metadata view v2
46
46
  const columns = [
47
- {
48
- // Always include the name column
49
- textValue: 'Name',
50
- id: 'name',
51
- type: 'string',
52
- allowsSorting: true,
53
- minWidth: 150,
54
- maxWidth: 150,
55
- },
56
47
  ...mockSchema.fields.map(field => ({
57
48
  textValue: field.displayName,
58
49
  id: `${metadataFieldNamePrefix}.${field.key}`,
@@ -138,17 +129,16 @@ export const metadataViewV2: Story = {
138
129
  args: metadataViewV2ElementProps,
139
130
  };
140
131
 
141
- // @TODO Assert that rows are actually sorted in a different order, once handleSortChange is implemented
142
132
  export const metadataViewV2SortsFromHeader: Story = {
143
133
  args: metadataViewV2ElementProps,
144
134
  play: async ({ canvas }) => {
145
- await waitFor(() => {
146
- expect(canvas.getByRole('row', { name: /Industry/i })).toBeInTheDocument();
147
- });
135
+ const industryHeader = await canvas.findByRole('columnheader', { name: 'Industry' });
136
+ expect(industryHeader).toBeInTheDocument();
137
+
138
+ const firstRow = await canvas.findByRole('row', { name: /Child 2/i });
139
+ expect(firstRow).toBeInTheDocument();
148
140
 
149
- const firstRow = canvas.getByRole('row', { name: /Industry/i });
150
- const industryHeader = within(firstRow).getByRole('columnheader', { name: 'Industry' });
151
- userEvent.click(industryHeader);
141
+ await userEvent.click(industryHeader);
152
142
  },
153
143
  };
154
144
 
@@ -166,9 +156,9 @@ export const metadataViewV2WithCustomActions: Story = {
166
156
 
167
157
  const initialFilterActionBarProps = {
168
158
  initialFilterValues: {
169
- 'industry-filter': { value: ['Legal'] },
159
+ industry: { value: ['Legal'] },
170
160
  'mimetype-filter': { value: ['boxnoteType', 'documentType', 'threedType'] },
171
- 'role-filter': { value: ['Developer', 'Business Owner', 'Marketing'] },
161
+ role: { value: ['Developer', 'Business Owner', 'Marketing'] },
172
162
  },
173
163
  };
174
164
 
@@ -237,6 +227,37 @@ export const metadataViewV2WithBulkItemActionMenuShowsItemActionMenu: Story = {
237
227
  },
238
228
  };
239
229
 
230
+ export const sidePanelOpenWithMultipleItemsSelected: Story = {
231
+ args: {
232
+ ...metadataViewV2ElementProps,
233
+ metadataViewProps: {
234
+ columns,
235
+ tableProps: {
236
+ isSelectAllEnabled: true,
237
+ },
238
+ },
239
+ },
240
+
241
+ play: async ({ canvas }) => {
242
+ await waitFor(() => {
243
+ expect(canvas.getByRole('row', { name: /Child 2/i })).toBeInTheDocument();
244
+ });
245
+
246
+ // Select the first row by clicking its checkbox
247
+ const firstItem = canvas.getByRole('row', { name: /Child 2/i });
248
+ const checkbox = within(firstItem).getByRole('checkbox');
249
+ await userEvent.click(checkbox);
250
+
251
+ // Select the second row by clicking its checkbox
252
+ const secondItem = canvas.getAllByRole('row', { name: /Child 1/i })[0];
253
+ const secondCheckbox = within(secondItem).getByRole('checkbox');
254
+ await userEvent.click(secondCheckbox);
255
+
256
+ const metadataButton = canvas.getByRole('button', { name: 'Metadata' });
257
+ await userEvent.click(metadataButton);
258
+ },
259
+ };
260
+
240
261
  const meta: Meta<typeof ContentExplorer> = {
241
262
  title: 'Elements/ContentExplorer/tests/MetadataView/visual',
242
263
  component: ContentExplorer,
@@ -248,7 +269,21 @@ const meta: Meta<typeof ContentExplorer> = {
248
269
  parameters: {
249
270
  msw: {
250
271
  handlers: [
251
- http.post(`${DEFAULT_HOSTNAME_API}/2.0/metadata_queries/execute_read`, () => {
272
+ // 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.
273
+ http.post(`${DEFAULT_HOSTNAME_API}/2.0/metadata_queries/execute_read`, async ({ request }) => {
274
+ const body = await request.clone().json();
275
+ const orderByDirection = body.order_by[0].direction;
276
+ const orderByFieldKey = body.order_by[0].field_key;
277
+
278
+ // Hardcoded case for sorting by industry
279
+ if (orderByFieldKey === `industry` && orderByDirection === 'ASC') {
280
+ const sortedMetadata = orderBy(
281
+ mockMetadata.entries,
282
+ 'metadata.enterprise_0.templateName.industry',
283
+ 'asc',
284
+ );
285
+ return HttpResponse.json({ ...mockMetadata, entries: sortedMetadata });
286
+ }
252
287
  return HttpResponse.json(mockMetadata);
253
288
  }),
254
289
  http.get(`${DEFAULT_HOSTNAME_API}/2.0/metadata_templates/enterprise/templateName/schema`, () => {