box-ui-elements 24.0.0-beta.5 → 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 (85) hide show
  1. package/dist/explorer.css +1 -1
  2. package/dist/explorer.js +1 -1
  3. package/es/elements/content-explorer/Content.js +3 -1
  4. package/es/elements/content-explorer/Content.js.map +1 -1
  5. package/es/elements/content-explorer/ContentExplorer.js +16 -5
  6. package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
  7. package/es/elements/content-explorer/MetadataQueryAPIHelper.js +104 -7
  8. package/es/elements/content-explorer/MetadataQueryAPIHelper.js.map +1 -1
  9. package/es/elements/content-explorer/MetadataQueryBuilder.js +115 -0
  10. package/es/elements/content-explorer/MetadataQueryBuilder.js.map +1 -0
  11. package/es/elements/content-explorer/MetadataViewContainer.js +92 -46
  12. package/es/elements/content-explorer/MetadataViewContainer.js.map +1 -1
  13. package/es/elements/content-explorer/stories/MetadataView.stories.js +3 -25
  14. package/es/elements/content-explorer/stories/MetadataView.stories.js.map +1 -1
  15. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js +4 -16
  16. package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js.map +1 -1
  17. package/es/src/elements/common/__mocks__/mockMetadata.d.ts +8 -24
  18. package/es/src/elements/content-explorer/Content.d.ts +4 -3
  19. package/es/src/elements/content-explorer/ContentExplorer.d.ts +8 -3
  20. package/es/src/elements/content-explorer/MetadataQueryAPIHelper.d.ts +11 -2
  21. package/es/src/elements/content-explorer/MetadataQueryBuilder.d.ts +27 -0
  22. package/es/src/elements/content-explorer/MetadataViewContainer.d.ts +8 -4
  23. package/es/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.d.ts +1 -0
  24. package/i18n/bn-IN.js +1 -1
  25. package/i18n/bn-IN.properties +8 -0
  26. package/i18n/da-DK.js +1 -1
  27. package/i18n/da-DK.properties +8 -0
  28. package/i18n/de-DE.js +1 -1
  29. package/i18n/de-DE.properties +8 -0
  30. package/i18n/en-AU.js +1 -1
  31. package/i18n/en-AU.properties +8 -0
  32. package/i18n/en-CA.js +1 -1
  33. package/i18n/en-CA.properties +8 -0
  34. package/i18n/en-GB.js +1 -1
  35. package/i18n/en-GB.properties +8 -0
  36. package/i18n/es-419.js +1 -1
  37. package/i18n/es-419.properties +8 -0
  38. package/i18n/es-ES.js +1 -1
  39. package/i18n/es-ES.properties +8 -0
  40. package/i18n/fi-FI.js +1 -1
  41. package/i18n/fi-FI.properties +8 -0
  42. package/i18n/fr-CA.js +1 -1
  43. package/i18n/fr-CA.properties +8 -0
  44. package/i18n/fr-FR.js +1 -1
  45. package/i18n/fr-FR.properties +8 -0
  46. package/i18n/hi-IN.js +1 -1
  47. package/i18n/hi-IN.properties +8 -0
  48. package/i18n/it-IT.js +1 -1
  49. package/i18n/it-IT.properties +8 -0
  50. package/i18n/ja-JP.js +1 -1
  51. package/i18n/ja-JP.properties +8 -0
  52. package/i18n/ko-KR.js +1 -1
  53. package/i18n/ko-KR.properties +8 -0
  54. package/i18n/nb-NO.js +1 -1
  55. package/i18n/nb-NO.properties +8 -0
  56. package/i18n/nl-NL.js +1 -1
  57. package/i18n/nl-NL.properties +8 -0
  58. package/i18n/pl-PL.js +1 -1
  59. package/i18n/pl-PL.properties +8 -0
  60. package/i18n/pt-BR.js +1 -1
  61. package/i18n/pt-BR.properties +8 -0
  62. package/i18n/ru-RU.js +1 -1
  63. package/i18n/ru-RU.properties +8 -0
  64. package/i18n/sv-SE.js +1 -1
  65. package/i18n/sv-SE.properties +8 -0
  66. package/i18n/tr-TR.js +1 -1
  67. package/i18n/tr-TR.properties +8 -0
  68. package/i18n/zh-CN.js +1 -1
  69. package/i18n/zh-CN.properties +8 -0
  70. package/i18n/zh-TW.js +1 -1
  71. package/i18n/zh-TW.properties +8 -0
  72. package/package.json +3 -3
  73. package/src/elements/common/__mocks__/mockMetadata.ts +7 -11
  74. package/src/elements/content-explorer/Content.tsx +8 -2
  75. package/src/elements/content-explorer/ContentExplorer.tsx +208 -193
  76. package/src/elements/content-explorer/MetadataQueryAPIHelper.ts +111 -5
  77. package/src/elements/content-explorer/MetadataQueryBuilder.ts +159 -0
  78. package/src/elements/content-explorer/MetadataViewContainer.tsx +112 -37
  79. package/src/elements/content-explorer/__tests__/Content.test.tsx +1 -0
  80. package/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +2 -5
  81. package/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +421 -8
  82. package/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts +419 -0
  83. package/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx +413 -9
  84. package/src/elements/content-explorer/stories/MetadataView.stories.tsx +3 -21
  85. package/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +2 -13
