@blaze-cms/plugin-data-ui 0.146.0-rc-eagle.0 → 0.146.0-rc-eagle-3.2

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