box-ui-elements 24.0.0-beta.5 → 24.0.0-beta.7
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.css +1 -1
- package/dist/explorer.js +1 -1
- package/dist/picker.js +1 -1
- package/es/elements/content-explorer/Content.js +3 -1
- package/es/elements/content-explorer/Content.js.map +1 -1
- package/es/elements/content-explorer/ContentExplorer.js +17 -6
- package/es/elements/content-explorer/ContentExplorer.js.map +1 -1
- package/es/elements/content-explorer/MetadataQueryAPIHelper.js +104 -7
- package/es/elements/content-explorer/MetadataQueryAPIHelper.js.map +1 -1
- package/es/elements/content-explorer/MetadataQueryBuilder.js +154 -0
- package/es/elements/content-explorer/MetadataQueryBuilder.js.map +1 -0
- package/es/elements/content-explorer/MetadataViewContainer.js +92 -46
- package/es/elements/content-explorer/MetadataViewContainer.js.map +1 -1
- package/es/elements/content-explorer/constants.js +4 -2
- package/es/elements/content-explorer/constants.js.map +1 -1
- package/es/elements/content-explorer/stories/MetadataView.stories.js +3 -25
- package/es/elements/content-explorer/stories/MetadataView.stories.js.map +1 -1
- package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js +4 -16
- package/es/elements/content-explorer/stories/tests/MetadataView-visual.stories.js.map +1 -1
- package/es/elements/content-explorer/utils.js +12 -0
- package/es/elements/content-explorer/utils.js.map +1 -1
- package/es/src/elements/common/__mocks__/mockMetadata.d.ts +8 -24
- package/es/src/elements/content-explorer/Content.d.ts +4 -3
- package/es/src/elements/content-explorer/ContentExplorer.d.ts +8 -3
- package/es/src/elements/content-explorer/MetadataQueryAPIHelper.d.ts +11 -2
- package/es/src/elements/content-explorer/MetadataQueryBuilder.d.ts +27 -0
- package/es/src/elements/content-explorer/MetadataViewContainer.d.ts +8 -4
- package/es/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.d.ts +1 -0
- package/es/src/elements/content-explorer/constants.d.ts +4 -2
- package/es/src/elements/content-explorer/utils.d.ts +2 -0
- package/i18n/bn-IN.js +1 -1
- package/i18n/bn-IN.properties +8 -0
- package/i18n/da-DK.js +1 -1
- package/i18n/da-DK.properties +8 -0
- package/i18n/de-DE.js +1 -1
- package/i18n/de-DE.properties +8 -0
- package/i18n/en-AU.js +1 -1
- package/i18n/en-AU.properties +8 -0
- package/i18n/en-CA.js +1 -1
- package/i18n/en-CA.properties +8 -0
- package/i18n/en-GB.js +1 -1
- package/i18n/en-GB.properties +8 -0
- package/i18n/es-419.js +1 -1
- package/i18n/es-419.properties +8 -0
- package/i18n/es-ES.js +1 -1
- package/i18n/es-ES.properties +8 -0
- package/i18n/fi-FI.js +1 -1
- package/i18n/fi-FI.properties +8 -0
- package/i18n/fr-CA.js +1 -1
- package/i18n/fr-CA.properties +8 -0
- package/i18n/fr-FR.js +1 -1
- package/i18n/fr-FR.properties +8 -0
- package/i18n/hi-IN.js +1 -1
- package/i18n/hi-IN.properties +8 -0
- package/i18n/it-IT.js +1 -1
- package/i18n/it-IT.properties +8 -0
- package/i18n/ja-JP.js +1 -1
- package/i18n/ja-JP.properties +8 -0
- package/i18n/ko-KR.js +1 -1
- package/i18n/ko-KR.properties +8 -0
- package/i18n/nb-NO.js +1 -1
- package/i18n/nb-NO.properties +8 -0
- package/i18n/nl-NL.js +1 -1
- package/i18n/nl-NL.properties +8 -0
- package/i18n/pl-PL.js +1 -1
- package/i18n/pl-PL.properties +8 -0
- package/i18n/pt-BR.js +1 -1
- package/i18n/pt-BR.properties +8 -0
- package/i18n/ru-RU.js +1 -1
- package/i18n/ru-RU.properties +8 -0
- package/i18n/sv-SE.js +1 -1
- package/i18n/sv-SE.properties +8 -0
- package/i18n/tr-TR.js +1 -1
- package/i18n/tr-TR.properties +8 -0
- package/i18n/zh-CN.js +1 -1
- package/i18n/zh-CN.properties +8 -0
- package/i18n/zh-TW.js +1 -1
- package/i18n/zh-TW.properties +8 -0
- package/package.json +3 -3
- package/src/elements/common/__mocks__/mockMetadata.ts +7 -11
- package/src/elements/content-explorer/Content.tsx +8 -2
- package/src/elements/content-explorer/ContentExplorer.tsx +209 -194
- package/src/elements/content-explorer/MetadataQueryAPIHelper.ts +111 -5
- package/src/elements/content-explorer/MetadataQueryBuilder.ts +194 -0
- package/src/elements/content-explorer/MetadataViewContainer.tsx +112 -37
- package/src/elements/content-explorer/__tests__/Content.test.tsx +1 -0
- package/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx +2 -5
- package/src/elements/content-explorer/__tests__/MetadataQueryAPIHelper.test.ts +427 -8
- package/src/elements/content-explorer/__tests__/MetadataQueryBuilder.test.ts +535 -0
- package/src/elements/content-explorer/__tests__/MetadataViewContainer.test.tsx +413 -9
- package/src/elements/content-explorer/constants.ts +39 -2
- package/src/elements/content-explorer/stories/MetadataView.stories.tsx +3 -21
- package/src/elements/content-explorer/stories/tests/MetadataView-visual.stories.tsx +2 -13
- package/src/elements/content-explorer/utils.ts +17 -0
|
@@ -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 {
|
|
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(
|
|
301
|
-
clonedFields.push(
|
|
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,194 @@
|
|
|
1
|
+
import { BoxItemSelection } from '@box/box-item-type-selector';
|
|
2
|
+
import isNil from 'lodash/isNil';
|
|
3
|
+
import { mapFileTypes } from './utils';
|
|
4
|
+
|
|
5
|
+
type QueryResult = {
|
|
6
|
+
queryParams: { [key: string]: number | Date | string };
|
|
7
|
+
queries: string[];
|
|
8
|
+
keysGenerated: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Custom type for range filters
|
|
12
|
+
type SimpleRangeType = {
|
|
13
|
+
range: {
|
|
14
|
+
gt: number | string;
|
|
15
|
+
lt: number | string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Union type for filter values
|
|
20
|
+
type SimpleFilterValue = string[] | SimpleRangeType;
|
|
21
|
+
|
|
22
|
+
export const mergeQueryParams = (
|
|
23
|
+
targetParams: { [key: string]: number | Date | string },
|
|
24
|
+
sourceParams: { [key: string]: number | Date | string },
|
|
25
|
+
): { [key: string]: number | Date | string } => {
|
|
26
|
+
return { ...targetParams, ...sourceParams };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const mergeQueries = (targetQueries: string[], sourceQueries: string[]): string[] => {
|
|
30
|
+
return [...targetQueries, ...sourceQueries];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const generateArgKey = (key: string, index: number): string => {
|
|
34
|
+
const purifyKey = key.replace(/[^\w]/g, '_');
|
|
35
|
+
return `arg_${purifyKey}_${index}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const escapeValue = (value: string): string => value.replace(/([_%])/g, '\\$1');
|
|
39
|
+
|
|
40
|
+
export const getStringFilter = (filterValue: string, fieldKey: string, argIndexStart: number): QueryResult => {
|
|
41
|
+
let currentArgIndex = argIndexStart;
|
|
42
|
+
|
|
43
|
+
const argKey = generateArgKey(fieldKey, (currentArgIndex += 1));
|
|
44
|
+
return {
|
|
45
|
+
queryParams: { [argKey]: `%${escapeValue(filterValue)}%` },
|
|
46
|
+
queries: [`(${fieldKey} ILIKE :${argKey})`],
|
|
47
|
+
keysGenerated: currentArgIndex - argIndexStart,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const isInvalid = (value: number | string) => {
|
|
52
|
+
return isNil(value) || value === '';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const getRangeFilter = (
|
|
56
|
+
filterValue: SimpleFilterValue,
|
|
57
|
+
fieldKey: string,
|
|
58
|
+
argIndexStart: number,
|
|
59
|
+
): QueryResult => {
|
|
60
|
+
let currentArgIndex = argIndexStart;
|
|
61
|
+
|
|
62
|
+
if (filterValue && typeof filterValue === 'object' && 'range' in filterValue && filterValue.range) {
|
|
63
|
+
const { gt, lt } = filterValue.range;
|
|
64
|
+
const queryParams: { [key: string]: number | string } = {};
|
|
65
|
+
const queries: string[] = [];
|
|
66
|
+
|
|
67
|
+
if (!isInvalid(gt) && !isInvalid(lt)) {
|
|
68
|
+
// Both gt and lt: between values
|
|
69
|
+
const argKeyGt = generateArgKey(fieldKey, (currentArgIndex += 1));
|
|
70
|
+
const argKeyLt = generateArgKey(fieldKey, (currentArgIndex += 1));
|
|
71
|
+
queryParams[argKeyGt] = gt;
|
|
72
|
+
queryParams[argKeyLt] = lt;
|
|
73
|
+
queries.push(`(${fieldKey} >= :${argKeyGt} AND ${fieldKey} <= :${argKeyLt})`);
|
|
74
|
+
} else if (!isInvalid(gt)) {
|
|
75
|
+
// Only gt: greater than
|
|
76
|
+
const argKey = generateArgKey(fieldKey, (currentArgIndex += 1));
|
|
77
|
+
queryParams[argKey] = gt;
|
|
78
|
+
queries.push(`(${fieldKey} >= :${argKey})`);
|
|
79
|
+
} else if (!isInvalid(lt)) {
|
|
80
|
+
// Only lt: less than
|
|
81
|
+
const argKey = generateArgKey(fieldKey, (currentArgIndex += 1));
|
|
82
|
+
queryParams[argKey] = lt;
|
|
83
|
+
queries.push(`(${fieldKey} <= :${argKey})`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
queryParams,
|
|
88
|
+
queries,
|
|
89
|
+
keysGenerated: currentArgIndex - argIndexStart,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
queryParams: {},
|
|
94
|
+
queries: [],
|
|
95
|
+
keysGenerated: 0,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const getSelectFilter = (filterValue: string[], fieldKey: string, argIndexStart: number): QueryResult => {
|
|
100
|
+
if (!Array.isArray(filterValue) || filterValue.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
queryParams: {},
|
|
103
|
+
queries: [],
|
|
104
|
+
keysGenerated: 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let currentArgIndex = argIndexStart;
|
|
109
|
+
|
|
110
|
+
const multiSelectQueryParams = Object.fromEntries(
|
|
111
|
+
filterValue.map(value => {
|
|
112
|
+
currentArgIndex += 1;
|
|
113
|
+
return [generateArgKey(fieldKey, currentArgIndex), String(value)];
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
queryParams: multiSelectQueryParams,
|
|
119
|
+
queries: [
|
|
120
|
+
`(${fieldKey} HASANY (${Object.keys(multiSelectQueryParams)
|
|
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
|
+
// Use mapFileTypes to get the correct extensions and handle special cases
|
|
138
|
+
const mappedExtensions = mapFileTypes(filterValue as BoxItemSelection);
|
|
139
|
+
if (mappedExtensions.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
queryParams: {},
|
|
142
|
+
queries: [],
|
|
143
|
+
keysGenerated: 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let currentArgIndex = argIndexStart;
|
|
148
|
+
const queryParams: { [key: string]: number | Date | string } = {};
|
|
149
|
+
const queries: string[] = [];
|
|
150
|
+
|
|
151
|
+
// Handle specific extensions and folder type
|
|
152
|
+
const extensions: string[] = [];
|
|
153
|
+
let hasFolder = false;
|
|
154
|
+
|
|
155
|
+
for (const extension of mappedExtensions) {
|
|
156
|
+
if (extension === 'folder') {
|
|
157
|
+
if (!hasFolder) {
|
|
158
|
+
currentArgIndex += 1;
|
|
159
|
+
const folderArgKey = generateArgKey('mime_folderType', currentArgIndex);
|
|
160
|
+
queryParams[folderArgKey] = 'folder';
|
|
161
|
+
queries.push(`(item.type = :${folderArgKey})`);
|
|
162
|
+
hasFolder = true;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
extensions.push(extension);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Handle extensions in batch if any exist
|
|
170
|
+
if (extensions.length > 0) {
|
|
171
|
+
const extensionQueryParams = Object.fromEntries(
|
|
172
|
+
extensions.map(extension => {
|
|
173
|
+
currentArgIndex += 1;
|
|
174
|
+
return [generateArgKey(fieldKey, currentArgIndex), extension];
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
Object.assign(queryParams, extensionQueryParams);
|
|
179
|
+
queries.push(
|
|
180
|
+
`(item.extension IN (${Object.keys(extensionQueryParams)
|
|
181
|
+
.map(argKey => `:${argKey}`)
|
|
182
|
+
.join(', ')}))`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Combine queries with OR if multiple exist
|
|
187
|
+
const finalQueries = queries.length > 1 ? [`(${queries.join(' OR ')})`] : queries;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
queryParams,
|
|
191
|
+
queries: finalQueries,
|
|
192
|
+
keysGenerated: currentArgIndex - argIndexStart,
|
|
193
|
+
};
|
|
194
|
+
};
|
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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
|
|
60
|
-
fields
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
?
|
|
66
|
-
:
|
|
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
|
|
89
|
-
|
|
90
|
-
const
|
|
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
|
-
|
|
175
|
+
fields?.map(field => {
|
|
96
176
|
return {
|
|
97
|
-
id:
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
196
|
+
onMetadataFilter(transformed);
|
|
197
|
+
if (onFilterSubmit) {
|
|
198
|
+
onFilterSubmit(transformed);
|
|
199
|
+
}
|
|
121
200
|
},
|
|
122
|
-
[
|
|
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,
|
|
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,
|
|
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={
|
|
244
|
+
columns={newColumns}
|
|
170
245
|
items={items}
|
|
171
246
|
tableProps={newTableProps}
|
|
172
|
-
{...
|
|
247
|
+
{...rest}
|
|
173
248
|
/>
|
|
174
249
|
);
|
|
175
250
|
};
|
|
@@ -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
|
-
|
|
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);
|