@blaze-cms/plugin-data-ui 0.147.0-rc-eagle.2 → 0.147.0-rc-eagle.4

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 (26) hide show
  1. package/lib/components/EntityManager/Entity/Entity.js +6 -7
  2. package/lib/components/EntityManager/Entity/Entity.js.map +1 -1
  3. package/lib/components/ListingTable/SearchFilter/SearchContainer.js +321 -49
  4. package/lib/components/ListingTable/SearchFilter/SearchContainer.js.map +1 -1
  5. package/lib/components/ListingTable/SearchFilter/SearchFilter.js +6 -2
  6. package/lib/components/ListingTable/SearchFilter/SearchFilter.js.map +1 -1
  7. package/lib/components/ListingTable/SearchFilter/helpers.js +41 -8
  8. package/lib/components/ListingTable/SearchFilter/helpers.js.map +1 -1
  9. package/lib/index.js +7 -0
  10. package/lib/index.js.map +1 -1
  11. package/lib-es/components/EntityManager/Entity/Entity.js +6 -7
  12. package/lib-es/components/EntityManager/Entity/Entity.js.map +1 -1
  13. package/lib-es/components/ListingTable/SearchFilter/SearchContainer.js +323 -51
  14. package/lib-es/components/ListingTable/SearchFilter/SearchContainer.js.map +1 -1
  15. package/lib-es/components/ListingTable/SearchFilter/SearchFilter.js +7 -3
  16. package/lib-es/components/ListingTable/SearchFilter/SearchFilter.js.map +1 -1
  17. package/lib-es/components/ListingTable/SearchFilter/helpers.js +39 -8
  18. package/lib-es/components/ListingTable/SearchFilter/helpers.js.map +1 -1
  19. package/lib-es/index.js +1 -0
  20. package/lib-es/index.js.map +1 -1
  21. package/package.json +11 -11
  22. package/src/components/EntityManager/Entity/Entity.js +11 -11
  23. package/src/components/ListingTable/SearchFilter/SearchContainer.js +363 -71
  24. package/src/components/ListingTable/SearchFilter/SearchFilter.js +5 -2
  25. package/src/components/ListingTable/SearchFilter/helpers.js +56 -8
  26. package/src/index.js +2 -0
@@ -1,10 +1,21 @@
1
1
  import React, { useState, useMemo, useEffect, useCallback } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import Select from '@blaze-react/select';
4
+ import MultiSelect from '@blaze-react/multiselect';
5
+ import debounce from 'lodash.debounce';
6
+ import unionBy from 'lodash.unionby';
7
+
4
8
  import { useQuery, useApolloClient } from '@apollo/client';
5
- import { getDynamicQuery, getQuery } from '@blaze-cms/admin-ui-utils';
9
+ import {
10
+ getDynamicQuery,
11
+ getQuery,
12
+ fetchFilterByDataFromElastic,
13
+ fetchAllEntities
14
+ } from '@blaze-cms/admin-ui-utils';
15
+ import { getStringTypeProps } from '@blaze-cms/utils';
16
+
6
17
  import { BsArrowCounterclockwise, BsSearch } from 'react-icons/bs';
7
- import { updateListFilters, buildQueryFields } from './helpers';
18
+ import { updateListFilters, buildQueryFields, getTidyLabel } from './helpers';
8
19
  import { NOOP_QUERY } from './querys';
9
20
  import { ENTER_KEY, DEFAULT_FILTER_OPTION_LABEL } from '../../../constants';
10
21
 
