@truedat/core 5.6.1 → 5.6.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "5.6.1",
3
+ "version": "5.6.3",
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": "5.6.1",
38
+ "@truedat/test": "5.6.3",
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": "a8e5efaf772f212a10192b36d853529890dfdea2"
120
+ "gitHead": "c79bc612b29e41c6c600c2e029ae762b36147193"
121
121
  }
@@ -33,11 +33,6 @@ const items = [
33
33
  routes: [GRANT_APPROVAL_RULES],
34
34
  groups: ["grants_management"],
35
35
  },
36
- {
37
- name: "structures_grant_requests",
38
- routes: [STRUCTURES_GRANT_REQUEST],
39
- groups: ["grants"],
40
- },
41
36
  ];
42
37
 
43
38
  export const GrantMenu = () => {
@@ -0,0 +1,116 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useHierarchy } from "@truedat/df/hooks/useHierarchies";
5
+ import {
6
+ getHierarchyOptions,
7
+ getKeyAndParents,
8
+ } from "../services/getHierarchyOptions";
9
+ import FilterMultilevelDropdown from "./FilterMultilevelDropdown";
10
+
11
+ const PopulatedHierarchyFilterDropdown = (props) => {
12
+ const {
13
+ activeValues,
14
+ closeFilter,
15
+ filter,
16
+ loading,
17
+ openFilter,
18
+ options,
19
+ removeFilter,
20
+ toggleFilterValue,
21
+ } = props;
22
+ const hierarchyId = _.flow(
23
+ _.first,
24
+ _.prop("value"),
25
+ (value) => value.split("_"),
26
+ _.first
27
+ )(options);
28
+
29
+ const { data, error, loading: hierarchyLoading } = useHierarchy(hierarchyId);
30
+ if (error) return null;
31
+ if (hierarchyLoading) return null;
32
+
33
+ const hierarchyOptions = getHierarchyOptions(data?.nodes);
34
+
35
+ const includedKeys = _.flow(
36
+ _.map("value"),
37
+ _.flatMap(getKeyAndParents(hierarchyOptions)),
38
+ _.uniq
39
+ )(options);
40
+
41
+ const filterIncludedKeys = _.filter(({ key }) =>
42
+ _.includes(key)(includedKeys)
43
+ );
44
+
45
+ const filteredChildren = _.flow(_.prop("children"), filterIncludedKeys);
46
+
47
+ const filteredOptions = _.flow(
48
+ filterIncludedKeys,
49
+ _.map((node) => ({ ...node, children: filteredChildren(node) }))
50
+ )(hierarchyOptions);
51
+
52
+ const idActiveValues = _.map((key) =>
53
+ _.flow(_.find({ key }), _.prop("id"))(filteredOptions)
54
+ )(activeValues);
55
+
56
+ const handleToggleFilterValue = ({ filter, value }) => {
57
+ const getChildrenKeys = (id) => {
58
+ const { key, descendents } = _.flow(
59
+ _.find({ id }),
60
+ _.pick(["key", "descendents"])
61
+ )(filteredOptions);
62
+ const descendentKeys = _.map("key")(descendents);
63
+ return [key, ...descendentKeys];
64
+ };
65
+ const newValue = _.flow(
66
+ _.flatMap(getChildrenKeys),
67
+ _.uniq,
68
+ _.reject(_.isNil)
69
+ )(value);
70
+ toggleFilterValue({ filter, value: newValue });
71
+ };
72
+
73
+ return (
74
+ <FilterMultilevelDropdown
75
+ activeValues={idActiveValues}
76
+ closeFilter={closeFilter}
77
+ filter={filter}
78
+ loading={loading}
79
+ openFilter={openFilter}
80
+ options={filteredOptions}
81
+ removeFilter={removeFilter}
82
+ toggleFilterValue={handleToggleFilterValue}
83
+ />
84
+ );
85
+ };
86
+
87
+ PopulatedHierarchyFilterDropdown.propTypes = {
88
+ activeValues: PropTypes.array,
89
+ closeFilter: PropTypes.func,
90
+ filter: PropTypes.string,
91
+ loading: PropTypes.bool,
92
+ openFilter: PropTypes.func,
93
+ options: PropTypes.array,
94
+ removeFilter: PropTypes.func,
95
+ toggleFilterValue: PropTypes.func,
96
+ };
97
+
98
+ const HierarchyFilterDropdown = (props) =>
99
+ _.isEmpty(props.options) ? (
100
+ <FilterMultilevelDropdown {...props} />
101
+ ) : (
102
+ <PopulatedHierarchyFilterDropdown {...props} />
103
+ );
104
+
105
+ HierarchyFilterDropdown.propTypes = {
106
+ activeValues: PropTypes.array,
107
+ closeFilter: PropTypes.func,
108
+ filter: PropTypes.string,
109
+ loading: PropTypes.bool,
110
+ openFilter: PropTypes.func,
111
+ options: PropTypes.array,
112
+ removeFilter: PropTypes.func,
113
+ toggleFilterValue: PropTypes.func,
114
+ };
115
+
116
+ export default HierarchyFilterDropdown;
@@ -4,6 +4,7 @@ import PropTypes from "prop-types";
4
4
  import { FormattedMessage } from "react-intl";
