@truedat/lm 8.2.0 → 8.2.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.
Files changed (26) hide show
  1. package/package.json +3 -3
  2. package/src/api.js +4 -0
  3. package/src/components/ConceptLinkForm.js +111 -0
  4. package/src/components/LinksPagination.js +15 -0
  5. package/src/components/LinksSearch.js +139 -0
  6. package/src/components/QualityControlConcepts.js +156 -0
  7. package/src/components/QualityControlStructures.js +183 -0
  8. package/src/components/RelationTagForm.js +4 -0
  9. package/src/components/StructureLinkForm.js +105 -0
  10. package/src/components/__tests__/ConceptLinkForm.spec.js +252 -0
  11. package/src/components/__tests__/LinksSearch.spec.js +299 -0
  12. package/src/components/__tests__/QualityControlConcepts.spec.js +262 -0
  13. package/src/components/__tests__/QualityControlStructures.spec.js +213 -0
  14. package/src/components/__tests__/StructureLinkForm.spec.js +284 -0
  15. package/src/components/__tests__/__snapshots__/ConceptLinkForm.spec.js.snap +48 -0
  16. package/src/components/__tests__/__snapshots__/LinksSearch.spec.js.snap +29 -0
  17. package/src/components/__tests__/__snapshots__/NewRelationTag.spec.js.snap +13 -0
  18. package/src/components/__tests__/__snapshots__/QualityControlConcepts.spec.js.snap +49 -0
  19. package/src/components/__tests__/__snapshots__/QualityControlStructures.spec.js.snap +49 -0
  20. package/src/components/__tests__/__snapshots__/RelationTagForm.spec.js.snap +13 -0
  21. package/src/components/__tests__/__snapshots__/StructureLinkForm.spec.js.snap +48 -0
  22. package/src/hooks/useRelations.js +53 -0
  23. package/src/messages/en.js +1 -0
  24. package/src/messages/es.js +1 -0
  25. package/src/selectors/index.js +1 -0
  26. package/src/selectors/messages.js +48 -0
