@truedat/bg 8.11.2 → 8.11.4

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/bg",
3
- "version": "8.11.2",
3
+ "version": "8.11.4",
4
4
  "description": "Truedat Web Business Glossary",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -57,7 +57,7 @@
57
57
  "@testing-library/jest-dom": "^6.6.3",
58
58
  "@testing-library/react": "^16.3.0",
59
59
  "@testing-library/user-event": "^14.6.1",
60
- "@truedat/test": "8.11.2",
60
+ "@truedat/test": "8.11.4",
61
61
  "identity-obj-proxy": "^3.0.0",
62
62
  "jest": "^29.7.0",
63
63
  "redux-saga-test-plan": "^4.0.6"
@@ -90,5 +90,5 @@
90
90
  "semantic-ui-react": "^3.0.0-beta.2",
91
91
  "swr": "^2.3.3"
92
92
  },
93
- "gitHead": "5c0ce3c555de4c9e89021b4d0ab785ed27a87305"
93
+ "gitHead": "a80d09c5a9a8f88a4f20dce8a1c79a6db0d281c4"
94
94
  }
@@ -15,8 +15,6 @@ import {
15
15
  CONCEPTS_PENDING,
16
16
  CONCEPTS_DEPRECATED,
17
17
  CONCEPTS_SUBSCOPE,
18
- CONCEPTS_SIDEMENU_MANAGEMENT_LINKS,
19
- CONCEPTS_SIDEMENU_MANAGEMENT_UPLOADS,
20
18
  CONCEPTS_SIDEMENU_SUBSCOPE,
21
19
  CONCEPTS_SIDEMENU_SUBSCOPE_PENDING,
22
20
  CONCEPTS_SIDEMENU_SUBSCOPE_DEPRECATED,
@@ -1,7 +1,11 @@
1
- import { render } from "@truedat/test/render";
1
+ import { render, waitForLoad } from "@truedat/test/render";
2
2
  import { ConceptRow } from "../ConceptRow";
3
3
  import { getConceptColumns } from "../../selectors";
4
4
 
5
+ jest.mock("../../../hooks/useDomains", () => ({
6
+ useDomains: () => ({ data: [] }),
7
+ }));
8
+
5
9
  describe("<ConceptRow />", () => {
6
10
  it("matches the latest snapshot", () => {
7
11
  const columns = getConceptColumns({ conceptColumns: null }).concepts;
@@ -44,4 +48,35 @@ describe("<ConceptRow />", () => {
44
48
  const link = wrapper.getByText("foo");
45
49
  expect(link).toHaveAttribute("href", "/concepts/12/versions/42");
46
50
  });
51
+
52
+ it("renders the full domain path in the domain title", async () => {
53
+ const columns = getConceptColumns({ conceptColumns: null }).concepts;
54
+ const concept = {
55
+ business_concept_id: "12",
56
+ id: "42",
57
+ name: "foo",
58
+ status: "draft",
59
+ domain: { id: 122, name: "Lollipop City" },
60
+ domain_parents: [
61
+ { id: 122, name: "Lollipop City" },
62
+ { id: 120, name: "Happyland" },
63
+ ],
64
+ };
65
+ const rendered = render(
66
+ <table>
67
+ <tbody>
68
+ <ConceptRow concept={concept} columns={columns} />
69
+ </tbody>
70
+ </table>,
71
+ );
72
+ await waitForLoad(rendered);
73
+ const domain = rendered.container.querySelector(
74
+ 'span[data-domain-path="Happyland › Lollipop City"]',
75
+ );
76
+ expect(domain).toHaveTextContent("Lollipop City");
77
+ expect(
78
+ domain.querySelector("i.domain-path-info-icon.info.circle.icon"),
79
+ ).not.toBeNull();
80
+ expect(rendered.container.querySelector("td[title]")).toBeNull();
81
+ });
47
82
  });
@@ -283,7 +283,9 @@ exports[`<ConceptsLinksManagement /> matches the latest snapshot 1`] = `
283
283
  <td
284
284
  class=""
285
285
  >
286
- bar
286
+ <span>
287
+ bar
288
+ </span>
287
289
  </td>
288
290
  <td
289
291
  class=""
@@ -114,7 +114,9 @@ exports[`<ConceptsPanel /> matches latest snapshot 1`] = `
114
114
  <td
115
115
  class=""
116
116
  >
117
- domain
117
+ <span>
118
+ domain
119
+ </span>
118
120
  </td>
119
121
  <td
120
122
  class="center aligned"
@@ -15,6 +15,7 @@ import {
15
15
  SearchContextProvider,
16
16
  useSearchContext,
17
17
  } from "@truedat/core/search/SearchContext";
18
+ import { DomainTooltip } from "../../../taxonomy/components/DomainPath";
18
19
  import ConceptsPagination from "../../components/ConceptsPagination";
19
20
  import { mapStatusColor } from "../../constants/mappings";
20
21
  import { translations } from "../../utils/filterOptions";
@@ -46,7 +47,10 @@ const ConceptSelectorRow = ({
46
47
  positive,
47
48
  extraColumns = [],
48
49
  }) => {
49
- const { name, status, domain } = concept;
50
+ const { name, status, domain, domain_parents } = concept;
51
+ const domainWithParents = domain
52
+ ? { ...domain, parents: [...(domain_parents || [])].reverse() }
53
+ : domain;
50
54
  return (
51
55
  <Table.Row
52
56
  disabled={disabled}
@@ -55,7 +59,7 @@ const ConceptSelectorRow = ({
55
59
  positive={positive}
56
60
  >
57
61
  <Table.Cell content={name} />
58
- <Table.Cell content={domain?.name} />
62
+ <Table.Cell content={<DomainTooltip domain={domainWithParents} />} />
59
63
  <Table.Cell
60
64
  content={
61
65
  <Label color={mapStatusColor[status]}>
@@ -8,6 +8,10 @@ const concept = {
8
8
  id: 1,
9
9
  name: "foo",
10
10
  domain: { id: 1, name: "bar" },
11
+ domain_parents: [
12
+ { id: 1, name: "bar" },
13
+ { id: 0, name: "root" },
14
+ ],
11
15
  status: "draft",
12
16
  business_concept_id: 1,
13
17
  last_change_at: "2020-01-01T00:00:00.000Z",
@@ -19,6 +23,10 @@ const data = {
19
23
  id: 1,
20
24
  name: "foo",
21
25
  domain: { id: 1, name: "bar" },
26
+ domain_parents: [
27
+ { id: 1, name: "bar" },
28
+ { id: 0, name: "root" },
29
+ ],
22
30
  status: "draft",
23
31
  business_concept_id: 1,
24
32
  last_change_at: "2020-01-01T00:00:00.000Z",
@@ -65,6 +73,10 @@ jest.mock("@truedat/bg/concepts/hooks/useConcepts", () => {
65
73
 
66
74
  jest.mock("@truedat/core/hooks/useUserFilters");
67
75
 
76
+ jest.mock("@truedat/bg/hooks/useDomains", () => ({
77
+ useDomains: () => ({ data: [] }),
78
+ }));
79
+
68
80
  describe("<ConceptSelector />", () => {
69
81
  const handleConceptSelected = jest.fn();
70
82
 
@@ -127,6 +139,10 @@ describe("<ConceptSelectorTable />", () => {
127
139
  id: 1,
128
140
  name: "foo",
129
141
  domain: { id: 1, name: "bar" },
142
+ domain_parents: [
143
+ { id: 1, name: "bar" },
144
+ { id: 0, name: "root" },
145
+ ],
130
146
  status: "draft",
131
147
  business_concept_id: 1,
132
148
  last_change_at: "2020-01-01T00:00:00.000Z",
@@ -171,6 +187,14 @@ describe("<ConceptSelectorTable />", () => {
171
187
  expect(rendered.container).toMatchSnapshot();
172
188
  });
173
189
 
190
+ it("renders the domain path tooltip", async () => {
191
+ const rendered = render(<ConceptSelectorTable {...props} />);
192
+ await waitForLoad(rendered);
193
+ expect(
194
+ rendered.container.querySelector('span[data-domain-path="root › bar"]'),
195
+ ).toHaveTextContent("bar");
196
+ });
197
+
174
198
  test("renders Message when there are no concepts and no selectedConcept", () => {
175
199
  const rendered = render(
176
200
  <ConceptSelectorTable
@@ -222,7 +222,9 @@ exports[`<ConceptRelationForm /> matches the latest snapshot 1`] = `
222
222
  <td
223
223
  class=""
224
224
  >
225
- foo domain
225
+ <span>
226
+ foo domain
227
+ </span>
226
228
  </td>
227
229
  <td
228
230
  class=""
@@ -245,7 +247,9 @@ exports[`<ConceptRelationForm /> matches the latest snapshot 1`] = `
245
247
  <td
246
248
  class=""
247
249
  >
248
- foo domain
250
+ <span>
251
+ foo domain
252
+ </span>
249
253
  </td>
250
254
  <td
251
255
  class=""
@@ -184,7 +184,16 @@ exports[`<ConceptSelector /> matches the latest snapshot 1`] = `
184
184
  <td
185
185
  class=""
186
186
  >
187
- bar
187
+ <span
188
+ class="domain-path-trigger"
189
+ data-domain-path="root › bar"
190
+ >
191
+ bar
192
+ <i
193
+ aria-label="Subdomain"
194
+ class="blue info circle icon domain-path-info-icon"
195
+ />
196
+ </span>
188
197
  </td>
189
198
  <td
190
199
  class=""
@@ -249,7 +258,16 @@ exports[`<ConceptSelectorTable /> matches the latest snapshot 1`] = `
249
258
  <td
250
259
  class=""
251
260
  >
252
- bar
261
+ <span
262
+ class="domain-path-trigger"
263
+ data-domain-path="root › bar"
264
+ >
265
+ bar
266
+ <i
267
+ aria-label="Subdomain"
268
+ class="blue info circle icon domain-path-info-icon"
269
+ />
270
+ </span>
253
271
  </td>
254
272
  <td
255
273
  class=""
@@ -5,6 +5,7 @@ import { createSelector } from "reselect";
5
5
  import Moment from "react-moment";
6
6
  import { Link } from "react-router";
7
7
  import { linkTo } from "@truedat/core/routes";
8
+ import { DomainTooltip } from "../../taxonomy/components/DomainPath";
8
9
 
9
10
  const ConceptLink = (concept) =>
10
11
  concept?.id ? (
@@ -20,6 +21,11 @@ const dateDecorator = ({ date }) => (
20
21
  <Moment locale="es" date={date} format="YYYY-MM-DD HH:mm" />
21
22
  );
22
23
 
24
+ const domainSelector = ({ domain, domain_parents }) =>
25
+ domain
26
+ ? { ...domain, parents: [...(domain_parents || [])].reverse() }
27
+ : domain;
28
+
23
29
  export const defaultConceptColumns = [
24
30
  {
25
31
  name: "name",
@@ -31,7 +37,8 @@ export const defaultConceptColumns = [
31
37
  {
32
38
  name: "domain",
33
39
  sort: { name: "domain.name.sort" },
34
- fieldSelector: _.path("domain.name"),
40
+ fieldSelector: domainSelector,
41
+ fieldDecorator: (domain) => <DomainTooltip domain={domain} />,
35
42
  width: 3,
36
43
  },
37
44
  {
@@ -0,0 +1,86 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import { Icon, Popup } from "semantic-ui-react";
4
+ import { ascendants } from "@truedat/core/services/tree";
5
+ import { useDomains } from "../../hooks/useDomains";
6
+
7
+ export { useDomains };
8
+
9
+ const domainKey = ({ id, name } = {}) =>
10
+ _.isNil(id) ? `name:${name}` : `id:${id}`;
11
+
12
+ const uniqueDomains = (domains) => {
13
+ const keys = new Set();
14
+ return domains.filter((domain) => {
15
+ if (_.isEmpty(domain)) return false;
16
+ const key = domainKey(domain);
17
+ if (keys.has(key)) return false;
18
+ keys.add(key);
19
+ return true;
20
+ });
21
+ };
22
+
23
+ const sameDomain = (left, right) => {
24
+ if (!_.isNil(left?.id) && !_.isNil(right?.id)) {
25
+ return `${left.id}` === `${right.id}`;
26
+ }
27
+ return left?.name === right?.name;
28
+ };
29
+
30
+ const pathFromParents = (domain) => {
31
+ const parents = domain?.parents;
32
+ return _.isEmpty(parents)
33
+ ? []
34
+ : uniqueDomains([
35
+ ...parents.filter((parent) => !sameDomain(parent, domain)),
36
+ domain,
37
+ ]);
38
+ };
39
+
40
+ export const useDomainPath = (domain) => {
41
+ const { data: domains } = useDomains();
42
+ if (_.isEmpty(domain)) return [];
43
+
44
+ if (!_.isNil(domain.id) && !_.isEmpty(domains)) {
45
+ const self = domains.find(({ id }) => `${id}` === `${domain.id}`);
46
+ if (self) {
47
+ return ascendants(self.id, domains)
48
+ .map((id) => domains.find((item) => `${item.id}` === `${id}`))
49
+ .filter(Boolean);
50
+ }
51
+ }
52
+
53
+ return pathFromParents(domain);
54
+ };
55
+
56
+ export const DomainTooltip = ({ domain }) => {
57
+ const path = useDomainPath(domain);
58
+ if (_.isEmpty(domain)) return null;
59
+ if (path.length <= 1) return <span>{domain.name}</span>;
60
+
61
+ const content = _.flow(_.map("name"), _.join(" › "))(path);
62
+ return (
63
+ <Popup
64
+ className="domain-path-popup"
65
+ content={content}
66
+ trigger={
67
+ <span className="domain-path-trigger" data-domain-path={content}>
68
+ {domain.name}
69
+ <Icon
70
+ aria-label="Subdomain"
71
+ className="domain-path-info-icon"
72
+ color="blue"
73
+ name="info circle"
74
+ />
75
+ </span>
76
+ }
77
+ on="hover"
78
+ position="top center"
79
+ basic
80
+ />
81
+ );
82
+ };
83
+
84
+ DomainTooltip.propTypes = {
85
+ domain: PropTypes.object,
86
+ };
@@ -0,0 +1,179 @@
1
+ import { render, waitForLoad } from "@truedat/test/render";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useDomainPath, DomainTooltip } from "../DomainPath";
4
+
5
+ const mockDomains = [
6
+ { id: 1, name: "Root" },
7
+ { id: 2, name: "Level1", parent_id: 1 },
8
+ { id: 3, name: "Level2", parent_id: 2 },
9
+ ];
10
+
11
+ jest.mock("../../../hooks/useDomains", () => ({
12
+ useDomains: () => ({ data: mockDomains }),
13
+ }));
14
+
15
+ describe("useDomainPath", () => {
16
+ it("returns empty array for null domain", () => {
17
+ const { result } = renderHook(() => useDomainPath(null));
18
+ expect(result.current).toEqual([]);
19
+ });
20
+
21
+ it("returns empty array for undefined domain", () => {
22
+ const { result } = renderHook(() => useDomainPath(undefined));
23
+ expect(result.current).toEqual([]);
24
+ });
25
+
26
+ it("returns empty array for empty object", () => {
27
+ const { result } = renderHook(() => useDomainPath({}));
28
+ expect(result.current).toEqual([]);
29
+ });
30
+
31
+ it("returns path from canonical tree when domain id exists", () => {
32
+ const domain = { id: 3, name: "Level2" };
33
+ const { result } = renderHook(() => useDomainPath(domain));
34
+ expect(result.current).toHaveLength(3);
35
+ expect(result.current.map((d) => d.name)).toEqual([
36
+ "Root",
37
+ "Level1",
38
+ "Level2",
39
+ ]);
40
+ });
41
+
42
+ it("returns path from parents when domain id not in tree", () => {
43
+ const domain = {
44
+ id: 999,
45
+ name: "Unknown",
46
+ parents: [
47
+ { id: 1, name: "Root" },
48
+ { id: 2, name: "Level1" },
49
+ ],
50
+ };
51
+ const { result } = renderHook(() => useDomainPath(domain));
52
+ expect(result.current).toHaveLength(3);
53
+ expect(result.current.map((d) => d.name)).toEqual([
54
+ "Root",
55
+ "Level1",
56
+ "Unknown",
57
+ ]);
58
+ });
59
+
60
+ it("returns path from parents when domain has no id", () => {
61
+ const domain = {
62
+ name: "NoId",
63
+ parents: [
64
+ { id: 1, name: "Root" },
65
+ { id: 2, name: "Level1" },
66
+ ],
67
+ };
68
+ const { result } = renderHook(() => useDomainPath(domain));
69
+ expect(result.current).toHaveLength(3);
70
+ expect(result.current.map((d) => d.name)).toEqual([
71
+ "Root",
72
+ "Level1",
73
+ "NoId",
74
+ ]);
75
+ });
76
+
77
+ it("returns empty array when parents are empty", () => {
78
+ const domain = { id: 999, name: "Orphan", parents: [] };
79
+ const { result } = renderHook(() => useDomainPath(domain));
80
+ expect(result.current).toEqual([]);
81
+ });
82
+
83
+ it("deduplicates domains in path", () => {
84
+ const domain = {
85
+ id: 2,
86
+ name: "Level1",
87
+ parents: [
88
+ { id: 1, name: "Root" },
89
+ { id: 2, name: "Level1" },
90
+ ],
91
+ };
92
+ const { result } = renderHook(() => useDomainPath(domain));
93
+ expect(result.current).toHaveLength(2);
94
+ expect(result.current.map((d) => d.name)).toEqual(["Root", "Level1"]);
95
+ });
96
+ });
97
+
98
+ describe("DomainTooltip", () => {
99
+ it("returns null for null domain", async () => {
100
+ const rendered = render(<DomainTooltip domain={null} />);
101
+ await waitForLoad(rendered);
102
+ expect(rendered.container.firstChild).toBeNull();
103
+ });
104
+
105
+ it("returns null for undefined domain", async () => {
106
+ const rendered = render(<DomainTooltip domain={undefined} />);
107
+ await waitForLoad(rendered);
108
+ expect(rendered.container.firstChild).toBeNull();
109
+ });
110
+
111
+ it("returns null for empty object", async () => {
112
+ const rendered = render(<DomainTooltip domain={{}} />);
113
+ await waitForLoad(rendered);
114
+ expect(rendered.container.firstChild).toBeNull();
115
+ });
116
+
117
+ it("renders simple span when path has only one element", async () => {
118
+ const domain = { id: 1, name: "Root" };
119
+ const rendered = render(<DomainTooltip domain={domain} />);
120
+ await waitForLoad(rendered);
121
+ const span = rendered.container.querySelector("span");
122
+ expect(span).toHaveTextContent("Root");
123
+ expect(span.getAttribute("data-domain-path")).toBeNull();
124
+ });
125
+
126
+ it("renders Popup with full path when path has multiple elements", async () => {
127
+ const domain = { id: 3, name: "Level2" };
128
+ const rendered = render(<DomainTooltip domain={domain} />);
129
+ await waitForLoad(rendered);
130
+ const trigger = rendered.container.querySelector(
131
+ 'span[data-domain-path="Root › Level1 › Level2"]',
132
+ );
133
+ expect(trigger).not.toBeNull();
134
+ expect(trigger).toHaveTextContent("Level2");
135
+ expect(
136
+ trigger.querySelector("i.domain-path-info-icon.info.circle.icon"),
137
+ ).not.toBeNull();
138
+ });
139
+
140
+ it("renders Popup with path from parents when domain not in tree", async () => {
141
+ const domain = {
142
+ id: 999,
143
+ name: "Unknown",
144
+ parents: [
145
+ { id: 1, name: "Root" },
146
+ { id: 2, name: "Level1" },
147
+ ],
148
+ };
149
+ const rendered = render(<DomainTooltip domain={domain} />);
150
+ await waitForLoad(rendered);
151
+ const trigger = rendered.container.querySelector(
152
+ 'span[data-domain-path="Root › Level1 › Unknown"]',
153
+ );
154
+ expect(trigger).not.toBeNull();
155
+ expect(trigger).toHaveTextContent("Unknown");
156
+ });
157
+
158
+ describe("snapshots", () => {
159
+ it("matches snapshot for null domain", async () => {
160
+ const rendered = render(<DomainTooltip domain={null} />);
161
+ await waitForLoad(rendered);
162
+ expect(rendered.container).toMatchSnapshot();
163
+ });
164
+
165
+ it("matches snapshot for single element path", async () => {
166
+ const domain = { id: 1, name: "Root" };
167
+ const rendered = render(<DomainTooltip domain={domain} />);
168
+ await waitForLoad(rendered);
169
+ expect(rendered.container).toMatchSnapshot();
170
+ });
171
+
172
+ it("matches snapshot for multi-element path", async () => {
173
+ const domain = { id: 3, name: "Level2" };
174
+ const rendered = render(<DomainTooltip domain={domain} />);
175
+ await waitForLoad(rendered);
176
+ expect(rendered.container).toMatchSnapshot();
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,26 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`DomainTooltip snapshots matches snapshot for multi-element path 1`] = `
4
+ <div>
5
+ <span
6
+ class="domain-path-trigger"
7
+ data-domain-path="Root › Level1 › Level2"
8
+ >
9
+ Level2
10
+ <i
11
+ aria-label="Subdomain"
12
+ class="blue info circle icon domain-path-info-icon"
13
+ />
14
+ </span>
15
+ </div>
16
+ `;
17
+
18
+ exports[`DomainTooltip snapshots matches snapshot for null domain 1`] = `<div />`;
19
+
20
+ exports[`DomainTooltip snapshots matches snapshot for single element path 1`] = `
21
+ <div>
22
+ <span>
23
+ Root
24
+ </span>
25
+ </div>
26
+ `;
@@ -0,0 +1,25 @@
1
+ .ui.popup.domain-path-popup {
2
+ --domain-path-bg: #1f1f1f;
3
+ --domain-path-glow: rgba(255, 255, 255, 0.35);
4
+
5
+ max-width: none !important;
6
+ width: max-content;
7
+ white-space: nowrap !important;
8
+ padding: 0.6em 1em !important;
9
+ background: var(--domain-path-bg) !important;
10
+ color: #fff !important;
11
+ border-radius: 10px !important;
12
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35) !important;
13
+ }
14
+
15
+ .ui.popup.domain-path-popup::before {
16
+ border-color: var(--domain-path-bg) transparent transparent !important;
17
+ }
18
+
19
+ .domain-path-trigger:hover {
20
+ text-shadow: 0 0 8px var(--domain-path-glow);
21
+ }
22
+
23
+ .domain-path-trigger .domain-path-info-icon {
24
+ margin-left: 0.4em;
25
+ }