5
5
  import FilterDropdown from "./FilterDropdown";
6
6
  import FilterMultilevelDropdown from "./FilterMultilevelDropdown";
7
+ import HierarchyFilterDropdown from "./HierarchyFilterDropdown";
7
8
  import ModalSaveFilter from "./ModalSaveFilter";
8
9
  import UserFilters from "./UserFilters";
9
10
 
@@ -19,6 +20,7 @@ export const SelectedFilters = ({
19
20
  saveFilters,
20
21
  selectedFilter,
21
22
  selectedFilters,
23
+ filterTypes,
22
24
  selectedFilterActiveValues: activeValues,
23
25
  selectedFilterValues,
24
26
  selectedUserFilter,
@@ -45,9 +47,11 @@ export const SelectedFilters = ({
45
47
  <FormattedMessage id="search.applied_filters" />
46
48
  </div>
47
49
  {selectedFilters.map((filter) => {
50
+ const filterType = _.prop(filter)(filterTypes);
48
51
  const options = _.isEqual(filter, selectedFilter)
49
52
  ? selectedFilterValues
50
53
  : null;
54
+
51
55
  const props = {
52
56
  key: filter,
53
57
  activeValues,
@@ -59,8 +63,10 @@ export const SelectedFilters = ({
59
63
  removeFilter,
60
64
  toggleFilterValue,
61
65
  };
62
- return selectedFilter === "taxonomy" ? (
66
+ return filterType === "domain" ? (
63
67
  <FilterMultilevelDropdown {...props} />
68
+ ) : filterType === "hierarchy" ? (
69
+ <HierarchyFilterDropdown {...props} />
64
70
  ) : (
65
71
  <FilterDropdown {...props} />
66
72
  );
@@ -95,6 +101,7 @@ SelectedFilters.propTypes = {
95
101
  selectedFilterActiveValues: PropTypes.array,
96
102
  selectedFilterValues: PropTypes.array,
97
103
  selectedFilters: PropTypes.array,
104
+ filterTypes: PropTypes.object,
98
105
  selectedUserFilter: PropTypes.string,
99
106
  toggleFilterValue: PropTypes.func,
100
107
  userFilters: PropTypes.array,
@@ -0,0 +1,190 @@
1
+ import React from "react";
2
+ import { waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { render } from "@truedat/test/render";
5
+ import HierarchyFilterDropdown from "../HierarchyFilterDropdown";
6
+
7
+ const hierarchy = {
8
+ id: 123,
9
+ nodes: [
10
+ {
11
+ id: 1,
12
+ key: "123_1",
13
+ parentId: null,
14
+ name: "foo",
15
+ },
16
+ {
17
+ id: 2,
18
+ key: "123_2",
19
+ parentId: 1,
20
+ name: "bar",
21
+ },
22
+ {
23
+ id: 3,
24
+ key: "123_3",
25
+ parentId: null,
26
+ name: "baz",
27
+ },
28
+ ],
29
+ };
30
+
31
+ jest.mock("@truedat/df/hooks/useHierarchies", () => {
32
+ const originalModule = jest.requireActual("@truedat/df/hooks/useHierarchies");
33
+
34
+ return {
35
+ __esModule: true,
36
+ ...originalModule,
37
+ useHierarchy: jest.fn(() => ({
38
+ data: hierarchy,
39
+ loading: false,
40
+ })),
41
+ };
42
+ });
43
+
44
+ describe("<HierarchyFilterDropdown />", () => {
45
+ const toggleFilterValue = jest.fn();
46
+ const openFilter = jest.fn();
47
+ const closeFilter = jest.fn();
48
+ const removeFilter = jest.fn();
49
+ const options = [
50
+ {
51
+ value: "123_2",
52
+ text: "bar",
53
+ },
54
+ ];
55
+ const filter = "hierarchy_field";
56
+ const props = {
57
+ filter,
58
+ closeFilter,
59
+ options,
60
+ openFilter,
61
+ toggleFilterValue,
62
+ loading: false,
63
+ removeFilter,
64
+ filterTypes: { hierarchy_field: "hierarchy" },
65
+ };
66
+
67
+ const messages = {
68
+ en: {
69
+ "filter.hierarchy_field": "Hierarchy Field",
70
+ },
71
+ };
72
+
73
+ it("matches the latest snapshot", () => {
74
+ const { container } = render(<HierarchyFilterDropdown {...props} />, {
75
+ messages,
76
+ });
77
+ expect(container).toMatchSnapshot();
78
+ });
79
+
80
+ it("matches the latest snapshot for empty options", () => {
81
+ const customProps = {
82
+ ...props,
83
+ options: [],
84
+ };
85
+ const { container } = render(<HierarchyFilterDropdown {...customProps} />, {
86
+ messages,
87
+ });
88
+ expect(container).toMatchSnapshot();
89
+ });
90
+
91
+ it("handles the selection of a colapsed element", async () => {
92
+ const { rerender, getByRole, container, queryByText } = render(
93
+ <HierarchyFilterDropdown {...props} />,
94
+ {
95
+ messages,
96
+ }
97
+ );
98
+
99
+ userEvent.click(getByRole("listbox"));
100
+
101
+ userEvent.click(container.querySelector('[class="plus icon"]'));
102
+ await waitFor(() => {
103
+ expect(getByRole("option", { name: /bar/i })).toBeTruthy();
104
+ });
105
+ userEvent.click(container.querySelector('[class="minus icon"]'));
106
+ await waitFor(() => {
107
+ expect(queryByText(/bar/)).toBeFalsy();
108
+ });
109
+ userEvent.click(container.querySelector('[class="plus icon"]'));
110
+ // Select value
111
+ userEvent.click(getByRole("option", { name: /bar/i }));
112
+ await waitFor(() => {
113
+ expect(toggleFilterValue).toHaveBeenCalledWith({
114
+ filter,
115
+ value: ["123_2"],
116
+ });
117
+ });
118
+ rerender(
119
+ <HierarchyFilterDropdown {...{ ...props, activeValues: ["123_2"] }} />
120
+ );
121
+ // Unselect
122
+ userEvent.click(getByRole("option", { name: /bar/i }));
123
+ await waitFor(() => {
124
+ expect(toggleFilterValue).toHaveBeenCalledWith({ filter, value: [] });
125
+ });
126
+ // Select parent
127
+ rerender(<HierarchyFilterDropdown {...{ ...props, activeValues: [] }} />);
128
+ userEvent.click(getByRole("option", { name: /foo/i }));
129
+ await waitFor(() => {
130
+ expect(toggleFilterValue).toHaveBeenCalledWith({
131
+ filter,
132
+ value: ["123_1", "123_2"],
133
+ });
134
+ });
135
+ // Unselect parent by selecting children
136
+ rerender(
137
+ <HierarchyFilterDropdown
138
+ {...{ ...props, activeValues: ["123_1", "123_2"] }}
139
+ />
140
+ );
141
+ userEvent.click(getByRole("option", { name: /bar/i }));
142
+ await waitFor(() => {
143
+ expect(toggleFilterValue).toHaveBeenCalledWith({
144
+ filter,
145
+ value: ["123_2"],
146
+ });
147
+ });
148
+ // Delete filter
149
+ userEvent.click(container.querySelector('[class="delete icon"]'));
150
+ await waitFor(() => {
151
+ expect(removeFilter).toBeCalledTimes(1);
152
+ });
153
+ });
154
+
155
+ it("handles the selection of searched elements", async () => {
156
+ const customProps = {
157
+ ...props,
158
+ options: [
159
+ {
160
+ value: "123_2",
161
+ text: "bar",
162
+ },
163
+ {
164
+ value: "123_3",
165
+ text: "baz",
166
+ },
167
+ ],
168
+ };
169
+ const { container, getByRole } = render(
170
+ <HierarchyFilterDropdown {...customProps} />,
171
+ {
172
+ messages,
173
+ }
174
+ );
175
+
176
+ userEvent.click(getByRole("listbox"));
177
+
178
+ await waitFor(() => {
179
+ expect(getByRole("option", { name: /foo/i })).toBeTruthy();
180
+ expect(getByRole("option", { name: /baz/i })).toBeTruthy();
181
+ });
182
+
183
+ const input = container.querySelector('[type="text"]');
184
+ userEvent.type(input, "bar");
185
+
186
+ await waitFor(() => {
187
+ expect(getByRole("option", { name: /bar/i })).toBeTruthy();
188
+ });
189
+ });
190
+ });
@@ -5,6 +5,9 @@ import { SelectedFilters } from "../SelectedFilters";
5
5
  describe("<SelectedFilters/>", () => {
6
6
  const getProps = () => ({
7
7
  resetFilters: jest.fn(),
8
+ filterTypes: {
9
+ foo: "fooType",
10
+ },
8
11
  selectedFilter: "foo",
9
12
  selectedFilters: ["foo", "bar"],
10
13
  selectedFilterActiveValues: ["value2"],
@@ -56,15 +59,29 @@ describe("<SelectedFilters/>", () => {
56
59
  expect(wrapper.find("UserFilters")).toHaveLength(1);
57
60
  });
58
61
 
59
- it("renders FilterMultilevelDropdown if filter is taxonomy", () => {
62
+ it("renders FilterMultilevelDropdown if filter is of type domain", () => {
60
63
  const props = getProps();
61
64
  const customProps = {
62
65
  ...props,
63
66
  selectedFilters: ["taxonomy"],
64
67
  selectedFilter: "taxonomy",
68
+ filterTypes: { taxonomy: "domain" },
65
69
  selectedFilterValues: [{ id: 1, name: "foo", level: 0 }],
66
70
  };
67
71
  const wrapper = shallowWithIntl(<SelectedFilters {...customProps} />);
68
72
  expect(wrapper.find("FilterMultilevelDropdown")).toHaveLength(1);
69
73
  });
74
+
75
+ it("renders HierarchyFilterDropdown if filter is of type hierarchy", () => {
76
+ const props = getProps();
77
+ const customProps = {
78
+ ...props,
79
+ selectedFilters: ["hierarchy_field"],
80
+ selectedFilter: "hierarchy_field",
81
+ filterTypes: { hierarchy_field: "hierarchy" },
82
+ selectedFilterValues: [{ key: "2_1", id: 1, name: "foo", level: 0 }],
83
+ };
84
+ const wrapper = shallowWithIntl(<SelectedFilters {...customProps} />);
85
+ expect(wrapper.find("HierarchyFilterDropdown")).toHaveLength(1);
86
+ });
70
87
  });
@@ -59,15 +59,6 @@ exports[`<GrantMenu /> matches the latest snapshot 1`] = `
59
59
  "/grantApprovalRules",
60
60
  ],
61
61
  },
62
- {
63
- "groups": [
64
- "grants",
65
- ],
66
- "name": "structures_grant_requests",
67
- "routes": [
68
- "/structuresGrantRequests",
69
- ],
70
- },
71
62
  ]
72
63
  }
73
64
  name="grants"
@@ -0,0 +1,99 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<HierarchyFilterDropdown /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ aria-expanded="true"
7
+ class="ui active visible floating item dropdown"
8
+ role="listbox"
9
+ tabindex="0"
10
+ >
11
+ <div
12
+ class="ui label"
13
+ >
14
+ hierarchy_field
15
+ <i
16
+ aria-hidden="true"
17
+ class="delete icon"
18
+ />
19
+ </div>
20
+ <div
21
+ class="menu transition dimmable visible"
22
+ >
23
+ <div
24
+ class="ui left icon input search"
25
+ >
26
+ <input
27
+ type="text"
28
+ />
29
+ <i
30
+ aria-hidden="true"
31
+ class="search icon"
32
+ />
33
+ </div>
34
+ <div
35
+ class="scrolling menu transition"
36
+ >
37
+ <div
38
+ aria-selected="false"
39
+ class="item"
40
+ role="option"
41
+ >
42
+ <div
43
+ style="margin-left: 0px;"
44
+ >
45
+ <i
46
+ aria-hidden="true"
47
+ class="plus icon"
48
+ />
49
+ <i
50
+ aria-hidden="true"
51
+ class="square outline icon"
52
+ />
53
+ foo
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ `;
61
+
62
+ exports[`<HierarchyFilterDropdown /> matches the latest snapshot for empty options 1`] = `
63
+ <div>
64
+ <div
65
+ aria-expanded="false"
66
+ class="ui floating item dropdown"
67
+ role="listbox"
68
+ tabindex="0"
69
+ >
70
+ <div
71
+ class="ui label"
72
+ >
73
+ hierarchy_field
74
+ <i
75
+ aria-hidden="true"
76
+ class="delete icon"
77
+ />
78
+ </div>
79
+ <div
80
+ class="menu transition dimmable"
81
+ >
82
+ <div
83
+ class="ui left icon input search"
84
+ >
85
+ <input
86
+ type="text"
87
+ />
88
+ <i
89
+ aria-hidden="true"
90
+ class="search icon"
91
+ />
92
+ </div>
93
+ <div
94
+ class="scrolling menu transition"
95
+ />
96
+ </div>
97
+ </div>
98
+ </div>
99
+ `;
@@ -30,8 +30,8 @@ export const getSearchPayload = (searchQuery, props) => {
30
30
 
31
31
  export const isEmbedded = _.overEvery(_.has("id"), _.has("name"));
32
32
 
33
- export const formatFilterValues = (selectedFilter) => (values) =>
34
- selectedFilter === "taxonomy"
33
+ export const formatFilterValues = ({ values, type }) =>
34
+ type === "domain"
35
35
  ? getDomainSelectorOptions({ domains: values })
36
36
  : _.any(isEmbedded)(values)
37
37
  ? _.map(({ id, name }) => ({ value: id, text: name }))(values)
@@ -0,0 +1,71 @@
1
+ import _ from "lodash/fp";
2
+ import { accentInsensitivePathOrder } from "@truedat/core/services/sort";
3
+
4
+ const isChildOf =
5
+ ({ id }) =>
6
+ ({ parentId }) =>
7
+ parentId === id;
8
+ const isParentOf =
9
+ ({ parentId }) =>
10
+ ({ id }) =>
11
+ parentId === id;
12
+
13
+ const hasNoParentIn = (domains) => (domain) =>
14
+ !_.some(isParentOf(domain))(domains);
15
+
16
+ const findParentsIn = (domains) => (child) =>
17
+ _.filter(isParentOf(child))(domains);
18
+ const findChildrenIn = (domains) => (parent) =>
19
+ _.filter(isChildOf(parent))(domains);
20
+
21
+ const findDescendents = (parents) => (domains) => {
22
+ const children = _.flatMap(findChildrenIn(domains))(parents);
23
+ return _.isEmpty(children)
24
+ ? children
25
+ : _.concat(children, findDescendents(children)(domains));
26
+ };
27
+
28
+ const findAncestorsIn = (domains) => (children) => {
29
+ const parents = _.flatMap(findParentsIn(domains))(children);
30
+ return _.isEmpty(parents)
31
+ ? parents
32
+ : _.concat(findAncestorsIn(domains)(parents), parents);
33
+ };
34
+
35
+ const reduceTaxonomy = (domains) => (domainsInLevel, level) => {
36
+ if (domains)
37
+ return _.reduce((acc, domain) => {
38
+ const children = _.sortBy(["name"])(findChildrenIn(domains)(domain));
39
+ return [
40
+ ...acc,
41
+ {
42
+ ...domain,
43
+ ancestors: findAncestorsIn(domains)([domain]),
44
+ descendents: _.sortBy(["name"])(findDescendents([domain])(domains)),
45
+ children,
46
+ level,
47
+ },
48
+ ...reduceTaxonomy(domains)(children, level + 1),
49
+ ];
50
+ }, [])(domainsInLevel);
51
+ return [];
52
+ };
53
+
54
+ const getHierarchyOptions = (nodes) => {
55
+ const roots = _.flow(
56
+ _.filter(hasNoParentIn(nodes)),
57
+ _.sortBy(accentInsensitivePathOrder("name"))
58
+ )(nodes);
59
+ return reduceTaxonomy(nodes)(roots, 0);
60
+ };
61
+
62
+ const getKeyAndParents = (hierarchyOptions) => (key) => {
63
+ const parentKeys = _.flow(
64
+ _.find({ key }),
65
+ _.getOr([], "ancestors"),
66
+ _.flatMap(({ key }) => getKeyAndParents(hierarchyOptions)(key))
67
+ )(hierarchyOptions);
68
+ return [...parentKeys, key];
69
+ };
70
+
71
+ export { getHierarchyOptions, getKeyAndParents };