@truedat/core 7.0.5 → 7.0.7

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 (32) hide show
  1. package/package.json +3 -3
  2. package/src/components/DomainSelector.js +15 -0
  3. package/src/components/DropdownMenuItem.js +12 -3
  4. package/src/components/Hierarchy.js +316 -0
  5. package/src/components/HierarchyNodeFinder.js +65 -0
  6. package/src/components/HierarchySelector.js +18 -2
  7. package/src/components/NodeOpenActions.js +30 -0
  8. package/src/components/ResourceMembers.js +2 -3
  9. package/src/components/TreeSelector.js +102 -52
  10. package/src/components/__tests__/AddResourceMember.spec.js +1 -1
  11. package/src/components/__tests__/DomainSelector.spec.js +18 -1
  12. package/src/components/__tests__/DropdownMenuItem.spec.js +6 -2
  13. package/src/components/__tests__/FilterMultilevelDropdown.spec.js +12 -4
  14. package/src/components/__tests__/Hierarchy.spec.js +42 -0
  15. package/src/components/__tests__/HierarchyFilterDropdown.spec.js +9 -3
  16. package/src/components/__tests__/HierarchyNodeFinder.spec.js +203 -0
  17. package/src/components/__tests__/ResourceMembers.spec.js +1 -2
  18. package/src/components/__tests__/TreeSelector.spec.js +25 -7
  19. package/src/components/__tests__/__snapshots__/DomainSelector.spec.js.snap +86 -5
  20. package/src/components/__tests__/__snapshots__/DropdownMenuItem.spec.js.snap +1 -1
  21. package/src/components/__tests__/__snapshots__/FilterMultilevelDropdown.spec.js.snap +2 -2
  22. package/src/components/__tests__/__snapshots__/Hierarchy.spec.js.snap +189 -0
  23. package/src/components/__tests__/__snapshots__/HierarchyFilterDropdown.spec.js.snap +1 -1
  24. package/src/components/__tests__/__snapshots__/HierarchyNodeFinder.spec.js.snap +146 -0
  25. package/src/components/__tests__/__snapshots__/HierarchySelector.spec.js.snap +3 -3
  26. package/src/components/__tests__/__snapshots__/TreeSelector.spec.js.snap +46 -3
  27. package/src/components/index.js +2 -0
  28. package/src/messages/en.js +3 -0
  29. package/src/messages/es.js +3 -1
  30. package/src/routes.js +0 -2
  31. package/src/services/__tests__/tree.spec.js +14 -0
  32. package/src/services/tree.js +16 -0
@@ -12,11 +12,6 @@ export const match =
12
12
  _.contains(query)(lowerDeburr(name));
13
13
  export const matchAny = recursiveMatch(match);
14
14
 