@@ -12,16 +23,39 @@ const SearchContainer = ({
12
23
  setListFilters,
13
24
  keywordFiltersToUse = [],
14
25
  selectFiltersToDisplay = [],
26
+ multiSelectFiltersToDisplay = [],
15
27
  schema,
16
- index
28
+ index,
29
+ values
17
30
  }) => {
18
- const apolloClient = useApolloClient();
31
+ const client = useApolloClient();
32
+
19
33
  const [searchTerm, setSearchTerm] = useState('');
20
34
  const [selectedFilters, setSelectedFilters] = useState({});
21
35
  const [selectOptions, setSelectOptions] = useState([]);
36
+ const [multiSelectOptions, setMultiSelectOptions] = useState([]);
37
+ const [allEntitySchemas, setAllEntitySchemas] = useState([]);
22
38
 
23
39
  const displaySearchFilter = keywordFiltersToUse.length > 0;
24
40
  const displaySelectFilters = selectFiltersToDisplay.length > 0;
41
+ const displayMultiSelectFilters = multiSelectFiltersToDisplay.length > 0;
42
+ const displayAggregations = displaySelectFilters || displayMultiSelectFilters;
43
+
44
+ useEffect(() => {
45
+ (async () => {
46
+ try {
47
+ const all = await fetchAllEntities({ client });
48
+ setAllEntitySchemas(all || []);
49
+ } catch (error) {
50
+ setAllEntitySchemas([]);
51
+ }
52
+ })();
53
+ }, [client]);
54
+
55
+ const multiSelectPropertySet = useMemo(
56
+ () => new Set(multiSelectFiltersToDisplay.map(([property]) => property)),
57
+ [multiSelectFiltersToDisplay]
58
+ );
25
59
 
26
60
  const { keywordSearchProperties, keywordSearchLabels } = useMemo(() => {
27
61
  const labels = [];
@@ -36,86 +70,140 @@ const SearchContainer = ({
36
70
  }, [keywordFiltersToUse]);
37
71
 
38
72
  const { gqlFields, rawQuery } = useMemo(
39
- () => buildQueryFields(selectFiltersToDisplay, schema.id),
40
- [selectFiltersToDisplay, schema.id]
73
+ () => buildQueryFields([...selectFiltersToDisplay, ...multiSelectFiltersToDisplay], schema.id),
74
+ [selectFiltersToDisplay, multiSelectFiltersToDisplay, schema.id]
41
75
  );
42
- const action = displaySelectFilters
76
+
77
+ const action = displayAggregations
43
78
  ? getDynamicQuery('ADMIN_SEARCH')([schema], gqlFields, false, index)
44
79
  : NOOP_QUERY;
45
80
 
46
81
  const { data } = useQuery(action, {
47
- skip: !displaySelectFilters,
82
+ skip: !displayAggregations,
48
83
  variables: { where: rawQuery, limit: 0 },
49
84
  fetchPolicy: 'cache-and-network'
50
85
  });
51
86
 
52
- const filterData = useMemo(() => {
87
+ const buildRelationsIndex = useCallback(schemaObj => {
88
+ const items = [];
89
+
90
+ if (schemaObj.properties) {
91
+ Object.entries(schemaObj.properties).forEach(([propName, definition]) => {
92
+ const { relation } = definition || {};
93
+ if (!relation) return;
94
+
95
+ const { localField: relationLocalField, entityIdentifier } = relation;
96
+
97
+ const localField = relationLocalField || propName;
98
+
99
+ if (localField) items.push({ localField, entityIdentifier });
100
+ });
101
+ }
102
+
103
+ if (Array.isArray(schemaObj.relations)) {
104
+ schemaObj.relations.forEach(relation => {
105
+ if (!relation.localField) return;
106
+
107
+ const { localField, entityIdentifier } = relation;
108
+
109
+ items.push({ localField, entityIdentifier });
110
+ });
111
+ }
112
+
113
+ const byKey = new Map();
114
+ items.forEach(item => {
115
+ const key = `${item.localField}|${item.entityIdentifier || ''}`;
116
+ if (!byKey.has(key)) byKey.set(key, item);
117
+ });
118
+
119
+ return Array.from(byKey.values());
120
+ }, []);
121
+
122
+ const relationsIndex = useMemo(() => buildRelationsIndex(schema), [schema, buildRelationsIndex]);
123
+
124
+ const stringTypeProps = useMemo(
125
+ () => getStringTypeProps(schema.id, relationsIndex, allEntitySchemas),
126
+ [schema.id, relationsIndex, allEntitySchemas]
127
+ );
128
+
129
+ const aggregationsData = useMemo(() => {
53
130
  const { searchResults: { rawResults: { aggregations = {} } = {} } = {} } = data || {};
54
- const proccessedData = [];
131
+ const processed = [];
55
132
  Object.entries(aggregations).forEach(([key, { buckets }]) => {
56
- proccessedData.push(
57
- Array.isArray(buckets) ? [key, buckets.map(b => b.key).filter(Boolean)] : []
133
+ processed.push(
134
+ Array.isArray(buckets) ? [key, buckets.map(bucket => bucket.key).filter(Boolean)] : []
58
135
  );
59
136
  });
60
-
61
- return proccessedData;
137
+ return processed;
62
138
  }, [data]);
63
139
 
140
+ const findFilterConfig = useCallback(
141
+ property =>
142
+ selectFiltersToDisplay.find(([prop]) => prop === property)?.[1] ||
143
+ multiSelectFiltersToDisplay.find(([prop]) => prop === property)?.[1],
144
+ [selectFiltersToDisplay, multiSelectFiltersToDisplay]
145
+ );
146
+
64
147
  useEffect(() => {
65
- if (!filterData.length) {
148
+ if (!aggregationsData.length) {
66
149
  setSelectOptions([]);
150
+ setMultiSelectOptions([]);
67
151
  return;
68
152
  }
69
153
 
70
154
  (async () => {
71
155
  const results = await Promise.all(
72
- filterData.map(async ([property, ids]) => {
156
+ aggregationsData.map(async ([property, ids]) => {
73
157
  try {
74
- if (!ids || !ids.length) return {};
158
+ if (!ids || !ids.length) return null;
75
159
 
76
- const [, selectionDetails] =
77
- selectFiltersToDisplay.find(([selectProperty]) => selectProperty === property) || [];
160
+ const config = findFilterConfig(property);
161
+ if (!config) return null;
78
162
 
79
- if (!selectionDetails) return null;
163
+ const { adminListingOptions: { tidyLabel } = {} } = config;
80
164
 
81
- if (!selectionDetails.relation) {
165
+ if (!config.relation) {
166
+ const baseOptions = ids.map(value => [value, getTidyLabel(value, tidyLabel)]);
82
167
  return {
83
- id: property,
84
- label: selectionDetails.label,
85
- options: ids.map(value => [value, value])
168
+ property,
169
+ label: config.label,
170
+ type: multiSelectPropertySet.has(property) ? 'multi' : 'single',
171
+ options: baseOptions,
172
+ selected: values && values[property]
86
173
  };
87
174
  }
88
- const query = getQuery('GET_ENTITY_SCHEMA');
175
+
176
+ const schemaQuery = getQuery('GET_ENTITY_SCHEMA');
89
177
  const {
90
178
  data: {
91
179
  getEntitySchemas: [relationSchema]
92
180
  }
93
- } = await apolloClient.query({
94
- query,
95
- variables: { identifier: selectionDetails.relation.entityIdentifier }
181
+ } = await client.query({
182
+ query: schemaQuery,
183
+ variables: { identifier: config.relation.entityIdentifier }
96
184
  });
97
185
 
98
186
  const {
99
187
  data: { result: selected = [] }
100
- } = await apolloClient
188
+ } = await client
101
189
  .query({
102
190
  query: getDynamicQuery('GET_ALL_ENTITIES')(relationSchema),
103
191
  variables: {
104
- where: {
105
- id: {
106
- _in: ids
107
- }
108
- },
192
+ where: { id: { _in: ids } },
109
193
  sort: [{ property: 'name', direction: 'asc' }]
110
194
  }
111
195
  })
112
196
  // eslint-disable-next-line no-console
113
- .catch(e => console.error(e));
197
+ .catch(error => console.error(error));
198
+
199
+ const baseOptions = selected.map(item => [item.id, getTidyLabel(item.name, tidyLabel)]);
114
200
 
115
201
  return {
116
- id: property,
117
- label: selectionDetails.label,
118
- options: selected.map(item => [item.id, item.name])
202
+ property,
203
+ label: config.label || config.relation.label,
204
+ type: multiSelectPropertySet.has(property) ? 'multi' : 'single',
205
+ options: baseOptions,
206
+ selected: values && values[property]
119
207
  };
120
208
  } catch {
121
209
  return null;
@@ -123,50 +211,215 @@ const SearchContainer = ({
123
211
  })
124
212
  );
125
213
 
126
- setSelectOptions(results.filter(Boolean));
214
+ const valid = results.filter(Boolean);
215
+
216
+ setSelectOptions(
217
+ valid
218
+ .filter(option => option.type === 'single')
219
+ .map(({ property, label, options, selected }) => ({
220
+ id: property,
221
+ label,
222
+ options,
223
+ selected
224
+ }))
225
+ );
226
+
227
+ setMultiSelectOptions(
228
+ valid
229
+ .filter(option => option.type === 'multi')
230
+ .map(({ property, label, options, selected }) => ({
231
+ id: property,
232
+ label,
233
+ items: options.map(([id, name]) => ({ id, name })),
234
+ selected
235
+ }))
236
+ );
127
237
  })();
128
- }, [apolloClient, filterData, schema, selectFiltersToDisplay]);
238
+ }, [client, aggregationsData, findFilterConfig, multiSelectPropertySet, values]);
129
239
 
130
240
  const updateFilters = useCallback(
131
241
  (term, filters) => updateListFilters(term, filters, setListFilters, keywordSearchProperties),
132
242
  [setListFilters, keywordSearchProperties]
133
243
  );
134
244
 
245
+ const debouncedUpdateFilters = useMemo(
246
+ () => debounce((term, filters) => updateFilters(term, filters), 300),
247
+ [updateFilters]
248
+ );
249
+
250
+ useEffect(() => () => debouncedUpdateFilters.cancel(), [debouncedUpdateFilters]);
251
+
135
252
  const handleSearch = useCallback(
136
253
  () => updateFilters(searchTerm, selectedFilters),
137
254
  [searchTerm, selectedFilters, updateFilters]
138
255
  );
139
- const handleSearchChange = useCallback(e => setSearchTerm(e.target.value), []);
256
+
257
+ const handleSearchChange = useCallback(({ target: { value } }) => setSearchTerm(value), []);
258
+
140
259
  const handleEnterKey = useCallback(
141
- e => {
142
- if (e.key === ENTER_KEY) handleSearch();
260
+ ({ key }) => {
261
+ if (key === ENTER_KEY) handleSearch();
143
262
  },
144
263
  [handleSearch]
145
264
  );
265
+
146
266
  const handleReset = useCallback(() => {
147
267
  setSearchTerm('');
148
- setSelectedFilters(
149
- Object.keys(selectedFilters).reduce(
150
- (acc, key) => ({ ...acc, [key]: DEFAULT_FILTER_OPTION_LABEL }),
151
- {}
152
- )
268
+ debouncedUpdateFilters.cancel();
269
+ setSelectedFilters(prev =>
270
+ Object.keys(prev).reduce((acc, key) => {
271
+ acc[key] = multiSelectPropertySet.has(key) ? [] : DEFAULT_FILTER_OPTION_LABEL;
272
+ return acc;
273
+ }, {})
153
274
  );
154
275
  updateFilters('', {});
155
- }, [selectedFilters, updateFilters]);
276
+ }, [multiSelectPropertySet, updateFilters, debouncedUpdateFilters]);
277
+
156
278
  const handleSelect = useCallback(
157
- (property, event) => {
158
- const { value } = event;
279
+ (property, { value }) => {
159
280
  const next = { ...selectedFilters, [property]: value };
160
281
  setSelectedFilters(next);
161
- updateFilters(searchTerm, next);
282
+ debouncedUpdateFilters(searchTerm, next);
162
283
  },
163
- [searchTerm, selectedFilters, updateFilters]
284
+ [searchTerm, selectedFilters, debouncedUpdateFilters]
285
+ );
286
+
287
+ const handleMultiSelect = useCallback(
288
+ (property, ids) => {
289
+ const next = { ...selectedFilters, [property]: ids };
290
+ setSelectedFilters(next);
291
+ debouncedUpdateFilters(searchTerm, next);
292
+ },
293
+ [searchTerm, selectedFilters, debouncedUpdateFilters]
294
+ );
295
+
296
+ const findRelationByLocalField = useCallback(
297
+ localField => relationsIndex.find(relation => relation.localField === localField),
298
+ [relationsIndex]
299
+ );
300
+
301
+ const resolveAggProp = useCallback((property, config) => {
302
+ if (!config.relation) return property;
303
+ if (property.includes('.')) return property;
304
+ const localField = config.relation.localField || property;
305
+ return `${localField}.name`;
306
+ }, []);
307
+
308
+ const resolveEntityIdentifier = useCallback(
309
+ (property, config) => {
310
+ if (!config.relation) return undefined;
311
+ if (config.relation.entityIdentifier) return config.relation.entityIdentifier;
312
+
313
+ const localField = property.includes('.')
314
+ ? property.split('.')[0]
315
+ : config.relation.localField || property;
316
+
317
+ return findRelationByLocalField(localField).entityIdentifier;
318
+ },
319
+ [findRelationByLocalField]
320
+ );
321
+
322
+ const addEntitySuffixIfNeeded = useCallback((aggProp, entityIdentifier, config) => {
323
+ if (!config.relation || !entityIdentifier) return aggProp;
324
+ return `${aggProp}/${entityIdentifier}`;
325
+ }, []);
326
+
327
+ const makeOnSearchChange = useCallback(
328
+ property =>
329
+ async ({ event: { value }, currentOffset }) => {
330
+ try {
331
+ if (!value || currentOffset) return [];
332
+
333
+ const config = findFilterConfig(property);
334
+ if (!config) return [];
335
+
336
+ let aggProp = resolveAggProp(property, config);
337
+
338
+ if (config.relation && !config.relation.localField && !property.includes('.')) {
339
+ const matchingRelation = findRelationByLocalField(property);
340
+ if (matchingRelation.localField) aggProp = `${matchingRelation.localField}.name`;
341
+ }
342
+
343
+ const entityIdentifier =
344
+ resolveEntityIdentifier(property, config) || config.relation?.entityIdentifier;
345
+
346
+ const filterByEntry = addEntitySuffixIfNeeded(aggProp, entityIdentifier, config);
347
+
348
+ const currentValues = {
349
+ entity: schema.id,
350
+ filterByProperty: [filterByEntry]
351
+ };
352
+
353
+ const aggregations = await fetchFilterByDataFromElastic(
354
+ client,
355
+ currentValues,
356
+ relationsIndex,
357
+ stringTypeProps,
358
+ value
359
+ );
360
+
361
+ const key = aggProp;
362
+ const buckets = aggregations?.[key]?.buckets || [];
363
+
364
+ let items = [];
365
+ if (config.relation) {
366
+ const names = buckets.map(bucket => bucket.key).filter(Boolean);
367
+ if (names.length) {
368
+ const schemaQuery = getQuery('GET_ENTITY_SCHEMA');
369
+ const {
370
+ data: {
371
+ getEntitySchemas: [relationSchema]
372
+ }
373
+ } = await client.query({
374
+ query: schemaQuery,
375
+ variables: { identifier: entityIdentifier }
376
+ });
377
+
378
+ const {
379
+ data: { result: selected = [] }
380
+ } = await client
381
+ .query({
382
+ query: getDynamicQuery('GET_ALL_ENTITIES')(relationSchema),
383
+ variables: {
384
+ where: { name: { _in: names } },
385
+ sort: [{ property: 'name', direction: 'asc' }]
386
+ }
387
+ })
388
+ // eslint-disable-next-line no-console
389
+ .catch(error => console.error(error));
390
+
391
+ items = selected.map(item => ({ id: item.id, name: item.name }));
392
+ }
393
+ } else {
394
+ items = buckets
395
+ .map(bucket => ({ id: bucket.key, name: bucket.key }))
396
+ .filter(option => option.id != null);
397
+ }
398
+
399
+ const existingItems = multiSelectOptions.find(opt => opt.id === property)?.items || [];
400
+ return unionBy(items, existingItems, 'id');
401
+ } catch (error) {
402
+ return [];
403
+ }
404
+ },
405
+ [
406
+ client,
407
+ schema.id,
408
+ relationsIndex,
409
+ stringTypeProps,
410
+ multiSelectOptions,
411
+ findFilterConfig,
412
+ resolveAggProp,
413
+ resolveEntityIdentifier,
414
+ addEntitySuffixIfNeeded,
415
+ findRelationByLocalField
416
+ ]
164
417
  );
165
418
 
166
419
  const placeholderText = `Search ${keywordSearchLabels.join(', ')}`;
167
420
 
168
421
  return (
169
- <div className="search-container">
422
+ <div className="search-container ms-filter">
170
423
  {displaySearchFilter && (
171
424
  <div className="search-container__search-input-container">
172
425
  <input
@@ -184,23 +437,60 @@ const SearchContainer = ({
184
437
  )}
185
438
 
186
439
  {displaySelectFilters &&
187
- selectOptions.map(({ id, options, label }) => (
188
- <>
189
- {options && !!options.length && (
190
- <div className="search-container__select-wrapper">
191
- <Select
192
- label={label}
193
- id={id}
194
- name={id}
195
- onChange={event => handleSelect(id, event)}
196
- options={options}
197
- value={selectedFilters[id]}
198
- defaultTextValue={DEFAULT_FILTER_OPTION_LABEL}
199
- />
200
- </div>
201
- )}
202
- </>
203
- ))}
440
+ selectOptions.map(({ id, options, label, selected }) =>
441
+ options && options.length ? (
442
+ <div className="search-container__select-wrapper" key={`select-${id}`}>
443
+ <Select
444
+ label={label}
445
+ id={id}
446
+ name={id}
447
+ onChange={({ value }) => handleSelect(id, { value })}
448
+ options={options}
449
+ value={selectedFilters[id] !== undefined ? selectedFilters[id] : selected}
450
+ defaultTextValue={DEFAULT_FILTER_OPTION_LABEL}
451
+ />
452
+ </div>
453
+ ) : null
454
+ )}
455
+
456
+ {displayMultiSelectFilters &&
457
+ multiSelectOptions.map(({ id, label, items }) => {
458
+ const selectedIds = selectedFilters[id] ? selectedFilters[id] : [];
459
+ const multiSelectData = {
460
+ filterBy: ['name'],
461
+ identification: 'id',
462
+ keyValue: 'name',
463
+ data: items.map(({ id: itemId, name }) => ({
464
+ id: itemId,
465
+ name,
466
+ description: '',
467
+ checked: selectedIds.includes(itemId),
468
+ show: true
469
+ }))
470
+ };
471
+
472
+ return (
473
+ <div className="search-container__select-wrapper" key={`multiselect-${id}`}>
474
+ <MultiSelect
475
+ onChange={({ value, currentOffset }) =>
476
+ makeOnSearchChange(id)({ event: { value }, currentOffset })
477
+ }
478
+ name={id}
479
+ label={label}
480
+ data={multiSelectData}
481
+ limit={10}
482
+ getSelected={({
483
+ event: {
484
+ target: { value }
485
+ }
486
+ }) => handleMultiSelect(id, value)}
487
+ required={false}
488
+ checkedPreviewCount={1}
489
+ formatMoreLabel={n => `${n} more`}
490
+ />
491
+ </div>
492
+ );
493
+ })}
204
494
 
205
495
  <div className="search-container__reset-button">
206
496
  <span role="button" onClick={handleReset}>
@@ -215,6 +505,7 @@ SearchContainer.propTypes = {
215
505
  setListFilters: PropTypes.func.isRequired,
216
506
  keywordFiltersToUse: PropTypes.array,
217
507
  selectFiltersToDisplay: PropTypes.array,
508
+ multiSelectFiltersToDisplay: PropTypes.array,
218
509
  schema: PropTypes.object.isRequired,
219
510
  index: PropTypes.string
220
511
  };
@@ -222,6 +513,7 @@ SearchContainer.propTypes = {
222
513
  SearchContainer.defaultProps = {
223
514
  keywordFiltersToUse: [],
224
515
  selectFiltersToDisplay: [],
516
+ multiSelectFiltersToDisplay: [],
225
517
  index: 'admin'
226
518
  };
227
519
 
@@ -1,9 +1,9 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { getKeywordSearchFilters, findSelectFilters } from './helpers';
3
+ import { getKeywordSearchFilters, findSelectFilters, findMultiSelectFilters } from './helpers';
4
4
  import SearchContainer from './SearchContainer';
5
5
 
6
- const SearchFilter = ({ schema, setListFilters }) => {
6
+ const SearchFilter = ({ schema, setListFilters, values }) => {
7
7
  const { displayProperties = {} } = schema || {};
8
8
  const {
9
9
  adminListings: { dataSource: { source, index } = {}, disableListingFilter = false } = {}
@@ -11,6 +11,7 @@ const SearchFilter = ({ schema, setListFilters }) => {
11
11
 
12
12
  const keywordFiltersToUse = useMemo(() => getKeywordSearchFilters(schema), [schema]);
13
13
  const selectFiltersToDisplay = useMemo(() => findSelectFilters(schema), [schema]);
14
+ const multiSelectFiltersToDisplay = useMemo(() => findMultiSelectFilters(schema), [schema]);
14
15
 
15
16
  if (disableListingFilter) return null;
16
17
 
@@ -25,7 +26,9 @@ const SearchFilter = ({ schema, setListFilters }) => {
25
26
  setListFilters={setListFilters}
26
27
  keywordFiltersToUse={keywordFiltersToUse}
27
28
  selectFiltersToDisplay={selectFiltersToDisplay}
29
+ multiSelectFiltersToDisplay={multiSelectFiltersToDisplay}
28
30
  schema={schema}
31
+ values={values}
29
32
  />
30
33
  );
31
34
  };
@@ -14,14 +14,32 @@ const updateListFilters = (searchTerm, selectedFilters, setListFilters, fields =
14
14
  }
15
15
 
16
16
  Object.entries(selectedFilters).forEach(([filterKey, filterValue]) => {
17
- if (!filterValue || filterValue === DEFAULT_FILTER_OPTION_LABEL) return;
18
- filters.push({
19
- match: {
20
- [`${filterKey}.keyword`]: {
21
- query: filterValue
22
- }
17
+ const isEmptyArray = Array.isArray(filterValue) && filterValue.length === 0;
18
+ if (
19
+ filterValue == null ||
20
+ filterValue === '' ||
21
+ filterValue === DEFAULT_FILTER_OPTION_LABEL ||
22
+ isEmptyArray
23
+ )
24
+ return;
25
+
26
+ const field = `${filterKey}.keyword`;
27
+
28
+ if (Array.isArray(filterValue)) {
29
+ if (filterValue.length > 1) {
30
+ filters.push({
31
+ terms: { [field]: filterValue }
32
+ });
33
+ } else {
34
+ filters.push({
35
+ term: { [field]: filterValue[0] }
36
+ });
23
37
  }
24
- });
38
+ } else {
39
+ filters.push({
40
+ term: { [field]: filterValue }
41
+ });
42
+ }
25
43
  });
26
44
 
27
45
  setListFilters(filters);
@@ -52,9 +70,23 @@ const findSelectFilters = schema => {
52
70
  return [...propertySelects, ...dynamicPropertySelects];
53
71
  };
54
72
 
73
+ const findMultiSelectFilters = schema => {
74
+ if (!schema || !schema.properties) return [];
75
+
76
+ const { properties = {}, dynamicProperties = {} } = schema;
77
+ const propertyMultiSelect = Object.entries(properties).filter(isFilterMultiSelectItems);
78
+ const dynamicPropertyMultiSelect = Object.entries(dynamicProperties).filter(
79
+ isFilterMultiSelectItems
80
+ );
81
+ return [...propertyMultiSelect, ...dynamicPropertyMultiSelect];
82
+ };
83
+
55
84
  const isFilterSelectItem = ([, item]) =>
56
85
  item.adminListingOptions && item.adminListingOptions.filterType === 'select';
57
86
 
87
+ const isFilterMultiSelectItems = ([, item]) =>
88
+ item.adminListingOptions && item.adminListingOptions.filterType === 'multiSelect';
89
+
58
90
  const buildQueryFields = (selectFilters, entityId) => {
59
91
  if (!Array.isArray(selectFilters) || selectFilters.length === 0) {
60
92
  return { gqlFields: '', rawQuery: '{}' };
@@ -96,4 +128,20 @@ const buildQueryFields = (selectFilters, entityId) => {
96
128
  return { gqlFields, rawQuery };
97
129
  };
98
130
 
99
- export { buildQueryFields, updateListFilters, getKeywordSearchFilters, findSelectFilters };
131
+ const getTidyLabel = (value, tidy) => {
132
+ if (!tidy) return value;
133
+
134
+ return value
135
+ .split(/[\s_-]+/)
136
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
137
+ .join(' ');
138
+ };
139
+
140
+ export {
141
+ buildQueryFields,
142
+ updateListFilters,
143
+ getKeywordSearchFilters,
144
+ findSelectFilters,
145
+ findMultiSelectFilters,
146
+ getTidyLabel
147
+ };
package/src/index.js CHANGED
@@ -14,6 +14,8 @@ import EntityManager from './components/EntityManager';
14
14
  // () => import(/* webpackChunkName: 'EntityManager' */ './components/EntityManager')
15
15
  // );
16
16
 
17
+ export { default as SearchFilter } from './components/ListingTable/SearchFilter/SearchFilter';
18
+
17
19
  export default async function load(app) {
18
20
  app.events.once('admin:menu:config:load', getAddContentMenuItems(app));
19
21