@truedat/lm 8.7.4 → 8.8.0
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/ConfirmDeleteRelation.js +14 -2
- package/src/components/LinksPane.js +116 -88
- package/src/components/LinksPaneDataCell.js +72 -0
- package/src/components/LinksPaneRow.js +18 -0
- package/src/components/LinksPaneTypeFilter.js +82 -0
- package/src/components/__tests__/ConfirmDeleteRelation.spec.js +9 -0
- package/src/components/__tests__/LinksPane.spec.js +60 -95
- package/src/components/__tests__/LinksPaneDataCell.spec.js +192 -0
- package/src/components/__tests__/LinksPaneRow.spec.js +94 -0
- package/src/components/__tests__/LinksPaneTypeFilter.spec.js +164 -0
- package/src/components/__tests__/__snapshots__/ConfirmDeleteRelation.spec.js.snap +2 -2
- package/src/components/__tests__/__snapshots__/ImplementationLinks.spec.js.snap +4 -4
- package/src/components/__tests__/__snapshots__/LinksPane.spec.js.snap +98 -40
- package/src/hooks/__tests__/useFilteredLinks.spec.js +116 -0
- package/src/hooks/__tests__/useSortedLinks.spec.js +129 -0
- package/src/hooks/__tests__/useStructureNotes.spec.js +138 -0
- package/src/hooks/__tests__/useTruncatedPopup.spec.js +109 -0
- package/src/hooks/useFilteredLinks.js +98 -0
- package/src/hooks/useSortedLinks.js +8 -0
- package/src/hooks/useStructureNotes.js +52 -0
- package/src/hooks/useTruncatedPopup.js +30 -0
- package/src/styles/LinksPane.less +117 -0
- package/src/styles/RelationActions.less +7 -0
- package/src/utils/__tests__/linksPaneHelpers.spec.js +316 -0
- package/src/utils/linksPaneHelpers.js +164 -0
|
@@ -92,11 +92,11 @@ exports[`<ImplementationLinks /> matches the latest snapshot 1`] = `
|
|
|
92
92
|
class=""
|
|
93
93
|
>
|
|
94
94
|
<button
|
|
95
|
-
class="ui red mini basic icon button"
|
|
95
|
+
class="ui red mini basic compact icon button relation-actions__delete-button"
|
|
96
96
|
>
|
|
97
97
|
<i
|
|
98
98
|
aria-hidden="true"
|
|
99
|
-
class="red trash alternate outline icon"
|
|
99
|
+
class="red trash alternate outline small icon relation-actions__delete-icon"
|
|
100
100
|
/>
|
|
101
101
|
</button>
|
|
102
102
|
</td>
|
|
@@ -185,11 +185,11 @@ exports[`<ImplementationLinks /> matches the latest snapshot when implementation
|
|
|
185
185
|
class=""
|
|
186
186
|
>
|
|
187
187
|
<button
|
|
188
|
-
class="ui red mini basic icon button"
|
|
188
|
+
class="ui red mini basic compact icon button relation-actions__delete-button"
|
|
189
189
|
>
|
|
190
190
|
<i
|
|
191
191
|
aria-hidden="true"
|
|
192
|
-
class="red trash alternate outline icon"
|
|
192
|
+
class="red trash alternate outline small icon relation-actions__delete-icon"
|
|
193
193
|
/>
|
|
194
194
|
</button>
|
|
195
195
|
</td>
|
|
@@ -27,24 +27,30 @@ exports[`<LinksPane /> matches the latest snapshot 1`] = `
|
|
|
27
27
|
>
|
|
28
28
|
Master title
|
|
29
29
|
</h3>
|
|
30
|
-
<
|
|
31
|
-
class="
|
|
30
|
+
<div
|
|
31
|
+
class="links-pane__table-scroll"
|
|
32
32
|
>
|
|
33
|
-
<
|
|
34
|
-
class=""
|
|
33
|
+
<table
|
|
34
|
+
class="ui celled sortable unstackable compact table links-pane__table"
|
|
35
|
+
style="--links-pane-table-min-width: 0px;"
|
|
35
36
|
>
|
|
36
|
-
<
|
|
37
|
+
<colgroup />
|
|
38
|
+
<thead
|
|
37
39
|
class=""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
40
|
+
>
|
|
41
|
+
<tr
|
|
42
|
+
class=""
|
|
43
|
+
/>
|
|
44
|
+
</thead>
|
|
45
|
+
<tbody
|
|
44
46
|
class=""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
>
|
|
48
|
+
<tr
|
|
49
|
+
class=""
|
|
50
|
+
/>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
48
54
|
</div>
|
|
49
55
|
</div>
|
|
50
56
|
</div>
|
|
@@ -78,43 +84,95 @@ exports[`<LinksPane /> should display note columns only when note values are pre
|
|
|
78
84
|
>
|
|
79
85
|
Master title
|
|
80
86
|
</h3>
|
|
81
|
-
<
|
|
82
|
-
class="
|
|
87
|
+
<div
|
|
88
|
+
class="links-pane__table-scroll"
|
|
83
89
|
>
|
|
84
|
-
<
|
|
85
|
-
class=""
|
|
90
|
+
<table
|
|
91
|
+
class="ui celled sortable unstackable compact table links-pane__table"
|
|
92
|
+
style="--links-pane-table-min-width: 384px;"
|
|
86
93
|
>
|
|
87
|
-
<
|
|
94
|
+
<colgroup>
|
|
95
|
+
<col
|
|
96
|
+
class="links-pane__column"
|
|
97
|
+
style="--links-pane-column-width: 264px;"
|
|
98
|
+
/>
|
|
99
|
+
<col
|
|
100
|
+
class="links-pane__column"
|
|
101
|
+
style="--links-pane-column-width: 120px;"
|
|
102
|
+
/>
|
|
103
|
+
</colgroup>
|
|
104
|
+
<thead
|
|
88
105
|
class=""
|
|
89
106
|
>
|
|
90
|
-
<
|
|
107
|
+
<tr
|
|
91
108
|
class=""
|
|
92
109
|
>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
<th
|
|
111
|
+
class="links-pane__cell links-pane__cell--header links-pane__cell--wrap"
|
|
112
|
+
style="--links-pane-column-width: 264px;"
|
|
113
|
+
>
|
|
114
|
+
<span
|
|
115
|
+
class="links-pane__filter-label"
|
|
116
|
+
>
|
|
117
|
+
<span
|
|
118
|
+
class="links-pane__filter-label-text"
|
|
119
|
+
>
|
|
120
|
+
Name
|
|
121
|
+
</span>
|
|
122
|
+
</span>
|
|
123
|
+
</th>
|
|
124
|
+
<th
|
|
125
|
+
class="links-pane__cell links-pane__cell--header"
|
|
126
|
+
style="--links-pane-column-width: 120px;"
|
|
127
|
+
>
|
|
128
|
+
<span
|
|
129
|
+
class="links-pane__filter-label"
|
|
130
|
+
>
|
|
131
|
+
<span
|
|
132
|
+
class="links-pane__filter-label-text"
|
|
133
|
+
>
|
|
134
|
+
Foo
|
|
135
|
+
</span>
|
|
136
|
+
</span>
|
|
137
|
+
</th>
|
|
138
|
+
</tr>
|
|
139
|
+
</thead>
|
|
140
|
+
<tbody
|
|
101
141
|
class=""
|
|
102
142
|
>
|
|
103
|
-
<
|
|
143
|
+
<tr
|
|
104
144
|
class=""
|
|
105
145
|
>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
<td
|
|
147
|
+
class="links-pane__cell links-pane__cell--wrap"
|
|
148
|
+
style="--links-pane-column-width: 264px;"
|
|
149
|
+
>
|
|
150
|
+
Structure 1
|
|
151
|
+
</td>
|
|
152
|
+
<td
|
|
153
|
+
class="links-pane__cell"
|
|
154
|
+
style="--links-pane-column-width: 120px;"
|
|
155
|
+
>
|
|
156
|
+
baz
|
|
157
|
+
</td>
|
|
158
|
+
</tr>
|
|
159
|
+
<tr
|
|
113
160
|
class=""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
161
|
+
>
|
|
162
|
+
<td
|
|
163
|
+
class="links-pane__cell links-pane__cell--wrap"
|
|
164
|
+
style="--links-pane-column-width: 264px;"
|
|
165
|
+
>
|
|
166
|
+
Structure 2
|
|
167
|
+
</td>
|
|
168
|
+
<td
|
|
169
|
+
class="links-pane__cell"
|
|
170
|
+
style="--links-pane-column-width: 120px;"
|
|
171
|
+
/>
|
|
172
|
+
</tr>
|
|
173
|
+
</tbody>
|
|
174
|
+
</table>
|
|
175
|
+
</div>
|
|
118
176
|
</div>
|
|
119
177
|
</div>
|
|
120
178
|
</div>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } from "@testing-library/react";
|
|
6
|
+
import { useFilteredLinks } from "../useFilteredLinks";
|
|
7
|
+
|
|
8
|
+
jest.mock(
|
|
9
|
+
"@truedat/core/services",
|
|
10
|
+
() => {
|
|
11
|
+
const lodashFp = require("lodash/fp");
|
|
12
|
+
return {
|
|
13
|
+
columnDecorator: () => () => null,
|
|
14
|
+
columnPredicate: () => () => true,
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
{ virtual: true },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
describe("useFilteredLinks", () => {
|
|
21
|
+
const enrichedLinks = [
|
|
22
|
+
{ id: 1, name: "Alpha", tags: ["main"], type: "column" },
|
|
23
|
+
{ id: 2, name: "Beta", tags: ["secondary"], type: "table" },
|
|
24
|
+
{ id: 3, name: "Gamma", tags: ["main"], type: "field" },
|
|
25
|
+
{ id: 4, name: "Delta", tags: ["secondary"], type: "column" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
it("returns all links when no filters are selected", () => {
|
|
29
|
+
const { result } = renderHook(() =>
|
|
30
|
+
useFilteredLinks({
|
|
31
|
+
enrichedLinks,
|
|
32
|
+
selectedTypes: [],
|
|
33
|
+
selectedRelationTypes: [],
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(result.current.filteredLinks).toHaveLength(4);
|
|
38
|
+
expect(result.current.typeOptions).toEqual(["column", "field", "table"]);
|
|
39
|
+
expect(result.current.relationTypeOptions).toEqual(["main", "secondary"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("filters links by type", () => {
|
|
43
|
+
const { result } = renderHook(() =>
|
|
44
|
+
useFilteredLinks({
|
|
45
|
+
enrichedLinks,
|
|
46
|
+
selectedTypes: ["column"],
|
|
47
|
+
selectedRelationTypes: [],
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(result.current.filteredLinks).toHaveLength(2);
|
|
52
|
+
expect(result.current.filteredLinks.map((l) => l.name)).toEqual([
|
|
53
|
+
"Alpha",
|
|
54
|
+
"Delta",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("filters links by relation type", () => {
|
|
59
|
+
const { result } = renderHook(() =>
|
|
60
|
+
useFilteredLinks({
|
|
61
|
+
enrichedLinks,
|
|
62
|
+
selectedTypes: [],
|
|
63
|
+
selectedRelationTypes: ["main"],
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(result.current.filteredLinks).toHaveLength(2);
|
|
68
|
+
expect(result.current.filteredLinks.map((l) => l.name)).toEqual([
|
|
69
|
+
"Alpha",
|
|
70
|
+
"Gamma",
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("combines type and relation type filters", () => {
|
|
75
|
+
const { result } = renderHook(() =>
|
|
76
|
+
useFilteredLinks({
|
|
77
|
+
enrichedLinks,
|
|
78
|
+
selectedTypes: ["column"],
|
|
79
|
+
selectedRelationTypes: ["main"],
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result.current.filteredLinks).toHaveLength(1);
|
|
84
|
+
expect(result.current.filteredLinks.map((l) => l.name)).toEqual(["Alpha"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("includes empty option for links without tags", () => {
|
|
88
|
+
const linksWithEmpty = [
|
|
89
|
+
{ id: 1, name: "Alpha", tags: ["main"] },
|
|
90
|
+
{ id: 2, name: "Beta", tags: [] },
|
|
91
|
+
{ id: 3, name: "Gamma" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(() =>
|
|
95
|
+
useFilteredLinks({
|
|
96
|
+
enrichedLinks: linksWithEmpty,
|
|
97
|
+
selectedTypes: [],
|
|
98
|
+
selectedRelationTypes: [],
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result.current.relationTypeOptions).toEqual(["", "main"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("updates typeOptions based on relationType filter", () => {
|
|
106
|
+
const { result } = renderHook(() =>
|
|
107
|
+
useFilteredLinks({
|
|
108
|
+
enrichedLinks,
|
|
109
|
+
selectedTypes: [],
|
|
110
|
+
selectedRelationTypes: ["secondary"],
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(result.current.typeOptions).toEqual(["column", "table"]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } from "@testing-library/react";
|
|
6
|
+
import { useSortedLinks } from "../useSortedLinks";
|
|
7
|
+
|
|
8
|
+
jest.mock(
|
|
9
|
+
"@truedat/core/services",
|
|
10
|
+
() => {
|
|
11
|
+
const lodashFp = require("lodash/fp");
|
|
12
|
+
return {
|
|
13
|
+
columnDecorator: (column) => (obj) => {
|
|
14
|
+
const { fieldSelector, fieldDecorator, name } = column || {};
|
|
15
|
+
const field = lodashFp.isFunction(fieldSelector)
|
|
16
|
+
? fieldSelector(obj)
|
|
17
|
+
: lodashFp.path(lodashFp.defaultTo(name)(fieldSelector))(obj);
|
|
18
|
+
return lodashFp.isFunction(fieldDecorator)
|
|
19
|
+
? fieldDecorator(field)
|
|
20
|
+
: field;
|
|
21
|
+
},
|
|
22
|
+
columnPredicate: () => () => true,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
{ virtual: true },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("useSortedLinks", () => {
|
|
29
|
+
const columns = [
|
|
30
|
+
{ name: "name" },
|
|
31
|
+
{ name: "relation.relation_type_name", fieldSelector: (link) => link.tags },
|
|
32
|
+
{ name: "type" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const links = [
|
|
36
|
+
{ id: 1, name: "Zulu", tags: ["secondary"], type: "field" },
|
|
37
|
+
{ id: 2, name: "Alpha", tags: ["main"], type: "table" },
|
|
38
|
+
{ id: 3, name: "Beta", tags: ["main"], type: "column" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
it("sorts by relation type then type when no sort column", () => {
|
|
42
|
+
const sort = { column: null, direction: "ascending" };
|
|
43
|
+
|
|
44
|
+
const { result } = renderHook(() => useSortedLinks(links, columns, sort));
|
|
45
|
+
|
|
46
|
+
expect(result.current.map((l) => l.name)).toEqual([
|
|
47
|
+
"Beta",
|
|
48
|
+
"Alpha",
|
|
49
|
+
"Zulu",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("sorts by type when relation type column is not present", () => {
|
|
54
|
+
const typeOnlyColumns = [{ name: "name" }, { name: "type" }];
|
|
55
|
+
const typeLinks = [
|
|
56
|
+
{ id: 1, name: "Zulu", type: "table" },
|
|
57
|
+
{ id: 2, name: "Alpha", type: "column" },
|
|
58
|
+
];
|
|
59
|
+
const sort = { column: null, direction: "ascending" };
|
|
60
|
+
|
|
61
|
+
const { result } = renderHook(() =>
|
|
62
|
+
useSortedLinks(typeLinks, typeOnlyColumns, sort),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(result.current.map((l) => l.name)).toEqual(["Alpha", "Zulu"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("sorts by default fallback when no sortable columns exist", () => {
|
|
69
|
+
const noSortColumns = [{ name: "_actions" }];
|
|
70
|
+
const fallbackLinks = [
|
|
71
|
+
{ id: 1, business_concept_id: 10, name: "Beta" },
|
|
72
|
+
{ id: 2, business_concept_id: 5, name: "Alpha" },
|
|
73
|
+
];
|
|
74
|
+
const sort = { column: null, direction: "ascending" };
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() =>
|
|
77
|
+
useSortedLinks(fallbackLinks, noSortColumns, sort),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result.current.map((l) => l.name)).toEqual(["Alpha", "Beta"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sorts ascending by a specific column", () => {
|
|
84
|
+
const sort = { column: "name", direction: "ascending" };
|
|
85
|
+
|
|
86
|
+
const { result } = renderHook(() => useSortedLinks(links, columns, sort));
|
|
87
|
+
|
|
88
|
+
expect(result.current.map((l) => l.name)).toEqual([
|
|
89
|
+
"Alpha",
|
|
90
|
+
"Beta",
|
|
91
|
+
"Zulu",
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("sorts descending by a specific column", () => {
|
|
96
|
+
const sort = { column: "name", direction: "descending" };
|
|
97
|
+
|
|
98
|
+
const { result } = renderHook(() => useSortedLinks(links, columns, sort));
|
|
99
|
+
|
|
100
|
+
expect(result.current.map((l) => l.name)).toEqual([
|
|
101
|
+
"Zulu",
|
|
102
|
+
"Beta",
|
|
103
|
+
"Alpha",
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("recomputes when sort changes", () => {
|
|
108
|
+
const { result, rerender } = renderHook(
|
|
109
|
+
({ sort }) => useSortedLinks(links, columns, sort),
|
|
110
|
+
{
|
|
111
|
+
initialProps: { sort: { column: "name", direction: "ascending" } },
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(result.current.map((l) => l.name)).toEqual([
|
|
116
|
+
"Alpha",
|
|
117
|
+
"Beta",
|
|
118
|
+
"Zulu",
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
rerender({ sort: { column: "name", direction: "descending" } });
|
|
122
|
+
|
|
123
|
+
expect(result.current.map((l) => l.name)).toEqual([
|
|
124
|
+
"Zulu",
|
|
125
|
+
"Beta",
|
|
126
|
+
"Alpha",
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
6
|
+
import { useDataStructureSearch } from "@truedat/dd/hooks/useStructures";
|
|
7
|
+
import { useStructureNotes } from "../useStructureNotes";
|
|
8
|
+
|
|
9
|
+
jest.mock(
|
|
10
|
+
"@truedat/dd/hooks/useStructures",
|
|
11
|
+
() => ({
|
|
12
|
+
useDataStructureSearch: jest.fn(),
|
|
13
|
+
}),
|
|
14
|
+
{ virtual: true },
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
describe("useStructureNotes", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
const mockTrigger = jest.fn().mockResolvedValue({
|
|
20
|
+
data: {
|
|
21
|
+
data: [
|
|
22
|
+
{ id: 123, note: { status: "published", content: "Test note" } },
|
|
23
|
+
{ id: 456, note: { status: "draft", content: "Another note" } },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
useDataStructureSearch.mockReturnValue({
|
|
29
|
+
trigger: mockTrigger,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns links unchanged if no data_structure resources", () => {
|
|
34
|
+
const links = [
|
|
35
|
+
{ id: 1, resource_type: "other", resource_id: 123, name: "Other 1" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const { result } = renderHook(() => useStructureNotes(links));
|
|
39
|
+
|
|
40
|
+
expect(result.current).toEqual(links);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("fetches and enriches data_structure links with notes", async () => {
|
|
44
|
+
const links = [
|
|
45
|
+
{
|
|
46
|
+
id: 1,
|
|
47
|
+
resource_type: "data_structure",
|
|
48
|
+
resource_id: 123,
|
|
49
|
+
name: "Structure 1",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 2,
|
|
53
|
+
resource_type: "data_structure",
|
|
54
|
+
resource_id: 456,
|
|
55
|
+
name: "Structure 2",
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const { result } = renderHook(() => useStructureNotes(links));
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(result.current[0].note).toEqual({
|
|
63
|
+
status: "published",
|
|
64
|
+
content: "Test note",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("calls trigger with correct structure IDs", async () => {
|
|
70
|
+
const mockTrigger = jest.fn().mockResolvedValue({
|
|
71
|
+
data: {
|
|
72
|
+
data: [],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
useDataStructureSearch.mockReturnValue({
|
|
77
|
+
trigger: mockTrigger,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const links = [
|
|
81
|
+
{
|
|
82
|
+
id: 1,
|
|
83
|
+
resource_type: "data_structure",
|
|
84
|
+
resource_id: 123,
|
|
85
|
+
name: "Structure 1",
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
renderHook(() => useStructureNotes(links));
|
|
90
|
+
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(mockTrigger).toHaveBeenCalledWith({
|
|
93
|
+
must: { ids: [123] },
|
|
94
|
+
page: 0,
|
|
95
|
+
size: 1,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not call trigger if no data_structure links", () => {
|
|
101
|
+
const mockTrigger = jest.fn();
|
|
102
|
+
|
|
103
|
+
useDataStructureSearch.mockReturnValue({
|
|
104
|
+
trigger: mockTrigger,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const links = [
|
|
108
|
+
{ id: 1, resource_type: "other", resource_id: 123, name: "Other 1" },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
renderHook(() => useStructureNotes(links));
|
|
112
|
+
|
|
113
|
+
expect(mockTrigger).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("handles API errors gracefully", async () => {
|
|
117
|
+
const mockTrigger = jest.fn().mockRejectedValue(new Error("API Error"));
|
|
118
|
+
|
|
119
|
+
useDataStructureSearch.mockReturnValue({
|
|
120
|
+
trigger: mockTrigger,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const links = [
|
|
124
|
+
{
|
|
125
|
+
id: 1,
|
|
126
|
+
resource_type: "data_structure",
|
|
127
|
+
resource_id: 123,
|
|
128
|
+
name: "Structure 1",
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const { result } = renderHook(() => useStructureNotes(links));
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(result.current).toEqual(links);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from "@testing-library/react";
|
|
6
|
+
import { useTruncatedPopup } from "../useTruncatedPopup";
|
|
7
|
+
|
|
8
|
+
describe("useTruncatedPopup", () => {
|
|
9
|
+
it("initializes with closed popup", () => {
|
|
10
|
+
const { result } = renderHook(() => useTruncatedPopup());
|
|
11
|
+
|
|
12
|
+
expect(result.current.popupOpen).toBe(false);
|
|
13
|
+
expect(result.current.popupContent).toBe("");
|
|
14
|
+
expect(result.current.cellRef.current).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("opens popup when content overflows", () => {
|
|
18
|
+
const { result } = renderHook(() => useTruncatedPopup());
|
|
19
|
+
|
|
20
|
+
const mockCell = {
|
|
21
|
+
textContent: "Long text that overflows",
|
|
22
|
+
scrollWidth: 200,
|
|
23
|
+
clientWidth: 100,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
act(() => {
|
|
27
|
+
result.current.handleMouseEnter({ currentTarget: mockCell });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.current.popupOpen).toBe(true);
|
|
31
|
+
expect(result.current.popupContent).toBe("Long text that overflows");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not open popup when content fits", () => {
|
|
35
|
+
const { result } = renderHook(() => useTruncatedPopup());
|
|
36
|
+
|
|
37
|
+
const mockCell = {
|
|
38
|
+
textContent: "Short text",
|
|
39
|
+
scrollWidth: 100,
|
|
40
|
+
clientWidth: 100,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
act(() => {
|
|
44
|
+
result.current.handleMouseEnter({ currentTarget: mockCell });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.current.popupOpen).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("sets popupContent to empty string when textContent is empty", () => {
|
|
51
|
+
const { result } = renderHook(() => useTruncatedPopup());
|
|
52
|
+
|
|
53
|
+
const mockCell = {
|
|
54
|
+
textContent: " ",
|
|
55
|
+
scrollWidth: 200,
|
|
56
|
+
clientWidth: 100,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current.handleMouseEnter({ currentTarget: mockCell });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.current.popupOpen).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("closes popup on mouse leave", () => {
|
|
67
|
+
const { result } = renderHook(() => useTruncatedPopup());
|
|
68
|
+
|
|
69
|
+
const mockCell = {
|
|
70
|
+
textContent: "Long text",
|
|
71
|
+
scrollWidth: 200,
|
|
72
|
+
clientWidth: 100,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
act(() => {
|
|
76
|
+
result.current.handleMouseEnter({ currentTarget: mockCell });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.current.popupOpen).toBe(true);
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
result.current.handleMouseLeave();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.current.popupOpen).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("manually closes popup via setPopupOpen", () => {
|
|
89
|
+
const { result } = renderHook(() => useTruncatedPopup());
|
|
90
|
+
|
|
91
|
+
const mockCell = {
|
|
92
|
+
textContent: "Long text",
|
|
93
|
+
scrollWidth: 200,
|
|
94
|
+
clientWidth: 100,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
act(() => {
|
|
98
|
+
result.current.handleMouseEnter({ currentTarget: mockCell });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result.current.popupOpen).toBe(true);
|
|
102
|
+
|
|
103
|
+
act(() => {
|
|
104
|
+
result.current.setPopupOpen(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.current.popupOpen).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|