15
- const maybeMapKeys = _.map((option) => ({
16
- ...option,
17
- id: _.has("key")(option) ? option.key : option.id,
18
- }));
19
-
20
15
  export const SelectedLabels = ({
21
16
  disabled,
22
17
  onClick,
@@ -64,50 +59,83 @@ const labelValues = _.cond([
64
59
  ]);
65
60
 
66
61
  export const TreeSelector = ({
62
+ allNodesOpen,
67
63
  check = false,
64
+ className = "",
68
65
  disabled = false,
69
66
  error,
70
67
  label,
71
68
  labels = false,
72
69
  multiple = false,
73
70
  name,
71
+ notDropdown,
74
72
  onBlur,
75
73
  onChange,
76
74
  options,
77
75
  minDepth = 0,
76
+ ascendants,
78
77
  placeholder,
79
78
  required = false,
80
- value: initialValue,
79
+ value,
81
80
  }) => {
82
- const [value, setValue] = useState(
83
- _.defaultTo(multiple ? [] : null)(initialValue)
84
- );
85
81
  const [query, setQuery] = useState();
86
- const [open, setOpen] = useState([]);
82
+ const [open, setOpen] = useState(null);
87
83
  const [displayed, setDisplayed] = useState([]);
84
+ const [currentAllNodesOpen, setCurrentAllNodesOpen] = useState(false);
88
85
 
89
- useEffect(() => {
90
- if (initialValue !== value)
91
- setValue(_.defaultTo(multiple ? [] : null)(initialValue));
92
- }, [initialValue]);
86
+ const selected = (optionId) =>
87
+ multiple ? _.contains(optionId)(value) : value === optionId;
93
88
 
94
89
  useEffect(() => {
90
+ if (!_.isEqual(currentAllNodesOpen, allNodesOpen)) {
91
+ if (allNodesOpen) {
92
+ const allNodeIds = _.map("id", options);
93
+ setOpen(allNodeIds);
94
+ setDisplayed(allNodeIds);
95
+ setCurrentAllNodesOpen(allNodesOpen);
96
+ } else {
97
+ setOpen([]);
98
+ setDisplayed([]);
99
+ setCurrentAllNodesOpen(allNodesOpen);
100
+ }
101
+ return;
102
+ }
103
+
95
104
  const ids = childIds(open)(options);
96
- setDisplayed((displayed) => _.union(displayed)(ids));
97
- }, [options, open, value]);
105
+ const missingAscendants = _.difference(ascendants, open);
106
+ setDisplayed(_.union(displayed)(ids));
107
+
108
+ if (missingAscendants.length > 0) {
109
+ setOpen(_.union(open, ascendants));
110
+ missingAscendants.forEach((ancestorId) => handleOpen(ancestorId));
111
+ }
112
+ if (notDropdown && value && open && !open.find((v) => v === value)) {
113
+ handleOpen(value);
114
+ }
115
+ }, [options, open, ascendants, allNodesOpen]);
116
+
117
+ useEffect(() => {
118
+ if (notDropdown && value && open && !open.find((v) => v === value)) {
119
+ handleOpen(value);
120
+ }
121
+ }, [value]);
98
122
 
99
123
  const handleOpen = (id) => {
100
- const option = _.find({ id })(maybeMapKeys(options));
101
- const isOpen = _.contains(id)(open);
102
- const childIds = _.map("id")(option.children);
103
- const descendentIds = descendents(option);
104
-
105
- if (isOpen) {
106
- setOpen(_.without([id, ...descendentIds])(open));
107
- setDisplayed(_.without(descendentIds)(displayed));
108
- } else {
109
- setOpen(_.union([id])(open));
110
- setDisplayed(_.union(childIds)(displayed));
124
+ const option = _.find({ id })(options);
125
+ if (option) {
126
+ const isOpen = _.contains(id)(open);
127
+ const childIds = _.map("id")(option.children);
128
+ const descendentIds = descendents(option);
129
+
130
+ if (isOpen) {
131
+ const subTree = [id, ...descendentIds];
132
+ const collapsible = !_.some(selected)(subTree);
133
+ collapsible && setOpen(_.without(subTree)(open));
134
+ collapsible && setDisplayed(_.without(descendentIds)(displayed));
135
+ } else {
136
+ setOpen(_.union([id])(open));
137
+ setDisplayed(_.union(childIds)(displayed));
138
+ }
111
139
  }
112
140
  };
113
141
 
@@ -120,14 +148,16 @@ export const TreeSelector = ({
120
148
  const handleSearch = (e, { value }) => {
121
149
  e.preventDefault();
122
150
  setQuery(lowerDeburr(value));
123
- if (!_.isEmpty(value)) {
151
+ if (_.isEmpty(value)) {
152
+ setOpen([]);
153
+ setDisplayed([]);
154
+ } else {
124
155
  displayAll();
125
156
  }
126
157
  };
127
158
 
128
159
  const handleClick = (e, id) => {
129
160
  const nextValue = multiple ? toggle(id)(value) : value === id ? null : id;
130
- setValue(nextValue);
131
161
  onChange && onChange(e, { value: nextValue });
132
162
  };
133
163
 
@@ -141,7 +171,7 @@ export const TreeSelector = ({
141
171
  <SelectedLabels
142
172
  disabled={disabled}
143
173
  placeholder={placeholder}
144
- options={maybeMapKeys(options)}
174
+ options={options}
145
175
  onClick={handleClick}
146
176
  value={labelValues(value)}
147
177
  />
@@ -154,7 +184,6 @@ export const TreeSelector = ({
154
184
  const items = _.flow(
155
185
  filterSearch,
156
186
  filterDisplayed,
157
- maybeMapKeys,
158
187
  _.map((option) => (
159
188
  <DropdownMenuItem
160
189
  key={option?.id}
@@ -165,32 +194,21 @@ export const TreeSelector = ({
165
194
  canOpen={!_.isEmpty(option.children)}
166
195
  level={option.level}
167
196
  disabled={!isEnabled(option, minDepth)}
168
- selected={multiple ? _.contains(option.id)(value) : value === option.id}
197
+ selected={selected(option.id)}
198
+ className={ascendants?.includes(String(option?.id)) ? "ascendant" : ""}
169
199
  {...option}
170
200
  />
171
201
  ))
172
202
  )(options);
173
203
 
174
- return (
175
- <Form.Dropdown
176
- className="fix-dropdown-selector"
177
- error={error}
178
- floating
179
- label={label}
180
- multiple={multiple}
181
- name={name}
182
- onBlur={onBlur}
183
- required={required}
184
- trigger={trigger}
185
- upward={false}
186
- value={value}
187
- disabled={disabled}
188
- >
189
- <Dropdown.Menu>
204
+ const renderTreeSelector = () => {
205
+ const content = (
206
+ <Dropdown.Menu className={notDropdown ? className : ""}>
190
207
  <Input
208
+ fluid
191
209
  icon="search"
192
210
  iconPosition="left"
193
- className="search"
211
+ className={`search ${notDropdown ? "notDropdownInput" : ""}`}
194
212
  onKeyDown={(e) => {
195
213
  if (e.key === " ") {
196
214
  e.stopPropagation();
@@ -202,20 +220,52 @@ export const TreeSelector = ({
202
220
  e.stopPropagation();
203
221
  }}
204
222
  />
205
- <Dropdown.Menu scrolling>{items}</Dropdown.Menu>
223
+ <Dropdown.Menu
224
+ scrolling
225
+ className={notDropdown ? "notDropdownSelector" : ""}
226
+ >
227
+ {items}
228
+ </Dropdown.Menu>
206
229
  </Dropdown.Menu>
207
- </Form.Dropdown>
208
- );
230
+ );
231
+
232
+ return !notDropdown ? (
233
+ <Form.Dropdown
234
+ className={`fix-dropdown-selector ${!notDropdown ? className : ""}`}
235
+ error={error}
236
+ floating
237
+ label={label}
238
+ multiple={multiple}
239
+ name={name}
240
+ onBlur={onBlur}
241
+ required={required}
242
+ trigger={trigger}
243
+ upward={false}
244
+ value={value}
245
+ disabled={disabled}
246
+ >
247
+ {content}
248
+ </Form.Dropdown>
249
+ ) : (
250
+ <>{content}</>
251
+ );
252
+ };
253
+
254
+ return renderTreeSelector();
209
255
  };
210
256
 
211
257
  TreeSelector.propTypes = {
258
+ allNodeIds: PropTypes.bool,
259
+ ascendants: PropTypes.array,
212
260
  check: PropTypes.bool,
261
+ className: PropTypes.string,
213
262
  disabled: PropTypes.bool,
214
263
  error: PropTypes.bool,
215
264
  label: PropTypes.string,
216
265
  labels: PropTypes.bool,
217
266
  multiple: PropTypes.bool,
218
267
  name: PropTypes.string,
268
+ notDropdown: PropTypes.bool,
219
269
  onBlur: PropTypes.func,
220
270
  onChange: PropTypes.func,
221
271
  options: PropTypes.array,
@@ -4,7 +4,7 @@ import { AddResourceMember } from "../AddResourceMember";
4
4
 
5
5
  const props = {
6
6
  type: "domain",
7
- id: 1,
7
+ id: "1",
8
8
  onSuccess: jest.fn(),
9
9
  };
10
10
 
@@ -12,7 +12,24 @@ const renderOpts = { mocks: [domainsMock(variables)] };
12
12
 
13
13
  describe("<DomainSelector />", () => {
14
14
  it("matches latest snapshot", async () => {
15
- const props = { action, onChange: jest.fn() };
15
+ const props = { action, onChange: jest.fn(), className: "test className" };
16
+ const { container, queryByText } = render(
17
+ <DomainSelector {...props} />,
18
+ renderOpts
19
+ );
20
+ await waitFor(() => {
21
+ expect(queryByText(/fooDomain/)).toBeInTheDocument();
22
+ });
23
+ expect(container).toMatchSnapshot();
24
+ });
25
+
26
+ it("matches latest snapshot with notDropdow parameter", async () => {
27
+ const props = {
28
+ action,
29
+ onChange: jest.fn(),
30
+ className: "test className",
31
+ notDropdown: true,
32
+ };
16
33
  const { container, queryByText } = render(
17
34
  <DomainSelector {...props} />,
18
35
  renderOpts
@@ -33,14 +33,18 @@ describe("<DropdownMenuItem />", () => {
33
33
  messages: {},
34
34
  });
35
35
 
36
- userEvent.click(container.querySelector('[class="plus icon"]'));
36
+ userEvent.click(
37
+ container.querySelector('[class="chevron circle down icon"]')
38
+ );
37
39
  await waitFor(() => {
38
40
  expect(onOpen).toHaveBeenCalledWith(id);
39
41
  });
40
42
  rerender(<DropdownMenuItem {...{ ...props, open: true }} />);
41
43
 
42
44
  await waitFor(() => {
43
- expect(container.querySelector('[class="minus icon"]')).toBeTruthy();
45
+ expect(
46
+ container.querySelector('[class="chevron circle right icon"]')
47
+ ).toBeTruthy();
44
48
  });
45
49
  });
46
50
 
@@ -66,15 +66,21 @@ describe("<FilterMultilevelDropdown />", () => {
66
66
 
67
67
  userEvent.click(getByRole("listbox"));
68
68
 
69
- userEvent.click(container.querySelector('[class="plus icon"]'));
69
+ userEvent.click(
70
+ container.querySelector('[class="chevron circle down icon"]')
71
+ );
70
72
  await waitFor(() => {
71
73
  expect(getByRole("option", { name: /Domain 2/i })).toBeTruthy();
72
74
  });
73
- userEvent.click(container.querySelector('[class="minus icon"]'));
75
+ userEvent.click(
76
+ container.querySelector('[class="chevron circle right icon"]')
77
+ );
74
78
  await waitFor(() => {
75
79
  expect(queryByText(/Domain 2/)).toBeFalsy();
76
80
  });
77
- userEvent.click(container.querySelector('[class="plus icon"]'));
81
+ userEvent.click(
82
+ container.querySelector('[class="chevron circle down icon"]')
83
+ );
78
84
  // Select value
79
85
  userEvent.click(getByRole("option", { name: /Domain 2/i }));
80
86
  await waitFor(() => {
@@ -120,7 +126,9 @@ describe("<FilterMultilevelDropdown />", () => {
120
126
  expect(getByRole("option", { name: /Domain 3/i })).toBeTruthy();
121
127
  });
122
128
 
123
- userEvent.click(container.querySelector('[class="plus icon"]'));
129
+ userEvent.click(
130
+ container.querySelector('[class="chevron circle down icon"]')
131
+ );
124
132
 
125
133
  await waitFor(() => {
126
134
  expect(getByRole("option", { name: /Domain 1/i })).toBeTruthy();
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import Hierarchy from "../Hierarchy";
4
+ import en from "../../../../df/src/messages/en";
5
+
6
+ const hierarchy = {
7
+ id: 1,
8
+ name: "Baggins",
9
+ description: "bar",
10
+ nodes: [
11
+ {
12
+ id: 11,
13
+ name: "Fosco",
14
+ parentId: null,
15
+ },
16
+ {
17
+ id: 12,
18
+ name: "Fosco children",
19
+ parentId: 11,
20
+ },
21
+ ],
22
+ };
23
+ const props = { hierarchy: hierarchy, isEditionMode: false };
24
+
25
+ const renderOpts = {
26
+ messages: { en: en },
27
+ };
28
+
29
+ describe("<Hierarchy />", () => {
30
+ it("matches the last snapshot with edition mode false", () => {
31
+ const { container } = render(<Hierarchy {...props} />, renderOpts);
32
+ expect(container).toMatchSnapshot();
33
+ });
34
+
35
+ it("matches snapshot with edition mode", () => {
36
+ const { container } = render(
37
+ <Hierarchy {...{ ...props, isEditionMode: true }} />,
38
+ renderOpts
39
+ );
40
+ expect(container).toMatchSnapshot();
41
+ });
42
+ });
@@ -98,15 +98,21 @@ describe("<HierarchyFilterDropdown />", () => {
98
98
 
99
99
  userEvent.click(getByRole("listbox"));
100
100
 
101
- userEvent.click(container.querySelector('[class="plus icon"]'));
101
+ userEvent.click(
102
+ container.querySelector('[class="chevron circle down icon"]')
103
+ );
102
104
  await waitFor(() => {
103
105
  expect(getByRole("option", { name: /bar/i })).toBeTruthy();
104
106
  });
105
- userEvent.click(container.querySelector('[class="minus icon"]'));
107
+ userEvent.click(
108
+ container.querySelector('[class="chevron circle right icon"]')
109
+ );
106
110
  await waitFor(() => {
107
111
  expect(queryByText(/bar/)).toBeFalsy();
108
112
  });
109
- userEvent.click(container.querySelector('[class="plus icon"]'));
113
+ userEvent.click(
114
+ container.querySelector('[class="chevron circle down icon"]')
115
+ );
110
116
  // Select value
111
117
  userEvent.click(getByRole("option", { name: /bar/i }));
112
118
  await waitFor(() => {
@@ -0,0 +1,203 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { render } from "@truedat/test/render";
4
+ import HierarchyNodeFinder from "../HierarchyNodeFinder";
5
+
6
+ jest.mock("@apollo/client", () => ({
7
+ ...jest.requireActual("@apollo/client"),
8
+ useQuery: jest.fn(),
9
+ }));
10
+
11
+ const nodes = [
12
+ {
13
+ description: "No tiene padre",
14
+ domain_group: null,
15
+ external_id: "1",
16
+ id: 1,
17
+ name: "element_1",
18
+ parent_id: null,
19
+ parents: null,
20
+ type: null,
21
+ },
22
+ {
23
+ description: "No tiene padre",
24
+ domain_group: null,
25
+ external_id: "2",
26
+ id: 2,
27
+ name: "element_2",
28
+ parent_id: null,
29
+ parents: null,
30
+ type: null,
31
+ },
32
+ {
33
+ description: "el padre es element_2",
34
+ domain_group: null,
35
+ external_id: "3",
36
+ id: 3,
37
+ name: "element_3",
38
+ parent_id: 2,
39
+ parents: null,
40
+ type: null,
41
+ },
42
+ {
43
+ description: "el padre es element_2",
44
+ domain_group: null,
45
+ external_id: "4",
46
+ id: 4,
47
+ name: "element_4",
48
+ parent_id: 2,
49
+ parents: null,
50
+ type: null,
51
+ },
52
+ {
53
+ description: "No tiene padre",
54
+ domain_group: null,
55
+ external_id: "5",
56
+ id: 5,
57
+ name: "element_5",
58
+ parent_id: null,
59
+ parents: null,
60
+ type: null,
61
+ },
62
+ {
63
+ description: "el padre es element_5",
64
+ domain_group: null,
65
+ external_id: "6",
66
+ id: 6,
67
+ name: "element_6",
68
+ parent_id: 5,
69
+ parents: null,
70
+ type: null,
71
+ },
72
+ {
73
+ description: "el padre es element_6",
74
+ domain_group: null,
75
+ external_id: "7",
76
+ id: 7,
77
+ name: "element_7",
78
+ parent_id: 6,
79
+ parents: null,
80
+ type: null,
81
+ },
82
+ ];
83
+ const idSelectedNode = 7;
84
+
85
+ const props = {
86
+ nodes,
87
+ idSelectedNode,
88
+ };
89
+
90
+ const renderOpts = {
91
+ messages: {
92
+ en: {
93
+ "domains.notExist": "notExistDomains",
94
+ "domain.selector.placeholder": "Select a domain...",
95
+ "actions.open.all": "Open all",
96
+ },
97
+ },
98
+ };
99
+
100
+ describe("HierarchyNodeFinder", () => {
101
+ it("matches the latest snapshot", () => {
102
+ require("@apollo/client").useQuery.mockReturnValue({
103
+ loading: false,
104
+ error: null,
105
+ data: {
106
+ domains: [
107
+ {
108
+ __typename: "Domain",
109
+ id: "1",
110
+ external_id: "element_1",
111
+ name: "element_1",
112
+ parentId: null,
113
+ actions: [],
114
+ },
115
+ {
116
+ __typename: "Domain",
117
+ id: "2",
118
+ name: "element_2",
119
+ external_id: "element_2",
120
+ parentId: null,
121
+ actions: [],
122
+ },
123
+ {
124
+ __typename: "Domain",
125
+ id: "3",
126
+ external_id: "element_3",
127
+ name: "element_3",
128
+ parentId: "2",
129
+ actions: [],
130
+ },
131
+ {
132
+ __typename: "Domain",
133
+ id: "4",
134
+ external_id: "element_4",
135
+ name: "element_4",
136
+ parentId: "2",
137
+ actions: [],
138
+ },
139
+ {
140
+ __typename: "Domain",
141
+ id: "5",
142
+ external_id: "element_5",
143
+ name: "element_5",
144
+ parentId: null,
145
+ actions: [],
146
+ },
147
+ {
148
+ __typename: "Domain",
149
+ id: "6",
150
+ external_id: "element_6",
151
+ name: "element_6",
152
+ parentId: "5",
153
+ actions: [],
154
+ },
155
+ {
156
+ __typename: "Domain",
157
+ id: "7",
158
+ external_id: "element_7",
159
+ name: "element_7",
160
+ parentId: "6",
161
+ actions: [],
162
+ },
163
+ ],
164
+ },
165
+ });
166
+ const { container } = render(
167
+ <HierarchyNodeFinder {...props} />,
168
+ renderOpts
169
+ );
170
+
171
+ expect(container).toMatchSnapshot();
172
+ });
173
+
174
+ it("should return 'no nodes' messages if node list empty", async () => {
175
+ const customProps = { ...props, nodes: [] };
176
+ const { findByText } = render(
177
+ <HierarchyNodeFinder {...customProps} />,
178
+ renderOpts
179
+ );
180
+
181
+ expect(await findByText("notExistDomains")).toBeInTheDocument();
182
+ });
183
+
184
+ it("should return 'no nodes' messages if node list undefined", async () => {
185
+ const customProps = { ...props, nodes: undefined };
186
+ const { findByText } = render(
187
+ <HierarchyNodeFinder {...customProps} />,
188
+ renderOpts
189
+ );
190
+
191
+ expect(await findByText("notExistDomains")).toBeInTheDocument();
192
+ });
193
+
194
+ it("should return 'no nodes' messages if node list null", async () => {
195
+ const customProps = { ...props, nodes: null };
196
+ const { findByText } = render(
197
+ <HierarchyNodeFinder {...customProps} />,
198
+ renderOpts
199
+ );
200
+
201
+ expect(await findByText("notExistDomains")).toBeInTheDocument();
202
+ });
203
+ });
@@ -11,7 +11,6 @@ jest.spyOn(React, "useContext").mockImplementation(() => intl);
11
11
  jest.mock("@truedat/core/hooks/useAclEntries");
12
12
  jest.mock("react-router-dom", () => ({
13
13
  ...jest.requireActual("react-router-dom"),
14
- useParams: () => ({ id: 1 }),
15
14
  }));
16
15
 
17
16
  jest.mock("@truedat/core/hooks/useAclEntries", () => ({
@@ -74,7 +73,7 @@ describe("<ResourceMembers />", () => {
74
73
  ],
75
74
  };
76
75
 
77
- const props = { type: "domain" };
76
+ const props = { type: "domain", id: 1 };
78
77
 
79
78
  useAclEntries.mockReturnValue({
80
79
  data: { ...aclEntries, actions: { canCreate: true } },