@@ -0,0 +1,262 @@
1
+ import { waitFor } from "@testing-library/react";
2
+ import { render, waitForLoad } from "@truedat/test/render";
3
+ import { QualityControlConcepts } from "../QualityControlConcepts";
4
+
5
+ const mockDeleteRelation = jest.fn();
6
+
7
+ jest.mock("../../hooks/useRelations", () => ({
8
+ useDeleteRelation: jest.fn(),
9
+ }));
10
+
11
+ jest.mock("react-router", () => ({
12
+ ...jest.requireActual("react-router"),
13
+ useLocation: jest.fn(() => ({ state: null })),
14
+ Link: ({ children, to, ...props }) => (
15
+ <a href={to} {...props}>
16
+ {children}
17
+ </a>
18
+ ),
19
+ }));
20
+
21
+ jest.mock("../LinksSearch", () =>
22
+ jest.fn(
23
+ ({
24
+ columns,
25
+ extraRow,
26
+ deletedLinkId,
27
+ defaultFilters,
28
+ refetchSearch,
29
+ setRefetchSearch,
30
+ }) => (
31
+ <div>
32
+ <div>LinksSearch</div>
33
+ <div data-testid="columns-count">{columns?.length || 0}</div>
34
+ {extraRow && (
35
+ <div data-testid="extra-row">{JSON.stringify(extraRow)}</div>
36
+ )}
37
+ {deletedLinkId && (
38
+ <div data-testid="deleted-link-id">{deletedLinkId}</div>
39
+ )}
40
+ {refetchSearch && <div data-testid="refetch-search">true</div>}
41
+ {setRefetchSearch && (
42
+ <div data-testid="set-refetch-search">function</div>
43
+ )}
44
+ <div data-testid="default-filters">
45
+ {JSON.stringify(defaultFilters)}
46
+ </div>
47
+ </div>
48
+ )
49
+ )
50
+ );
51
+
52
+ jest.mock("../ConfirmDeleteRelation", () =>
53
+ jest.fn(({ id, deleteRelation }) => (
54
+ <button
55
+ data-testid={`delete-relation-${id}`}
56
+ onClick={() => deleteRelation({ id })}
57
+ >
58
+ Delete {id}
59
+ </button>
60
+ ))
61
+ );
62
+
63
+ jest.mock("@truedat/core/routes", () => ({
64
+ linkTo: {
65
+ QUALITY_CONTROL_CONCEPT_LINKS_NEW: jest.fn(
66
+ ({ id, version }) =>
67
+ `/qualityControls/${id}/version/${version}/links/concepts/new`
68
+ ),
69
+ CONCEPT_LINKS_QUALITY_CONTROLS: jest.fn(
70
+ ({ business_concept_id, id }) =>
71
+ `/concepts/${business_concept_id}/links/qualityControls/${id}`
72
+ ),
73
+ },
74
+ }));
75
+
76
+ const qualityControl = {
77
+ id: 10,
78
+ version: 1,
79
+ name: "Test Quality Control",
80
+ };
81
+
82
+ const actions = ["link_to_business_concept"];
83
+
84
+ describe("<QualityControlConcepts />", () => {
85
+ const { useLocation } = require("react-router");
86
+ const { useDeleteRelation } = require("../../hooks/useRelations");
87
+
88
+ beforeEach(() => {
89
+ jest.clearAllMocks();
90
+ useLocation.mockReturnValue({ state: null });
91
+ mockDeleteRelation.mockResolvedValue({ status: 204, data: { id: 5 } });
92
+ useDeleteRelation.mockReturnValue({
93
+ trigger: mockDeleteRelation,
94
+ isMutating: false,
95
+ });
96
+ });
97
+
98
+ it("matches the latest snapshot", async () => {
99
+ const rendered = render(
100
+ <QualityControlConcepts
101
+ qualityControl={qualityControl}
102
+ actions={actions}
103
+ />
104
+ );
105
+ await waitForLoad(rendered);
106
+ expect(rendered.container).toMatchSnapshot();
107
+ });
108
+
109
+ it("renders LinksSearch with correct props", async () => {
110
+ const rendered = render(
111
+ <QualityControlConcepts
112
+ qualityControl={qualityControl}
113
+ actions={actions}
114
+ />
115
+ );
116
+ await waitForLoad(rendered);
117
+
118
+ expect(rendered.getByText("LinksSearch")).toBeInTheDocument();
119
+ expect(rendered.getByTestId("default-filters").textContent).toBe(
120
+ JSON.stringify({
121
+ target_id: 10,
122
+ target_type: "quality_control",
123
+ source_type: "business_concept",
124
+ })
125
+ );
126
+ });
127
+
128
+ it("shows create button when user has link_to_business_concept permission", async () => {
129
+ const rendered = render(
130
+ <QualityControlConcepts
131
+ qualityControl={qualityControl}
132
+ actions={actions}
133
+ />
134
+ );
135
+ await waitForLoad(rendered);
136
+
137
+ await waitFor(() => {
138
+ expect(rendered.getByText("links.actions.create")).toBeInTheDocument();
139
+ });
140
+ });
141
+
142
+ it("hides create button when user does not have link_to_business_concept permission", async () => {
143
+ const rendered = render(
144
+ <QualityControlConcepts qualityControl={qualityControl} actions={[]} />
145
+ );
146
+ await waitForLoad(rendered);
147
+
148
+ expect(
149
+ rendered.queryByText("links.actions.create")
150
+ ).not.toBeInTheDocument();
151
+ });
152
+
153
+ it("includes delete column when user has permission", async () => {
154
+ const rendered = render(
155
+ <QualityControlConcepts
156
+ qualityControl={qualityControl}
157
+ actions={actions}
158
+ />
159
+ );
160
+ await waitForLoad(rendered);
161
+
162
+ await waitFor(() => {
163
+ const columnsCount = parseInt(
164
+ rendered.getByTestId("columns-count").textContent
165
+ );
166
+ expect(columnsCount).toBe(5); // name, domain, tags, status, delete
167
+ });
168
+ });
169
+
170
+ it("excludes delete column when user does not have permission", async () => {
171
+ const rendered = render(
172
+ <QualityControlConcepts qualityControl={qualityControl} actions={[]} />
173
+ );
174
+ await waitForLoad(rendered);
175
+
176
+ await waitFor(() => {
177
+ const columnsCount = parseInt(
178
+ rendered.getByTestId("columns-count").textContent
179
+ );
180
+ expect(columnsCount).toBe(4); // name, domain, tags, status (no delete)
181
+ });
182
+ });
183
+
184
+ it("passes extraRow from location.state to LinksSearch", async () => {
185
+ const createdRelation = {
186
+ id: 999,
187
+ source_name: "New Concept",
188
+ source_data: { name: "New Concept" },
189
+ };
190
+
191
+ useLocation.mockReturnValue({ state: { createdRelation } });
192
+
193
+ const rendered = render(
194
+ <QualityControlConcepts
195
+ qualityControl={qualityControl}
196
+ actions={actions}
197
+ />
198
+ );
199
+ await waitForLoad(rendered);
200
+
201
+ await waitFor(() => {
202
+ expect(rendered.getByTestId("extra-row")).toBeInTheDocument();
203
+ expect(rendered.getByTestId("extra-row").textContent).toBe(
204
+ JSON.stringify(createdRelation)
205
+ );
206
+ });
207
+ });
208
+
209
+ it("sets up delete relation handler correctly", async () => {
210
+ const rendered = render(
211
+ <QualityControlConcepts
212
+ qualityControl={qualityControl}
213
+ actions={actions}
214
+ />
215
+ );
216
+ await waitForLoad(rendered);
217
+
218
+ await waitFor(() => {
219
+ expect(rendered.getByText("LinksSearch")).toBeInTheDocument();
220
+ });
221
+
222
+ // Verify the delete column is included when user has permission
223
+ const columnsCount = parseInt(
224
+ rendered.getByTestId("columns-count").textContent
225
+ );
226
+ expect(columnsCount).toBe(5); // Includes delete column
227
+ });
228
+
229
+ it("handles missing qualityControl gracefully", async () => {
230
+ const rendered = render(
231
+ <QualityControlConcepts qualityControl={null} actions={actions} />
232
+ );
233
+ await waitForLoad(rendered);
234
+
235
+ expect(rendered.getByText("LinksSearch")).toBeInTheDocument();
236
+ expect(rendered.getByTestId("default-filters").textContent).toBe(
237
+ JSON.stringify({
238
+ target_id: undefined,
239
+ target_type: "quality_control",
240
+ source_type: "business_concept",
241
+ })
242
+ );
243
+ });
244
+
245
+ it("shows loading state when deleting", () => {
246
+ useDeleteRelation.mockReturnValueOnce({
247
+ trigger: mockDeleteRelation,
248
+ isMutating: true,
249
+ });
250
+
251
+ const rendered = render(
252
+ <QualityControlConcepts
253
+ qualityControl={qualityControl}
254
+ actions={actions}
255
+ />
256
+ );
257
+
258
+ expect(
259
+ rendered.container.querySelector(".segment.loading")
260
+ ).toBeInTheDocument();
261
+ });
262
+ });
@@ -0,0 +1,213 @@
1
+ import { waitFor } from "@testing-library/react";
2
+ import { render, waitForLoad } from "@truedat/test/render";
3
+ import { QualityControlStructures } from "../QualityControlStructures";
4
+
5
+ const mockDeleteRelation = jest.fn();
6
+
7
+ jest.mock("../../hooks/useRelations", () => ({
8
+ useDeleteRelation: jest.fn(),
9
+ useRelations: jest.fn(),
10
+ useRelationFilters: jest.fn(),
11
+ }));
12
+
13
+ jest.mock("react-router", () => ({
14
+ ...jest.requireActual("react-router"),
15
+ useLocation: jest.fn(() => ({ state: null })),
16
+ Link: ({ children, to, ...props }) => (
17
+ <a href={to} {...props}>
18
+ {children}
19
+ </a>
20
+ ),
21
+ }));
22
+
23
+ jest.mock("../LinksSearch", () =>
24
+ jest.fn(
25
+ ({
26
+ columns,
27
+ extraRow,
28
+ deletedLinkId,
29
+ defaultFilters,
30
+ refetchSearch,
31
+ setRefetchSearch,
32
+ }) => (
33
+ <div>
34
+ <div>LinksSearch</div>
35
+ <div data-testid="columns-count">{columns?.length || 0}</div>
36
+ {extraRow && (
37
+ <div data-testid="extra-row">{JSON.stringify(extraRow)}</div>
38
+ )}
39
+ {deletedLinkId && (
40
+ <div data-testid="deleted-link-id">{deletedLinkId}</div>
41
+ )}
42
+ {refetchSearch && <div data-testid="refetch-search">true</div>}
43
+ {setRefetchSearch && (
44
+ <div data-testid="set-refetch-search">function</div>
45
+ )}
46
+ <div data-testid="default-filters">
47
+ {JSON.stringify(defaultFilters)}
48
+ </div>
49
+ </div>
50
+ )
51
+ )
52
+ );
53
+
54
+ jest.mock("../ConfirmDeleteRelation", () =>
55
+ jest.fn(({ id, deleteRelation }) => (
56
+ <button
57
+ data-testid={`delete-relation-${id}`}
58
+ onClick={() => deleteRelation({ id })}
59
+ >
60
+ Delete {id}
61
+ </button>
62
+ ))
63
+ );
64
+
65
+ jest.mock("@truedat/core/routes", () => ({
66
+ linkTo: {
67
+ QUALITY_CONTROL_STRUCTURE_LINKS_NEW: jest.fn(
68
+ ({ id, version }) =>
69
+ `/qualityControls/${id}/version/${version}/links/structures/new`
70
+ ),
71
+ STRUCTURE: jest.fn(({ id }) => `/structures/${id}`),
72
+ },
73
+ }));
74
+
75
+ const qualityControl = {
76
+ id: 10,
77
+ version: 1,
78
+ name: "Test Quality Control",
79
+ };
80
+
81
+ const actions = ["link_to_data_structure"];
82
+
83
+ describe("<QualityControlStructures />", () => {
84
+ const { useLocation } = require("react-router");
85
+ const {
86
+ useDeleteRelation,
87
+ useRelations,
88
+ useRelationFilters,
89
+ } = require("../../hooks/useRelations");
90
+
91
+ beforeEach(() => {
92
+ jest.clearAllMocks();
93
+ useLocation.mockReturnValue({ state: null });
94
+ mockDeleteRelation.mockResolvedValue({ status: 204 });
95
+ useDeleteRelation.mockReturnValue({
96
+ trigger: mockDeleteRelation,
97
+ isMutating: false,
98
+ });
99
+ useRelations.mockReturnValue({
100
+ trigger: jest.fn(),
101
+ });
102
+ useRelationFilters.mockReturnValue({
103
+ trigger: jest.fn(),
104
+ });
105
+ });
106
+
107
+ it("matches the latest snapshot", async () => {
108
+ const rendered = render(
109
+ <QualityControlStructures
110
+ qualityControl={qualityControl}
111
+ actions={actions}
112
+ />
113
+ );
114
+ await waitForLoad(rendered);
115
+ expect(rendered.container).toMatchSnapshot();
116
+ });
117
+
118
+ it("renders create button when canCreateLink is true", async () => {
119
+ const rendered = render(
120
+ <QualityControlStructures
121
+ qualityControl={qualityControl}
122
+ actions={actions}
123
+ />
124
+ );
125
+ await waitForLoad(rendered);
126
+
127
+ expect(rendered.getByText("links.actions.create")).toBeInTheDocument();
128
+ });
129
+
130
+ it("does not render create button when canCreateLink is false", async () => {
131
+ const rendered = render(
132
+ <QualityControlStructures qualityControl={qualityControl} actions={[]} />
133
+ );
134
+ await waitForLoad(rendered);
135
+
136
+ expect(
137
+ rendered.queryByText("links.actions.create")
138
+ ).not.toBeInTheDocument();
139
+ });
140
+
141
+ it("renders LinksSearch with correct props", async () => {
142
+ const rendered = render(
143
+ <QualityControlStructures
144
+ qualityControl={qualityControl}
145
+ actions={actions}
146
+ />
147
+ );
148
+ await waitForLoad(rendered);
149
+
150
+ const defaultFilters = JSON.parse(
151
+ rendered.getByTestId("default-filters").textContent
152
+ );
153
+ expect(defaultFilters).toEqual({
154
+ source_id: 10,
155
+ target_type: "data_structure",
156
+ source_type: "quality_control",
157
+ });
158
+ });
159
+
160
+ it("includes delete column when user has permission", async () => {
161
+ const rendered = render(
162
+ <QualityControlStructures
163
+ qualityControl={qualityControl}
164
+ actions={actions}
165
+ />
166
+ );
167
+ await waitForLoad(rendered);
168
+
169
+ await waitFor(() => {
170
+ const columnsCount = parseInt(
171
+ rendered.getByTestId("columns-count").textContent
172
+ );
173
+ // Should include delete column: name, domain, system, path, last_change_at, relation_tags, deleted_at, delete
174
+ expect(columnsCount).toBe(8);
175
+ });
176
+ });
177
+
178
+ it("excludes delete column when user does not have permission", async () => {
179
+ const rendered = render(
180
+ <QualityControlStructures qualityControl={qualityControl} actions={[]} />
181
+ );
182
+ await waitForLoad(rendered);
183
+
184
+ await waitFor(() => {
185
+ const columnsCount = parseInt(
186
+ rendered.getByTestId("columns-count").textContent
187
+ );
188
+ // Should not include delete column: name, domain, system, path, last_change_at, relation_tags, deleted_at
189
+ expect(columnsCount).toBe(7);
190
+ });
191
+ });
192
+
193
+ it("displays extraRow when createdRelation is in location state", async () => {
194
+ const createdRelation = {
195
+ id: 999,
196
+ source_id: 10,
197
+ target_id: 456,
198
+ };
199
+ useLocation.mockReturnValue({ state: { createdRelation } });
200
+
201
+ const rendered = render(
202
+ <QualityControlStructures
203
+ qualityControl={qualityControl}
204
+ actions={actions}
205
+ />
206
+ );
207
+ await waitForLoad(rendered);
208
+
209
+ expect(rendered.getByTestId("extra-row")).toBeInTheDocument();
210
+ const extraRow = JSON.parse(rendered.getByTestId("extra-row").textContent);
211
+ expect(extraRow).toEqual(createdRelation);
212
+ });
213
+ });