@truedat/core 6.0.5 → 6.1.1

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.
@@ -0,0 +1,202 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState, useEffect } from "react";
3
+ import { FormattedMessage } from "react-intl";
4
+ import {
5
+ Label,
6
+ Icon,
7
+ Input,
8
+ Dropdown,
9
+ Dimmer,
10
+ Loader,
11
+ } from "semantic-ui-react";
12
+ import { lowerDeburr } from "@truedat/core/services/sort";
13
+ import DropdownMenuItem from "@truedat/core/components/DropdownMenuItem";
14
+
15
+ import { useSearchContext } from "./SearchContext";
16
+
17
+ export default function FilterMultilevelDropdown() {
18
+ const {
19
+ name,
20
+ loadingFilters: loading,
21
+ filter,
22
+ options,
23
+ activeFilterSelectedValues,
24
+
25
+ openFilter,
26
+ closeFilter,
27
+ removeFilter,
28
+ toggleFilterValue,
29
+ } = useSearchContext();
30
+
31
+ const [selected, setSelected] = useState();
32
+ const [query, setQuery] = useState();
33
+ const [open, setOpen] = useState([]);
34
+ const [displayed, setDisplayed] = useState([]);
35
+
36
+ useEffect(() => {
37
+ const activeOptions = _.filter((option) =>
38
+ _.includes(option.id)(activeFilterSelectedValues)
39
+ )(options);
40
+
41
+ _.flow(
42
+ _.reduce(
43
+ (acc, option) => [
44
+ ...acc,
45
+ option.id,
46
+ ..._.map("id")(option.descendents),
47
+ ],
48
+ []
49
+ ),
50
+ _.uniq,
51
+ setSelected
52
+ )(activeOptions);
53
+ if (_.isEmpty(open) && _.isEmpty(displayed)) {
54
+ const withAncestors = _.flow(
55
+ _.reduce(
56
+ (acc, option) => [...acc, ..._.map("id")(option.ancestors)],
57
+ []
58
+ ),
59
+ _.uniq
60
+ )(activeOptions);
61
+ const newDisplayed = [..._.map("id")(activeOptions), ...withAncestors];
62
+ !_.isEqual(open, withAncestors) && setOpen(withAncestors);
63
+ !_.isEqual(displayed, newDisplayed) && setDisplayed(newDisplayed);
64
+ }
65
+ }, [activeFilterSelectedValues, options, open, displayed]);
66
+
67
+ const handleOpen = (selection) => {
68
+ const option = _.find({ id: selection })(options);
69
+ const isOpen = _.contains(selection)(open);
70
+ const children = _.map("id")(option.children);
71
+ const descendents = _.map("id")(option.descendents);
72
+
73
+ if (isOpen) {
74
+ setOpen(_.without([selection, ...descendents])(open));
75
+ setDisplayed(_.without(descendents)(displayed));
76
+ } else {
77
+ setOpen(_.union([selection])(open));
78
+ setDisplayed(_.union(children)(displayed));
79
+ }
80
+ };
81
+
82
+ const handleClick = (e, selection) => {
83
+ const option = _.find({ id: selection })(options);
84
+ const ancestorsToDelete = _.intersection(_.map("id")(option.ancestors))(
85
+ activeFilterSelectedValues
86
+ );
87
+ if (!_.isEmpty(ancestorsToDelete)) {
88
+ const active = _.union([selection])(activeFilterSelectedValues);
89
+ const value = _.without(ancestorsToDelete)(active);
90
+ toggleFilterValue({ filter, value });
91
+ } else if (_.includes(selection)(activeFilterSelectedValues)) {
92
+ const descendents = _.map("id")(option.descendents);
93
+ const value = _.without([selection, ...descendents])(
94
+ activeFilterSelectedValues
95
+ );
96
+ toggleFilterValue({ filter, value });
97
+ } else {
98
+ const descendents = _.map("id")(option.descendents);
99
+ const active = _.without(descendents)(activeFilterSelectedValues);
100
+ const value = _.union([selection])(active);
101
+ toggleFilterValue({ filter, value });
102
+ }
103
+ };
104
+
105
+ const displayAll = () => {
106
+ const ids = _.map("id")(options);
107
+ setOpen(ids);
108
+ setDisplayed(ids);
109
+ };
110
+
111
+ const handleSearch = (e, { value }) => {
112
+ e.preventDefault();
113
+ setQuery(lowerDeburr(value));
114
+ if (!_.isEmpty(value)) {
115
+ displayAll();
116
+ }
117
+ };
118
+ const match = (name, query) => _.contains(query)(lowerDeburr(name));
119
+ const filterSearch = (all) => {
120
+ if (query) {
121
+ return _.filter(
122
+ (domain) =>
123
+ match(domain.name, query) ||
124
+ _.some((descendent) => match(descendent.name, query))(
125
+ domain.descendents
126
+ )
127
+ )(all);
128
+ }
129
+ return all;
130
+ };
131
+
132
+ const filterDisplayed = (all) =>
133
+ _.filter((domain) => domain.level == 0 || _.contains(domain.id)(displayed))(
134
+ all
135
+ );
136
+
137
+ const filteredOptions = _.flow(filterSearch, filterDisplayed)(options);
138
+
139
+ return (
140
+ <Dropdown
141
+ name={name || "filterMultilevelDropdown"}
142
+ item
143
+ floating
144
+ icon={false}
145
+ upward={false}
146
+ onOpen={() => openFilter({ filter })}
147
+ onClose={() => closeFilter({ filter })}
148
+ trigger={
149
+ <Label key={filter}>
150
+ <FormattedMessage id={`filters.${filter}`} defaultMessage={filter} />
151
+ <Icon
152
+ name="delete"
153
+ onClick={(e) => {
154
+ e.preventDefault();
155
+ e.stopPropagation();
156
+ removeFilter({ filter });
157
+ }}
158
+ />
159
+ </Label>
160
+ }
161
+ open={!_.isEmpty(options)}
162
+ >
163
+ <Dimmer.Dimmable dimmed={loading} as={Dropdown.Menu}>
164
+ <>
165
+ <Input
166
+ icon="search"
167
+ iconPosition="left"
168
+ className="search"
169
+ onKeyDown={(e) => {
170
+ if (e.key === " ") {
171
+ e.stopPropagation();
172
+ }
173
+ }}
174
+ onChange={handleSearch}
175
+ onClick={(e) => {
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ }}
179
+ />
180
+ <Dropdown.Menu scrolling>
181
+ {_.map.convert({ cap: false })((option, i) => (
182
+ <DropdownMenuItem
183
+ key={i}
184
+ onOpen={handleOpen}
185
+ onClick={handleClick}
186
+ open={_.contains(option.id)(open)}
187
+ canOpen={_.negate(_.isEmpty)(option.children)}
188
+ selected={_.contains(option.id)(selected)}
189
+ {...option}
190
+ />
191
+ ))(filteredOptions)}
192
+ </Dropdown.Menu>
193
+ </>
194
+ {loading && (
195
+ <Dimmer active inverted>
196
+ <Loader size="tiny" />
197
+ </Dimmer>
198
+ )}
199
+ </Dimmer.Dimmable>
200
+ </Dropdown>
201
+ );
202
+ }
@@ -0,0 +1,95 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { useHierarchy } from "@truedat/df/hooks/useHierarchies";
4
+ import {
5
+ getHierarchyOptions,
6
+ getKeyAndParents,
7
+ } from "@truedat/core/services/getHierarchyOptions";
8
+ import FilterMultilevelDropdown from "./FilterMultilevelDropdown";
9
+ import SearchContext, { useSearchContext } from "./SearchContext";
10
+
11
+ const PopulatedHierarchyFilterDropdown = () => {
12
+ const context = useSearchContext();
13
+
14
+ const { filter, options, toggleFilterValue, activeFilterSelectedValues } =
15
+ context;
16
+ const hierarchyId = _.flow(
17
+ _.reject((item) => _.values(item).includes(undefined)),
18
+ _.first,
19
+ _.prop("value"),
20
+ (value) => value.split("_"),
21
+ _.first
22
+ )(options);
23
+
24
+ const { data, error, loading: hierarchyLoading } = useHierarchy(hierarchyId);
25
+ if (error) return null;
26
+ if (hierarchyLoading) return null;
27
+
28
+ const hierarchyOptions = getHierarchyOptions(data?.nodes);
29
+
30
+ const includedKeys = _.flow(
31
+ _.map("value"),
32
+ _.flatMap(getKeyAndParents(hierarchyOptions)),
33
+ _.uniq
34
+ )(options);
35
+
36
+ const filterIncludedKeys = _.filter(({ key }) =>
37
+ _.includes(key)(includedKeys)
38
+ );
39
+
40
+ const filteredChildren = _.flow(_.prop("children"), filterIncludedKeys);
41
+
42
+ const filteredOptions = _.flow(
43
+ filterIncludedKeys,
44
+ _.map((node) => ({ ...node, children: filteredChildren(node) }))
45
+ )(hierarchyOptions);
46
+
47
+ const idActiveValues = _.map((key) =>
48
+ _.flow(_.find({ key }), _.prop("id"))(filteredOptions)
49
+ )(activeFilterSelectedValues);
50
+
51
+ const handleToggleFilterValue = ({ filter, value }) => {
52
+ const getChildrenKeys = (id) => {
53
+ const { key, descendents } = _.flow(
54
+ _.find({ id }),
55
+ _.pick(["key", "descendents"])
56
+ )(filteredOptions);
57
+ const descendentKeys = _.map("key")(descendents);
58
+ return [key, ...descendentKeys];
59
+ };
60
+ const newValue = _.flow(
61
+ _.flatMap(getChildrenKeys),
62
+ _.uniq,
63
+ _.reject(_.isNil)
64
+ )(value);
65
+ toggleFilterValue({ filter, value: newValue });
66
+ };
67
+
68
+ return (
69
+ <SearchContext.Provider
70
+ value={{
71
+ ...context,
72
+ name: "hierarchyFilterDropdown",
73
+ filter,
74
+ options: filteredOptions,
75
+ activeFilterSelectedValues: idActiveValues,
76
+ toggleFilterValue: handleToggleFilterValue,
77
+ }}
78
+ key={filter}
79
+ >
80
+ <FilterMultilevelDropdown />
81
+ </SearchContext.Provider>
82
+ );
83
+ };
84
+
85
+ const HierarchyFilterDropdown = () => {
86
+ const { options } = useSearchContext();
87
+
88
+ return _.isEmpty(options) ? (
89
+ <FilterMultilevelDropdown />
90
+ ) : (
91
+ <PopulatedHierarchyFilterDropdown />
92
+ );
93
+ };
94
+
95
+ export default HierarchyFilterDropdown;
@@ -0,0 +1,234 @@
1
+ import _ from "lodash/fp";
2
+ import React, {
3
+ useState,
4
+ useEffect,
5
+ useContext,
6
+ createContext,
7
+ useMemo,
8
+ } from "react";
9
+ import { useIntl } from "react-intl";
10
+
11
+ import {
12
+ toFilterValues,
13
+ formatFilterValues,
14
+ } from "@truedat/core/services/filters";
15
+ import { makeOption } from "@truedat/core/services/i18n";
16
+
17
+ const SearchContext = createContext();
18
+
19
+ export const SearchContextProvider = (props) => {
20
+ const children = _.prop("children")(props);
21
+ const initialSortColumn = _.prop("initialSortColumn")(props);
22
+ const initialSortDirection = _.prop("initialSortDirection")(props);
23
+ const defaultFilters = _.prop("defaultFilters")(props);
24
+ const useSearch = _.prop("useSearch")(props);
25
+ const useFilters = _.prop("useFilters")(props);
26
+ const pageSize = _.propOr(20, "pageSize")(props);
27
+
28
+ const { formatMessage } = useIntl();
29
+
30
+ const [loading, setLoading] = useState(true);
31
+ const [searchData, setSearchData] = useState([]);
32
+
33
+ const [filtersPayload, setFiltersPayload] = useState([]);
34
+ const [loadingFilters, setLoadingFilters] = useState(true);
35
+ const [query, setQuery] = useState("");
36
+ const [activeFilterName, setActiveFilterName] = useState([]);
37
+ const [allActiveFilters, setAllActiveFilters] = useState({});
38
+ const [hiddenFilters, setHiddenFilters] = useState({});
39
+
40
+ const [sortColumn, setSortColumn] = useState(initialSortColumn);
41
+ const [sortDirection, setSortDirection] = useState(initialSortDirection);
42
+ const [page, setPage] = useState(1);
43
+ const [size, setSize] = useState(pageSize);
44
+ const [count, setCount] = useState(0);
45
+
46
+ //STATE FUNCTIONS
47
+ const addFilter = ({ filter }) => {
48
+ setAllActiveFilters({ ...allActiveFilters, [filter]: [] });
49
+ setActiveFilterName(filter);
50
+ };
51
+ const resetFilters = () => setAllActiveFilters({});
52
+
53
+ const openFilter = ({ filter }) => setActiveFilterName(filter);
54
+ const closeFilter = () => {
55
+ setActiveFilterName(null);
56
+ setAllActiveFilters(_.pickBy(_.negate(_.isEmpty))(allActiveFilters));
57
+ };
58
+ const removeFilter = ({ filter }) => {
59
+ setAllActiveFilters(_.omit(filter)(allActiveFilters));
60
+ setActiveFilterName(activeFilterName == filter ? null : activeFilterName);
61
+ };
62
+
63
+ const toggleFilterValue = ({ filter, value }) => {
64
+ const values = _.propOr([], filter)(allActiveFilters);
65
+ const newValue = _.isArray(value)
66
+ ? value
67
+ : _.includes(value)(values)
68
+ ? _.without([value])(values)
69
+ : _.union([value])(values);
70
+
71
+ setAllActiveFilters({ ...allActiveFilters, [filter]: newValue });
72
+ };
73
+
74
+ const toggleHiddenFilterValue = ({ filter, value }) => {
75
+ const values = _.propOr([], filter)(hiddenFilters);
76
+ const newValue = _.isArray(value)
77
+ ? value
78
+ : _.includes(value)(values)
79
+ ? _.without([value])(values)
80
+ : _.union([value])(values);
81
+
82
+ console.log("toggleHiddenFilterValue", values, newValue);
83
+ setHiddenFilters({ ...hiddenFilters, [filter]: newValue });
84
+ };
85
+
86
+ //CALCULATIONS ON STATE
87
+
88
+ const selectPage = (props) => {
89
+ setPage(props.activePage);
90
+ };
91
+
92
+ const setCountData = (headers) => {
93
+ setCount(parseInt(_.propOr("0", "x-total-count")(headers)));
94
+ };
95
+ const selectedFilters = _.keys(allActiveFilters);
96
+
97
+ const filters = _.flow(
98
+ _.propOr({}, "data"),
99
+ _.omitBy(_.flow(_.propOr([], "values"), (values) => _.size(values) < 2))
100
+ )(filtersPayload);
101
+
102
+ const availableFilters = _.flow(_.keys, _.without(selectedFilters))(filters);
103
+ const filterTypes = _.mapValues("type")(filters);
104
+
105
+ const translations = (formatMessage) => ({
106
+ "status.raw": (v) => formatMessage({ id: v, defaultMessage: v }),
107
+ });
108
+
109
+ const activeFilterValues = _.flow(
110
+ _.propOr({ values: [] }, activeFilterName),
111
+ ({ values, type }) => ({
112
+ values: _.flow(
113
+ _.concat(_.prop(activeFilterName)(allActiveFilters)),
114
+ _.uniq
115
+ )(values),
116
+ type,
117
+ }),
118
+ formatFilterValues,
119
+ _.map(makeOption(translations(formatMessage), activeFilterName))
120
+ )(filters);
121
+
122
+ const activeFilterSelectedValues = _.flow(
123
+ _.propOr([], activeFilterName),
124
+ toFilterValues
125
+ )(allActiveFilters);
126
+
127
+ const searchMust = useMemo(
128
+ () => ({
129
+ ...defaultFilters,
130
+ ..._.pickBy(_.negate(_.isEmpty))(allActiveFilters),
131
+ }),
132
+ [allActiveFilters, defaultFilters]
133
+ );
134
+
135
+ const filterMust = useMemo(
136
+ () => ({
137
+ ...defaultFilters,
138
+ ..._.flow(
139
+ _.pickBy(_.negate(_.isEmpty)),
140
+ _.omit(activeFilterName)
141
+ )(allActiveFilters),
142
+ }),
143
+
144
+ [allActiveFilters, activeFilterName, defaultFilters]
145
+ );
146
+
147
+ const sort = useMemo(
148
+ () =>
149
+ sortColumn
150
+ ? {
151
+ [sortColumn]: sortDirection === "ascending" ? "asc" : "desc",
152
+ }
153
+ : null,
154
+ [sortColumn, sortDirection]
155
+ );
156
+ const { trigger: triggerFilters } = useFilters();
157
+ useEffect(() => {
158
+ setLoadingFilters(true);
159
+
160
+ const filterParam = {
161
+ ...(!_.isEmpty(query) && { query }),
162
+ must: filterMust,
163
+ };
164
+ triggerFilters(filterParam).then(({ data }) => {
165
+ setFiltersPayload(data);
166
+ setLoadingFilters(false);
167
+ });
168
+ }, [query, filterMust, triggerFilters]);
169
+
170
+ const { trigger: triggerSearch } = useSearch();
171
+ useEffect(() => {
172
+ setLoading(true);
173
+
174
+ const filterParam = {
175
+ ...(!_.isEmpty(query) && { query }),
176
+ must: searchMust,
177
+ sort,
178
+ page: page - 1,
179
+ size,
180
+ };
181
+ triggerSearch(filterParam).then(({ data, headers }) => {
182
+ setSearchData(data);
183
+ setCountData(headers);
184
+ setLoading(false);
185
+ });
186
+ }, [query, searchMust, sort, triggerSearch, defaultFilters, page]);
187
+
188
+ const context = {
189
+ disabled: false,
190
+ loadingFilters,
191
+
192
+ availableFilters,
193
+ selectedFilters,
194
+ filterTypes,
195
+
196
+ activeFilterName,
197
+ activeFilterSelectedValues,
198
+ activeFilterValues,
199
+ defaultFilters,
200
+ hiddenFilters,
201
+ query,
202
+ filterMust,
203
+
204
+ addFilter,
205
+ resetFilters,
206
+ openFilter,
207
+ closeFilter,
208
+ removeFilter,
209
+ toggleFilterValue,
210
+ toggleHiddenFilterValue,
211
+ searchMust,
212
+ setQuery,
213
+
214
+ searchData,
215
+ loading,
216
+
217
+ sortColumn,
218
+ sortDirection,
219
+ setSortColumn,
220
+ setSortDirection,
221
+ selectPage,
222
+ setSize,
223
+ count,
224
+ page,
225
+ size,
226
+ };
227
+
228
+ return (
229
+ <SearchContext.Provider value={context}>{children}</SearchContext.Provider>
230
+ );
231
+ };
232
+
233
+ export const useSearchContext = () => useContext(SearchContext);
234
+ export default SearchContext;
@@ -0,0 +1,60 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { Dropdown } from "semantic-ui-react";
4
+ import { FormattedMessage, useIntl } from "react-intl";
5
+ import { i18nOrder } from "@truedat/core/services/sort";
6
+ import { useSearchContext } from "./SearchContext";
7
+
8
+ const removePrefix = _.replace(/^.*\./, "");
9
+
10
+ export default function SearchFilters() {
11
+ const {
12
+ disabled,
13
+ availableFilters,
14
+ addFilter,
15
+ resetFilters,
16
+ loadingFilters: loading,
17
+ } = useSearchContext();
18
+
19
+ const { formatMessage } = useIntl();
20
+
21
+ return (
22
+ <Dropdown
23
+ button
24
+ className="icon"
25
+ disabled={disabled}
26
+ floating
27
+ icon="filter"
28
+ labeled
29
+ loading={loading}
30
+ scrolling
31
+ text={formatMessage({ id: "filters", defaultMessage: "Filters" })}
32
+ upward={false}
33
+ >
34
+ <Dropdown.Menu>
35
+ <Dropdown.Item onClick={resetFilters}>
36
+ <em>
37
+ <FormattedMessage
38
+ id="filters.reset"
39
+ defaultMessage="(reset filters)"
40
+ />
41
+ </em>
42
+ </Dropdown.Item>
43
+ {_.flow(
44
+ _.defaultTo([]),
45
+ _.sortBy(i18nOrder(formatMessage, "filters")),
46
+ _.map((filter) => (
47
+ <Dropdown.Item
48
+ key={filter}
49
+ text={formatMessage({
50
+ id: `filters.${filter}`,
51
+ defaultMessage: removePrefix(filter),
52
+ })}
53
+ onClick={() => addFilter({ filter })}
54
+ />
55
+ ))
56
+ )(availableFilters)}
57
+ </Dropdown.Menu>
58
+ </Dropdown>
59
+ );
60
+ }
@@ -0,0 +1,55 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { FormattedMessage } from "react-intl";
4
+ import FilterDropdown from "./FilterDropdown";
5
+ import FilterMultilevelDropdown from "./FilterMultilevelDropdown";
6
+ import HierarchyFilterDropdown from "./HierarchyFilterDropdown";
7
+ import SearchContext, { useSearchContext } from "./SearchContext";
8
+
9
+ export default function SearchSelectedFilters() {
10
+ const context = useSearchContext();
11
+ const {
12
+ selectedFilters,
13
+ resetFilters,
14
+ filterTypes,
15
+ activeFilterName,
16
+ activeFilterValues,
17
+ } = context;
18
+
19
+ return (
20
+ <>
21
+ <div className="selectedFilters">
22
+ {_.isEmpty(selectedFilters) ? null : (
23
+ <>
24
+ <div className="appliedFilters">
25
+ <FormattedMessage id="search.applied_filters" />
26
+ </div>
27
+ {selectedFilters.map((filter) => {
28
+ const filterType = _.prop(filter)(filterTypes);
29
+ const options = _.isEqual(filter, activeFilterName)
30
+ ? activeFilterValues
31
+ : null;
32
+ return (
33
+ <SearchContext.Provider
34
+ value={{ ...context, filter, options }}
35
+ key={filter}
36
+ >
37
+ {filterType === "domain" ? (
38
+ <FilterMultilevelDropdown />
39
+ ) : filterType === "hierarchy" ? (
40
+ <HierarchyFilterDropdown />
41
+ ) : (
42
+ <FilterDropdown />
43
+ )}
44
+ </SearchContext.Provider>
45
+ );
46
+ })}
47
+ <a className="resetFilters" onClick={() => resetFilters()}>
48
+ <FormattedMessage id="search.clear_filters" />
49
+ </a>
50
+ </>
51
+ )}
52
+ </div>
53
+ </>
54
+ );
55
+ }
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { Input } from "semantic-ui-react";
3
+ import { useIntl } from "react-intl";
4
+ import SearchFilters from "@truedat/core/search/SearchFilters";
5
+ import SearchSelectedFilters from "@truedat/core/search/SearchSelectedFilters";
6
+
7
+ import { useSearchContext } from "@truedat/core/search/SearchContext";
8
+
9
+ export default function SearchWidget() {
10
+ const { formatMessage } = useIntl();
11
+
12
+ const { query, setQuery, loadingFilters: loading } = useSearchContext();
13
+
14
+ return (
15
+ <>
16
+ <Input
17
+ value={query}
18
+ onChange={(_e, data) => setQuery(data.value)}
19
+ icon={{ name: "search", link: true }}
20
+ iconPosition="left"
21
+ action={<SearchFilters />}
22
+ placeholder={formatMessage({
23
+ id: "search.placeholder",
24
+ })}
25
+ loading={loading}
26
+ />
27
+ <SearchSelectedFilters />
28
+ </>
29
+ );
30
+ }