@@ -5,6 +5,7 @@ import includes from 'lodash/includes';
5
5
  import isArray from 'lodash/isArray';
6
6
  import type { MetadataTemplateField } from '@box/metadata-editor';
7
7
  import type { MetadataFieldType } from '@box/metadata-view';
8
+
8
9
  import API from '../../api';
9
10
  import { areFieldValuesEqual, isEmptyValue, isMultiValuesField } from './utils';
10
11
 
@@ -16,7 +17,7 @@ import {
16
17
  METADATA_FIELD_TYPE_ENUM,
17
18
  METADATA_FIELD_TYPE_MULTISELECT,
18
19
  } from '../../common/constants';
19
- import { FIELD_NAME, FIELD_METADATA, FIELD_EXTENSION, FIELD_PERMISSIONS } from '../../constants';
20
+ import { FIELD_ITEM_NAME, FIELD_METADATA, FIELD_EXTENSION, FIELD_PERMISSIONS } from '../../constants';
20
21
 
21
22
  import type { MetadataQuery as MetadataQueryType, MetadataQueryResponseData } from '../../common/types/metadataQueries';
22
23
  import type {
@@ -28,6 +29,15 @@ import type {
28
29
  } from '../../common/types/metadata';
29
30
  import type { ElementsXhrError, JSONPatchOperations } from '../../common/types/api';
30
31
  import type { Collection, BoxItem } from '../../common/types/core';
32
+ import {
33
+ getMimeTypeFilter,
34
+ getRangeFilter,
35
+ getSelectFilter,
36
+ getStringFilter,
37
+ mergeQueries,
38
+ mergeQueryParams,
39
+ } from './MetadataQueryBuilder';
40
+ import type { ExternalFilterValues } from './MetadataViewContainer';
31
41
 
32
42
  type SuccessCallback = (metadataQueryCollection: Collection, metadataTemplate: MetadataTemplate) => void;
33
43
  type ErrorCallback = (e: ElementsXhrError) => void;
@@ -232,8 +242,10 @@ export default class MetadataQueryAPIHelper {
232
242
  metadataQuery: MetadataQueryType,
233
243
  successCallback: SuccessCallback,
234
244
  errorCallback: ErrorCallback,
245
+ fields?: ExternalFilterValues,
235
246
  ): Promise<void> => {
236
- this.metadataQuery = this.verifyQueryFields(metadataQuery);
247
+ this.metadataQuery = this.verifyQueryFields(metadataQuery, fields);
248
+
237
249
  return this.queryMetadata()
238
250
  .then(this.getTemplateSchemaInfo)
239
251
  .then(this.getDataWithTypes)
@@ -285,20 +297,114 @@ export default class MetadataQueryAPIHelper {
285
297
  .bulkUpdateMetadata(items, this.metadataTemplate, operations, successCallback, errorCallback);
286
298
  };
287
299
 
300
+ buildMetadataQueryParams = (filters: ExternalFilterValues) => {
301
+ let argIndex = 0;
302
+ let queries: string[] = [];
303
+ let queryParams: { [key: string]: number | Date | string } = {};
304
+
305
+ if (filters) {
306
+ Object.keys(filters).forEach(key => {
307
+ const filter = filters[key];
308
+ if (!filter) {
309
+ return;
310
+ }
311
+
312
+ const { fieldType, value } = filter;
313
+
314
+ switch (fieldType) {
315
+ case 'date':
316
+ case 'float': {
317
+ if (typeof value === 'object' && value !== null && 'range' in value) {
318
+ const result = getRangeFilter(value, key, argIndex);
319
+ queryParams = mergeQueryParams(queryParams, result.queryParams);
320
+ queries = mergeQueries(queries, result.queries);
321
+ argIndex += result.keysGenerated;
322
+ break;
323
+ }
324
+ break;
325
+ }
326
+ case 'enum':
327
+ case 'multiSelect': {
328
+ const arrayValue = Array.isArray(value) ? value.map(v => String(v)) : [String(value)];
329
+ let result;
330
+ if (key === 'mimetype-filter') {
331
+ result = getMimeTypeFilter(arrayValue, key, argIndex);
332
+ } else {
333
+ result = getSelectFilter(arrayValue, key, argIndex);
334
+ }
335
+ queryParams = mergeQueryParams(queryParams, result.queryParams);
336
+ queries = mergeQueries(queries, result.queries);
337
+ argIndex += result.keysGenerated;
338
+ break;
339
+ }
340
+
341
+ case 'string': {
342
+ if (value && value[0]) {
343
+ const result = getStringFilter(value[0], key, argIndex);
344
+ queryParams = mergeQueryParams(queryParams, result.queryParams);
345
+ queries = mergeQueries(queries, result.queries);
346
+ argIndex += result.keysGenerated;
347
+ }
348
+ break;
349
+ }
350
+
351
+ default:
352
+ break;
353
+ }
354
+ });
355
+ }
356
+
357
+ const query = queries.reduce((acc, curr, index) => {
358
+ if (index > 0) {
359
+ acc += ` AND ${curr}`;
360
+ } else {
361
+ acc = curr;
362
+ }
363
+ return acc;
364
+ }, '');
365
+
366
+ return {
367
+ queryParams,
368
+ query,
369
+ };
370
+ };
371
+
372
+ mergeQuery = (customQuery: string, filterQuery: string): string => {
373
+ if (!customQuery) {
374
+ return filterQuery;
375
+ }
376
+ if (!filterQuery) {
377
+ return customQuery;
378
+ }
379
+ // Merge queries with AND operator
380
+ return `${customQuery} AND ${filterQuery}`;
381
+ };
382
+
288
383
  /**
289
384
  * Verify that the metadata query has required fields and update it if necessary
290
385
  * For a file item, default fields included in the response are "type", "id", "etag"
291
386
  *
292
387
  * @param {MetadataQueryType} metadataQuery metadata query object
388
+ * @param {ExternalFilterValues} [fields] optional filter values to apply to the metadata query
293
389
  * @return {MetadataQueryType} updated metadata query object with required fields
294
390
  */
295
- verifyQueryFields = (metadataQuery: MetadataQueryType): MetadataQueryType => {
391
+ verifyQueryFields = (metadataQuery: MetadataQueryType, fields?: ExternalFilterValues): MetadataQueryType => {
296
392
  const clonedQuery = cloneDeep(metadataQuery);
297
393
  const clonedFields = isArray(clonedQuery.fields) ? clonedQuery.fields : [];
298
394
 
395
+ if (fields) {
396
+ const { query: filterQuery, queryParams: filteredQueryParams } = this.buildMetadataQueryParams(fields);
397
+ const { query: customQuery, query_params: customQueryParams } = clonedQuery;
398
+ const query = this.mergeQuery(customQuery, filterQuery);
399
+ const queryParams = mergeQueryParams(filteredQueryParams, customQueryParams);
400
+ if (query) {
401
+ clonedQuery.query = query;
402
+ clonedQuery.query_params = queryParams;
403
+ }
404
+ }
299
405
  // Make sure the query fields array has "name" field which is necessary to display info.
300
- if (!clonedFields.includes(FIELD_NAME)) {
301
- clonedFields.push(FIELD_NAME);
406
+ if (!clonedFields.includes(FIELD_ITEM_NAME)) {
407
+ clonedFields.push(FIELD_ITEM_NAME);
302
408
  }
303
409
 
304
410
  if (!clonedFields.includes(FIELD_EXTENSION)) {
@@ -0,0 +1,159 @@
1
+ import isNil from 'lodash/isNil';
2
+
3
+ type QueryResult = {
4
+ queryParams: { [key: string]: number | Date | string };
5
+ queries: string[];
6
+ keysGenerated: number;
7
+ };
8
+
9
+ // Custom type for range filters
10
+ type SimpleRangeType = {
11
+ range: {
12
+ gt: number | string;
13
+ lt: number | string;
14
+ };
15
+ };
16
+
17
+ // Union type for filter values
18
+ type SimpleFilterValue = string[] | SimpleRangeType;
19
+
20
+ export const mergeQueryParams = (
21
+ targetParams: { [key: string]: number | Date | string },
22
+ sourceParams: { [key: string]: number | Date | string },
23
+ ): { [key: string]: number | Date | string } => {
24
+ return { ...targetParams, ...sourceParams };
25
+ };
26
+
27
+ export const mergeQueries = (targetQueries: string[], sourceQueries: string[]): string[] => {
28
+ return [...targetQueries, ...sourceQueries];
29
+ };
30
+
31
+ const generateArgKey = (key: string, index: number): string => {
32
+ const purifyKey = key.replace(/[^\w]/g, '_');
33
+ return `arg_${purifyKey}_${index}`;
34
+ };
35
+
36
+ const escapeValue = (value: string): string => value.replace(/([_%])/g, '\\$1');
37
+
38
+ export const getStringFilter = (filterValue: string, fieldKey: string, argIndexStart: number): QueryResult => {
39
+ let currentArgIndex = argIndexStart;
40
+
41
+ const argKey = generateArgKey(fieldKey, (currentArgIndex += 1));
42
+ return {
43
+ queryParams: { [argKey]: `%${escapeValue(filterValue)}%` },
44
+ queries: [`(${fieldKey} ILIKE :${argKey})`],
45
+ keysGenerated: currentArgIndex - argIndexStart,
46
+ };
47
+ };
48
+
49
+ const isInvalid = (value: number | string) => {
50
+ return isNil(value) || value === '';
51
+ };
52
+
53
+ export const getRangeFilter = (
54
+ filterValue: SimpleFilterValue,
55
+ fieldKey: string,
56
+ argIndexStart: number,
57
+ ): QueryResult => {
58
+ let currentArgIndex = argIndexStart;
59
+
60
+ if (filterValue && typeof filterValue === 'object' && 'range' in filterValue && filterValue.range) {
61
+ const { gt, lt } = filterValue.range;
62
+ const queryParams: { [key: string]: number | string } = {};
63
+ const queries: string[] = [];
64
+
65
+ if (!isInvalid(gt) && !isInvalid(lt)) {
66
+ // Both gt and lt: between values
67
+ const argKeyGt = generateArgKey(fieldKey, (currentArgIndex += 1));
68
+ const argKeyLt = generateArgKey(fieldKey, (currentArgIndex += 1));
69
+ queryParams[argKeyGt] = gt;
70
+ queryParams[argKeyLt] = lt;
71
+ queries.push(`(${fieldKey} >= :${argKeyGt} AND ${fieldKey} <= :${argKeyLt})`);
72
+ } else if (!isInvalid(gt)) {
73
+ // Only gt: greater than
74
+ const argKey = generateArgKey(fieldKey, (currentArgIndex += 1));
75
+ queryParams[argKey] = gt;
76
+ queries.push(`(${fieldKey} >= :${argKey})`);
77
+ } else if (!isInvalid(lt)) {
78
+ // Only lt: less than
79
+ const argKey = generateArgKey(fieldKey, (currentArgIndex += 1));
80
+ queryParams[argKey] = lt;
81
+ queries.push(`(${fieldKey} <= :${argKey})`);
82
+ }
83
+
84
+ return {
85
+ queryParams,
86
+ queries,
87
+ keysGenerated: currentArgIndex - argIndexStart,
88
+ };
89
+ }
90
+ return {
91
+ queryParams: {},
92
+ queries: [],
93
+ keysGenerated: 0,
94
+ };
95
+ };
96
+
97
+ export const getSelectFilter = (filterValue: string[], fieldKey: string, argIndexStart: number): QueryResult => {
98
+ if (!Array.isArray(filterValue) || filterValue.length === 0) {
99
+ return {
100
+ queryParams: {},
101
+ queries: [],
102
+ keysGenerated: 0,
103
+ };
104
+ }
105
+
106
+ let currentArgIndex = argIndexStart;
107
+
108
+ const multiSelectQueryParams = Object.fromEntries(
109
+ filterValue.map(value => {
110
+ currentArgIndex += 1;
111
+ return [generateArgKey(fieldKey, currentArgIndex), String(value)];
112
+ }),
113
+ );
114
+
115
+ return {
116
+ queryParams: multiSelectQueryParams,
117
+ queries: [
118
+ `(${fieldKey === 'mimetype-filter' ? 'item.extension' : fieldKey} HASANY (${Object.keys(
119
+ multiSelectQueryParams,
120
+ )
121
+ .map(argKey => `:${argKey}`)
122
+ .join(', ')}))`,
123
+ ],
124
+ keysGenerated: currentArgIndex - argIndexStart,
125
+ };
126
+ };
127
+
128
+ export const getMimeTypeFilter = (filterValue: string[], fieldKey: string, argIndexStart: number): QueryResult => {
129
+ if (!Array.isArray(filterValue) || filterValue.length === 0) {
130
+ return {
131
+ queryParams: {},
132
+ queries: [],
133
+ keysGenerated: 0,
134
+ };
135
+ }
136
+
137
+ let currentArgIndex = argIndexStart;
138
+
139
+ const multiSelectQueryParams = Object.fromEntries(
140
+ filterValue.map(value => {
141
+ currentArgIndex += 1;
142
+ // the item-type-selector is returning the extensions with the suffix 'Type', so we remove it for the query
143
+ return [
144
+ generateArgKey(fieldKey, currentArgIndex),
145
+ String(value.endsWith('Type') ? value.slice(0, -4) : value),
146
+ ];
147
+ }),
148
+ );
149
+
150
+ return {
151
+ queryParams: multiSelectQueryParams,
152
+ queries: [
153
+ `(item.extension IN (${Object.keys(multiSelectQueryParams)
154
+ .map(argKey => `:${argKey}`)
155
+ .join(', ')}))`,
156
+ ],
157
+ keysGenerated: currentArgIndex - argIndexStart,
158
+ };
159
+ };
@@ -1,20 +1,39 @@
1
1
  import * as React from 'react';
2
- import type { EnumType, FloatType, MetadataFormFieldValue, RangeType } from '@box/metadata-filter';
3
- import { MetadataView, type MetadataViewProps } from '@box/metadata-view';
2
+ import { useIntl } from 'react-intl';
3
+ import {
4
+ EnumType,
5
+ FloatType,
6
+ MetadataFormFieldValue,
7
+ MetadataTemplateFieldOption,
8
+ RangeType,
9
+ } from '@box/metadata-filter';
10
+ import {
11
+ MetadataView,
12
+ type FilterValues,
13
+ type MetadataViewProps,
14
+ type MetadataFieldType,
15
+ type Column,
16
+ } from '@box/metadata-view';
4
17
  import { type Key } from '@react-types/shared';
18
+ import cloneDeep from 'lodash/cloneDeep';
5
19
 
6
20
  import { SortDescriptor } from 'react-aria-components';
21
+ import { FIELD_ITEM_NAME } from '../../constants';
7
22
  import type { Collection } from '../../common/types/core';
8
- import type { MetadataTemplate } from '../../common/types/metadata';
23
+ import type { MetadataTemplate, MetadataTemplateField } from '../../common/types/metadata';
24
+
25
+ import messages from '../common/messages';
9
26
 
10
27
  // Public-friendly version of MetadataFormFieldValue from @box/metadata-filter
11
28
  // (string[] for enum type, range/float objects stay the same)
12
29
  type EnumToStringArray<T> = T extends EnumType ? string[] : T;
13
30
  type ExternalMetadataFormFieldValue = EnumToStringArray<MetadataFormFieldValue>;
14
31
 
15
- type ExternalFilterValues = Record<
32
+ export type ExternalFilterValues = Record<
16
33
  string,
17
34
  {
35
+ options?: FilterValues[string]['options'] | MetadataTemplateFieldOption[];
36
+ fieldType: FilterValues[string]['fieldType'] | MetadataFieldType;
18
37
  value: ExternalMetadataFormFieldValue;
19
38
  }
20
39
  >;
@@ -27,6 +46,8 @@ type ActionBarProps = Omit<
27
46
  onFilterSubmit?: (filterValues: ExternalFilterValues) => void;
28
47
  };
29
48
 
49
+ const ITEM_FILTER_NAME = 'item_name';
50
+
30
51
  /**
31
52
  * Helper function to trim metadataFieldNamePrefix from column names
32
53
  * For example: 'metadata.enterprise_1515946.mdViewTemplate1.industry' -> 'industry'
@@ -56,22 +77,36 @@ function transformInitialFilterValuesToInternal(
56
77
  );
57
78
  }
58
79
 
59
- function transformInternalFieldsToPublic(
60
- fields: Record<string, { value: MetadataFormFieldValue }>,
61
- ): ExternalFilterValues {
62
- return Object.entries(fields).reduce<ExternalFilterValues>((acc, [key, { value }]) => {
63
- acc[key] =
80
+ export function convertFilterValuesToExternal(fields: FilterValues): ExternalFilterValues {
81
+ return Object.entries(fields).reduce<ExternalFilterValues>((acc, [key, field]) => {
82
+ const { value, options, fieldType } = field;
83
+
84
+ // Transform the value based on its type
85
+ const transformedValue: ExternalMetadataFormFieldValue =
64
86
  'enum' in value && Array.isArray(value.enum)
65
- ? { value: value.enum }
66
- : { value: value as RangeType | FloatType };
87
+ ? value.enum // Convert enum type to string array
88
+ : (value as RangeType | FloatType); // Keep range/float objects as-is
89
+
90
+ acc[key === ITEM_FILTER_NAME ? FIELD_ITEM_NAME : key] = {
91
+ options,
92
+ fieldType,
93
+ value: transformedValue,
94
+ };
95
+
67
96
  return acc;
68
97
  }, {});
69
98
  }
70
99
 
100
+ // Internal helper function for component use
101
+ function transformInternalFieldsToPublic(fields: FilterValues): ExternalFilterValues {
102
+ return convertFilterValuesToExternal(fields);
103
+ }
104
+
71
105
  export interface MetadataViewContainerProps extends Omit<MetadataViewProps, 'items' | 'actionBarProps'> {
72
106
  actionBarProps?: ActionBarProps;
73
107
  currentCollection: Collection;
74
108
  metadataTemplate: MetadataTemplate;
109
+ onMetadataFilter: (fields: ExternalFilterValues) => void;
75
110
  /* Internally controlled onSortChange prop for the MetadataView component. */
76
111
  onSortChange?: (sortBy: Key, sortDirection: string) => void;
77
112
  }
@@ -81,20 +116,65 @@ const MetadataViewContainer = ({
81
116
  columns,
82
117
  currentCollection,
83
118
  metadataTemplate,
119
+ onMetadataFilter,
84
120
  onSortChange: onSortChangeInternal,
121
+ tableProps,
85
122
  ...rest
86
123
  }: MetadataViewContainerProps) => {
124
+ const { formatMessage } = useIntl();
87
125
  const { items = [] } = currentCollection;
88
- const { initialFilterValues: initialFilterValuesProp, onFilterSubmit: onFilterSubmitProp } = actionBarProps ?? {};
89
-
90
- const filterGroups = React.useMemo(
91
- () => [
126
+ const { initialFilterValues: initialFilterValuesProp, onFilterSubmit } = actionBarProps ?? {};
127
+
128
+ const newColumns = React.useMemo(() => {
129
+ let clonedColumns = cloneDeep(columns);
130
+
131
+ const hasItemNameField = clonedColumns.some((col: Column) => col.id === FIELD_ITEM_NAME);
132
+
133
+ if (!hasItemNameField) {
134
+ clonedColumns = [
135
+ {
136
+ allowsSorting: true,
137
+ id: FIELD_ITEM_NAME,
138
+ isItemMetadata: true,
139
+ isRowHeader: true,
140
+ minWidth: 250,
141
+ maxWidth: 250,
142
+ textValue: formatMessage(messages.name),
143
+ type: 'string',
144
+ },
145
+ ...clonedColumns,
146
+ ];
147
+ }
148
+
149
+ return clonedColumns;
150
+ }, [columns, formatMessage]);
151
+
152
+ const filterGroups = React.useMemo(() => {
153
+ const clonedTemplate = cloneDeep(metadataTemplate);
154
+ let fields = clonedTemplate?.fields || [];
155
+
156
+ // Check if item_name field already exists to avoid duplicates
157
+ const hasItemNameField = fields.some((field: MetadataTemplateField) => field.key === ITEM_FILTER_NAME);
158
+
159
+ if (!hasItemNameField) {
160
+ fields = [
161
+ {
162
+ key: ITEM_FILTER_NAME,
163
+ displayName: formatMessage(messages.name),
164
+ type: 'string',
165
+ shouldRenderChip: true,
166
+ },
167
+ ...fields,
168
+ ];
169
+ }
170
+
171
+ return [
92
172
  {
93
173
  toggleable: true,
94
174
  filters:
95
- metadataTemplate?.fields?.map(field => {
175
+ fields?.map(field => {
96
176
  return {
97
- id: `${field.key}-filter`,
177
+ id: field.key,
98
178
  name: field.displayName,
99
179
  fieldType: field.type,
100
180
  options: field.options?.map(({ key }) => key) || [],
@@ -102,38 +182,33 @@ const MetadataViewContainer = ({
102
182
  };
103
183
  }) || [],
104
184
  },
105
- ],
106
- [metadataTemplate],
107
- );
185
+ ];
186
+ }, [formatMessage, metadataTemplate]);
108
187
 
109
- // Transform initial filter values to internal field format
110
188
  const initialFilterValues = React.useMemo(
111
189
  () => transformInitialFilterValuesToInternal(initialFilterValuesProp),
112
190
  [initialFilterValuesProp],
113
191
  );
114
192
 
115
- // Transform field values to public-friendly format
116
- const onFilterSubmit = React.useCallback(
117
- (fields: Record<string, { value: MetadataFormFieldValue }>) => {
118
- if (!onFilterSubmitProp) return;
193
+ const handleFilterSubmit = React.useCallback(
194
+ (fields: FilterValues) => {
119
195
  const transformed = transformInternalFieldsToPublic(fields);
120
- onFilterSubmitProp(transformed);
196
+ onMetadataFilter(transformed);
197
+ if (onFilterSubmit) {
198
+ onFilterSubmit(transformed);
199
+ }
121
200
  },
122
- [onFilterSubmitProp],
201
+ [onFilterSubmit, onMetadataFilter],
123
202
  );
124
203
 
125
204
  const transformedActionBarProps = React.useMemo(() => {
126
205
  return {
127
206
  ...actionBarProps,
128
207
  initialFilterValues,
129
- onFilterSubmit,
208
+ onFilterSubmit: handleFilterSubmit,
130
209
  filterGroups,
131
210
  };
132
- }, [actionBarProps, initialFilterValues, onFilterSubmit, filterGroups]);
133
-
134
- // Extract the original tableProps.onSortChange from rest
135
- const { tableProps, ...otherRest } = rest;
136
- const onSortChangeExternal = tableProps?.onSortChange;
211
+ }, [actionBarProps, initialFilterValues, handleFilterSubmit, filterGroups]);
137
212
 
138
213
  // Create a wrapper function that calls both. The wrapper function should follow the signature of onSortChange from RAC
139
214
  const handleSortChange = React.useCallback(
@@ -144,7 +219,7 @@ const MetadataViewContainer = ({
144
219
  const trimmedColumn = trimMetadataFieldPrefix(String(column));
145
220
  onSortChangeInternal(trimmedColumn, direction === 'ascending' ? 'ASC' : 'DESC');
146
221
  }
147
-
222
+ const onSortChangeExternal = tableProps?.onSortChange;
148
223
  // Then call the original customer-provided onSortChange if it exists
149
224
  // Accepts "ascending" / "descending" (https://react-spectrum.adobe.com/react-aria/Table.html)
150
225
  if (onSortChangeExternal) {
@@ -154,7 +229,7 @@ const MetadataViewContainer = ({
154
229
  });
155
230
  }
156
231
  },
157
- [onSortChangeInternal, onSortChangeExternal],
232
+ [onSortChangeInternal, tableProps],
158
233
  );
159
234
 
160
235
  // Create new tableProps with our wrapper function
@@ -166,10 +241,10 @@ const MetadataViewContainer = ({
166
241
  return (
167
242
  <MetadataView
168
243
  actionBarProps={transformedActionBarProps}
169
- columns={columns}
244
+ columns={newColumns}
170
245
  items={items}
171
246
  tableProps={newTableProps}
172
- {...otherRest}
247
+ {...rest}
173
248
  />
174
249
  );
175
250
  };
@@ -30,6 +30,7 @@ const mockProps: ContentProps = {
30
30
  onItemRename: jest.fn(),
31
31
  onItemSelect: jest.fn(),
32
32
  onItemShare: jest.fn(),
33
+ onMetadataFilter: jest.fn(),
33
34
  onMetadataUpdate: jest.fn(),
34
35
  onSortChange: jest.fn(),
35
36
  portalElement: null,
@@ -468,10 +468,9 @@ describe('elements/content-explorer/ContentExplorer', () => {
468
468
  })),
469
469
  ];
470
470
  const defaultView = 'metadata';
471
- const metadataViewV2ElementProps = {
471
+ const metadataViewV2ElementProps: Partial<ContentExplorerProps> = {
472
472
  metadataViewProps: {
473
473
  columns,
474
- metadataTemplate: mockSchema,
475
474
  tableProps: {
476
475
  isSelectAllEnabled: true,
477
476
  },
@@ -496,9 +495,7 @@ describe('elements/content-explorer/ContentExplorer', () => {
496
495
  expect(screen.queryByRole('button', { name: 'Switch to Grid View' })).toBeInTheDocument();
497
496
  });
498
497
 
499
- await waitFor(() => {
500
- expect(screen.getByRole('row', { name: /Child 2/i })).toBeInTheDocument();
501
- });
498
+ expect(screen.getByRole('row', { name: /Child 2/i })).toBeInTheDocument();
502
499
 
503
500
  const selectAllCheckbox = screen.getByLabelText('Select all');
504
501
  await userEvent.click(selectAllCheckbox);