@truedat/core 6.1.0 → 6.1.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.
- package/package.json +3 -3
- package/src/components/Pagination.js +1 -1
- package/src/search/FilterDropdown.js +76 -0
- package/src/search/FilterItem.js +49 -0
- package/src/search/FilterMultilevelDropdown.js +202 -0
- package/src/search/HierarchyFilterDropdown.js +95 -0
- package/src/search/SearchContext.js +234 -0
- package/src/search/SearchFilters.js +60 -0
- package/src/search/SearchSelectedFilters.js +55 -0
- package/src/search/SearchWidget.js +30 -0
- package/src/truedatConfig.js +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
4
4
|
"description": "Truedat Web Core",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"jsnext:main": "src/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@testing-library/jest-dom": "^5.16.5",
|
|
36
36
|
"@testing-library/react": "^12.0.0",
|
|
37
37
|
"@testing-library/user-event": "^13.2.1",
|
|
38
|
-
"@truedat/test": "6.
|
|
38
|
+
"@truedat/test": "6.1.2",
|
|
39
39
|
"babel-jest": "^28.1.0",
|
|
40
40
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
41
41
|
"babel-plugin-lodash": "^3.3.4",
|
|
@@ -117,5 +117,5 @@
|
|
|
117
117
|
"react-dom": ">= 16.8.6 < 17",
|
|
118
118
|
"semantic-ui-react": ">= 2.0.3 < 2.2"
|
|
119
119
|
},
|
|
120
|
-
"gitHead": "
|
|
120
|
+
"gitHead": "34f3dad97550142a4a23048fee3214ce04e957bf"
|
|
121
121
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { FormattedMessage } from "react-intl";
|
|
4
|
+
import { Label, Icon, Dropdown, Dimmer, Loader } from "semantic-ui-react";
|
|
5
|
+
import { accentInsensitivePathOrder } from "@truedat/core/services/sort";
|
|
6
|
+
import FilterItem from "./FilterItem";
|
|
7
|
+
|
|
8
|
+
import { useSearchContext } from "./SearchContext";
|
|
9
|
+
|
|
10
|
+
const removePrefix = _.replace(/^.*\./, "");
|
|
11
|
+
|
|
12
|
+
export default function FilterDropdown() {
|
|
13
|
+
const {
|
|
14
|
+
loadingFilters: loading,
|
|
15
|
+
filter,
|
|
16
|
+
options,
|
|
17
|
+
activeFilterSelectedValues,
|
|
18
|
+
|
|
19
|
+
openFilter,
|
|
20
|
+
closeFilter,
|
|
21
|
+
removeFilter,
|
|
22
|
+
toggleFilterValue,
|
|
23
|
+
} = useSearchContext();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Dropdown
|
|
27
|
+
item
|
|
28
|
+
floating
|
|
29
|
+
scrolling
|
|
30
|
+
icon={false}
|
|
31
|
+
upward={false}
|
|
32
|
+
trigger={
|
|
33
|
+
<Label key={filter}>
|
|
34
|
+
<FormattedMessage
|
|
35
|
+
id={`filters.${filter}`}
|
|
36
|
+
defaultMessage={removePrefix(filter)}
|
|
37
|
+
/>
|
|
38
|
+
<Icon
|
|
39
|
+
name="delete"
|
|
40
|
+
onClick={(e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
removeFilter({ filter });
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
</Label>
|
|
47
|
+
}
|
|
48
|
+
onOpen={() => openFilter({ filter })}
|
|
49
|
+
onClose={() => closeFilter({ filter })}
|
|
50
|
+
open={!_.isEmpty(options)}
|
|
51
|
+
>
|
|
52
|
+
<Dimmer.Dimmable as={Dropdown.Menu} dimmed={loading}>
|
|
53
|
+
{options &&
|
|
54
|
+
_.flow(
|
|
55
|
+
_.sortBy(accentInsensitivePathOrder("text")),
|
|
56
|
+
_.map.convert({ cap: false })((option, i) => (
|
|
57
|
+
<FilterItem
|
|
58
|
+
key={i}
|
|
59
|
+
filter={filter}
|
|
60
|
+
option={option}
|
|
61
|
+
toggleFilterValue={toggleFilterValue}
|
|
62
|
+
active={_.includes(_.prop("value")(option))(
|
|
63
|
+
activeFilterSelectedValues
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
))
|
|
67
|
+
)(options)}
|
|
68
|
+
{loading && (
|
|
69
|
+
<Dimmer active inverted>
|
|
70
|
+
<Loader size="tiny" />
|
|
71
|
+
</Dimmer>
|
|
72
|
+
)}
|
|
73
|
+
</Dimmer.Dimmable>
|
|
74
|
+
</Dropdown>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import { FormattedMessage } from "react-intl";
|
|
5
|
+
import { Icon, Dropdown } from "semantic-ui-react";
|
|
6
|
+
|
|
7
|
+
const FilterItemText = ({ filterName, text }) =>
|
|
8
|
+
_.trim(text) ? (
|
|
9
|
+
<FormattedMessage
|
|
10
|
+
id={`filters.${filterName}.${_.trim(text)}`}
|
|
11
|
+
defaultMessage={_.trim(text)}
|
|
12
|
+
/>
|
|
13
|
+
) : (
|
|
14
|
+
<i>
|
|
15
|
+
<FormattedMessage id="filter.empty" />
|
|
16
|
+
</i>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const preventDefault = (e, callback) => {
|
|
20
|
+
e && e.preventDefault();
|
|
21
|
+
e && e.stopPropagation();
|
|
22
|
+
callback();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const FilterItem = ({
|
|
26
|
+
active,
|
|
27
|
+
filter,
|
|
28
|
+
toggleFilterValue,
|
|
29
|
+
option: { text, value },
|
|
30
|
+
}) => (
|
|
31
|
+
<Dropdown.Item
|
|
32
|
+
onClick={(e) =>
|
|
33
|
+
preventDefault(e, () => toggleFilterValue({ filter, value }))
|
|
34
|
+
}
|
|
35
|
+
active={active}
|
|
36
|
+
>
|
|
37
|
+
<Icon name={active ? "check square outline" : "square outline"} />
|
|
38
|
+
<FilterItemText filterName={filter} text={text} />
|
|
39
|
+
</Dropdown.Item>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
FilterItem.propTypes = {
|
|
43
|
+
filterName: PropTypes.string,
|
|
44
|
+
text: PropTypes.string,
|
|
45
|
+
toggleFilterValue: PropTypes.func,
|
|
46
|
+
option: PropTypes.object,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default FilterItem;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Global configuration object written by td-web and read by td-web-modules.
|
|
3
|
+
* Using this instead of a global variable. This is written just once, so
|
|
4
|
+
* no need for propagation of change with Redux Store.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line fp/no-let
|
|
8
|
+
let config = {};
|
|
9
|
+
|
|
10
|
+
function setConfig(newConfig) {
|
|
11
|
+
// eslint-disable-next-line fp/no-mutation
|
|
12
|
+
config = newConfig;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { config, setConfig };
|