@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,284 @@
1
+ import userEvent from "@testing-library/user-event";
2
+ import { waitFor } from "@testing-library/react";
3
+ import { render, waitForLoad } from "@truedat/test/render";
4
+ import { StructureLinkForm } from "../StructureLinkForm";
5
+
6
+ const mockNavigate = jest.fn();
7
+ const mockCreateRelation = jest.fn();
8
+ const mockDispatch = jest.fn();
9
+ const mockSetAlertMessage = jest.fn();
10
+
11
+ jest.mock("react-router", () => ({
12
+ ...jest.requireActual("react-router"),
13
+ useNavigate: () => mockNavigate,
14
+ }));
15
+
16
+ jest.mock("../../hooks/useRelations", () => ({
17
+ useCreateRelation: () => ({
18
+ trigger: mockCreateRelation,
19
+ isMutating: false,
20
+ }),
21
+ }));
22
+
23
+ jest.mock("react-redux", () => ({
24
+ ...jest.requireActual("react-redux"),
25
+ useDispatch: jest.fn(() => mockDispatch),
26
+ useSelector: jest.fn((selector) => {
27
+ const state = {
28
+ selectedRelationTags: [1],
29
+ relationTags: [{ value: { target_type: "data_structure" } }],
30
+ };
31
+ return selector(state);
32
+ }),
33
+ }));
34
+
35
+ jest.mock("react-intl", () => ({
36
+ ...jest.requireActual("react-intl"),
37
+ useIntl: () => ({
38
+ formatMessage: ({ id }) => id,
39
+ }),
40
+ }));
41
+
42
+ jest.mock("@truedat/core/webContext", () => ({
43
+ useWebContext: () => ({
44
+ setAlertMessage: mockSetAlertMessage,
45
+ alertMessage: {},
46
+ }),
47
+ }));
48
+
49
+ jest.mock("../../routines", () => ({
50
+ clearSelectedRelationTags: {
51
+ trigger: jest.fn(() => ({ type: "CLEAR_SELECTED_RELATION_TAGS" })),
52
+ },
53
+ }));
54
+
55
+ jest.mock("../RelationTagsLoader", () =>
56
+ jest.fn(() => <div>RelationTagsLoader</div>)
57
+ );
58
+
59
+ jest.mock("@truedat/lm/components/TagTypeDropdownSelector", () =>
60
+ jest.fn(({ options }) => (
61
+ <div>
62
+ <div>TagTypeDropdownSelector</div>
63
+ {options?.map((option, i) => (
64
+ <button
65
+ key={i}
66
+ onClick={() => option.onClick && option.onClick(option.value)}
67
+ >
68
+ {option.text}
69
+ </button>
70
+ ))}
71
+ </div>
72
+ ))
73
+ );
74
+
75
+ jest.mock("@truedat/dd/components/StructureSelector", () =>
76
+ jest.fn(({ selectedStructure, onSelect }) => (
77
+ <div>
78
+ <div>StructureSelector</div>
79
+ <button
80
+ onClick={() =>
81
+ onSelect({
82
+ id: 456,
83
+ name: "Test Structure",
84
+ })
85
+ }
86
+ >
87
+ Select Structure
88
+ </button>
89
+ {selectedStructure && (
90
+ <div data-testid="selected-structure">{selectedStructure.name}</div>
91
+ )}
92
+ </div>
93
+ ))
94
+ );
95
+
96
+ jest.mock("@truedat/core/components", () => ({
97
+ HistoryBackButton: ({ content }) => <button>{content}</button>,
98
+ }));
99
+
100
+ const props = {
101
+ sourceId: 10,
102
+ sourceType: "quality_control",
103
+ redirectUrl: "/qualityControls/10/version/1/links/structures",
104
+ selectTagOptions: jest.fn(() => [{ id: 1, text: "relates_to", value: 1 }]),
105
+ };
106
+
107
+ describe("<StructureLinkForm />", () => {
108
+ beforeEach(() => {
109
+ jest.clearAllMocks();
110
+ mockCreateRelation.mockResolvedValue({
111
+ data: {
112
+ data: {
113
+ id: 789,
114
+ source_id: 10,
115
+ target_id: 456,
116
+ },
117
+ },
118
+ });
119
+ });
120
+
121
+ it("matches the latest snapshot", async () => {
122
+ const rendered = render(<StructureLinkForm {...props} />);
123
+ await waitForLoad(rendered);
124
+ expect(rendered.container).toMatchSnapshot();
125
+ });
126
+
127
+ it("renders all form components", async () => {
128
+ const rendered = render(<StructureLinkForm {...props} />);
129
+ await waitForLoad(rendered);
130
+
131
+ expect(rendered.getByText("RelationTagsLoader")).toBeInTheDocument();
132
+ expect(rendered.getByText("TagTypeDropdownSelector")).toBeInTheDocument();
133
+ expect(rendered.getByText("StructureSelector")).toBeInTheDocument();
134
+ expect(rendered.getByText("actions.create")).toBeInTheDocument();
135
+ expect(rendered.getByText("actions.cancel")).toBeInTheDocument();
136
+ });
137
+
138
+ it("disables create button when structure or tags are not selected", async () => {
139
+ const rendered = render(<StructureLinkForm {...props} />);
140
+ await waitForLoad(rendered);
141
+
142
+ const createButton = rendered.getByText("actions.create");
143
+ expect(createButton).toBeDisabled();
144
+ });
145
+
146
+ it("enables create button when both structure and tags are selected", async () => {
147
+ const user = userEvent.setup({ delay: null });
148
+ const rendered = render(<StructureLinkForm {...props} />);
149
+ await waitForLoad(rendered);
150
+
151
+ const createButton = rendered.getByText("actions.create");
152
+ expect(createButton).toBeDisabled();
153
+
154
+ await user.click(rendered.getByText("Select Structure"));
155
+
156
+ await waitFor(() => {
157
+ expect(createButton).toBeEnabled();
158
+ });
159
+ });
160
+
161
+ it("calls createRelation and navigates on submit", async () => {
162
+ const user = userEvent.setup({ delay: null });
163
+ const rendered = render(<StructureLinkForm {...props} />);
164
+ await waitForLoad(rendered);
165
+
166
+ await user.click(rendered.getByText("Select Structure"));
167
+
168
+ await waitFor(() => {
169
+ expect(rendered.getByText("actions.create")).toBeEnabled();
170
+ });
171
+
172
+ await user.click(rendered.getByText("actions.create"));
173
+
174
+ await waitFor(() => {
175
+ expect(mockCreateRelation).toHaveBeenCalledWith({
176
+ relation: {
177
+ source_id: 10,
178
+ source_type: "quality_control",
179
+ target_id: 456,
180
+ target_type: "data_structure",
181
+ tag_ids: [1],
182
+ },
183
+ });
184
+ });
185
+
186
+ await waitFor(() => {
187
+ expect(mockNavigate).toHaveBeenCalledWith(props.redirectUrl, {
188
+ state: {
189
+ createdRelation: {
190
+ id: 789,
191
+ source_id: 10,
192
+ target_id: 456,
193
+ target_name: "Test Structure",
194
+ target_data: {
195
+ id: 456,
196
+ name: "Test Structure",
197
+ },
198
+ },
199
+ },
200
+ });
201
+ });
202
+ });
203
+
204
+ it("handles error with _originalErrors and sets alert message", async () => {
205
+ const user = userEvent.setup({ delay: null });
206
+ const error = {
207
+ _originalErrors: {
208
+ source_id: ["has already been taken"],
209
+ },
210
+ message: "An error occurred",
211
+ };
212
+ mockCreateRelation.mockRejectedValueOnce(error);
213
+
214
+ const rendered = render(<StructureLinkForm {...props} />);
215
+ await waitForLoad(rendered);
216
+
217
+ await user.click(rendered.getByText("Select Structure"));
218
+
219
+ await waitFor(() => {
220
+ expect(rendered.getByText("actions.create")).toBeEnabled();
221
+ });
222
+
223
+ await user.click(rendered.getByText("actions.create"));
224
+
225
+ await waitFor(() => {
226
+ expect(mockSetAlertMessage).toHaveBeenCalledWith(
227
+ expect.objectContaining({
228
+ error: true,
229
+ header: "relations.create.failed.header",
230
+ icon: "attention",
231
+ content: expect.any(String),
232
+ })
233
+ );
234
+ });
235
+ });
236
+
237
+ it("handles error without _originalErrors and sets alert message", async () => {
238
+ const user = userEvent.setup({ delay: null });
239
+ const error = {
240
+ message: "Network error",
241
+ };
242
+ mockCreateRelation.mockRejectedValueOnce(error);
243
+
244
+ const rendered = render(<StructureLinkForm {...props} />);
245
+ await waitForLoad(rendered);
246
+
247
+ await user.click(rendered.getByText("Select Structure"));
248
+
249
+ await waitFor(() => {
250
+ expect(rendered.getByText("actions.create")).toBeEnabled();
251
+ });
252
+
253
+ await user.click(rendered.getByText("actions.create"));
254
+
255
+ await waitFor(() => {
256
+ expect(mockSetAlertMessage).toHaveBeenCalledWith({
257
+ error: true,
258
+ content: "Network error",
259
+ });
260
+ });
261
+ });
262
+
263
+ it("dispatches clearSelectedRelationTags on unmount", () => {
264
+ const { unmount } = render(<StructureLinkForm {...props} />);
265
+ unmount();
266
+
267
+ expect(mockDispatch).toHaveBeenCalledWith({
268
+ type: "CLEAR_SELECTED_RELATION_TAGS",
269
+ });
270
+ });
271
+
272
+ it("does not render TagTypeDropdownSelector when tagOptions is empty", async () => {
273
+ const rendered = render(
274
+ <StructureLinkForm
275
+ {...{ ...props, selectTagOptions: jest.fn(() => []) }}
276
+ />
277
+ );
278
+ await waitForLoad(rendered);
279
+
280
+ expect(
281
+ rendered.queryByText("TagTypeDropdownSelector")
282
+ ).not.toBeInTheDocument();
283
+ });
284
+ });
@@ -0,0 +1,48 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<ConceptLinkForm /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui hidden divider"
7
+ />
8
+ <div>
9
+ RelationTagsLoader
10
+ </div>
11
+ <div>
12
+ <div>
13
+ TagTypeDropdownSelector
14
+ </div>
15
+ <button>
16
+ relates_to
17
+ </button>
18
+ </div>
19
+ <div
20
+ class="ui hidden divider"
21
+ />
22
+ <div>
23
+ <div>
24
+ ConceptSelector
25
+ </div>
26
+ <button>
27
+ Select Concept
28
+ </button>
29
+ </div>
30
+ <div
31
+ class="ui hidden divider"
32
+ />
33
+ <div
34
+ class="ui buttons"
35
+ >
36
+ <button
37
+ class="ui primary disabled button"
38
+ disabled=""
39
+ tabindex="-1"
40
+ >
41
+ actions.create
42
+ </button>
43
+ <button>
44
+ actions.cancel
45
+ </button>
46
+ </div>
47
+ </div>
48
+ `;
@@ -0,0 +1,29 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<LinksSearch /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui segment"
7
+ >
8
+ <div>
9
+ SearchWidget
10
+ </div>
11
+ <h4
12
+ class="ui header"
13
+ >
14
+ <i
15
+ aria-hidden="true"
16
+ class="search icon"
17
+ />
18
+ <div
19
+ class="content"
20
+ >
21
+ linked_concepts.empty
22
+ </div>
23
+ </h4>
24
+ <div>
25
+ LinksPagination
26
+ </div>
27
+ </div>
28
+ </div>
29
+ `;
@@ -126,6 +126,19 @@ exports[`<NewRelationTag /> matches the latest snapshot 1`] = `
126
126
  target_type.data_field
127
127
  </span>
128
128
  </div>
129
+ <div
130
+ aria-checked="false"
131
+ aria-selected="false"
132
+ class="item"
133
+ role="option"
134
+ style="pointer-events: all;"
135
+ >
136
+ <span
137
+ class="text"
138
+ >
139
+ target_type.quality_control
140
+ </span>
141
+ </div>
129
142
  </div>
130
143
  </div>
131
144
  </div>
@@ -0,0 +1,49 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<QualityControlConcepts /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui basic segment"
7
+ >
8
+ <div
9
+ class="ui grid"
10
+ >
11
+ <div
12
+ class="sixteen wide column"
13
+ >
14
+ <a
15
+ class="ui primary right floated button"
16
+ href="/qualityControls/10/version/1/links/concepts/new"
17
+ role="button"
18
+ >
19
+ links.actions.create
20
+ </a>
21
+ </div>
22
+ <div
23
+ class="sixteen wide column"
24
+ >
25
+ <div>
26
+ <div>
27
+ LinksSearch
28
+ </div>
29
+ <div
30
+ data-testid="columns-count"
31
+ >
32
+ 5
33
+ </div>
34
+ <div
35
+ data-testid="set-refetch-search"
36
+ >
37
+ function
38
+ </div>
39
+ <div
40
+ data-testid="default-filters"
41
+ >
42
+ {"target_id":10,"target_type":"quality_control","source_type":"business_concept"}
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ `;
@@ -0,0 +1,49 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<QualityControlStructures /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui basic segment"
7
+ >
8
+ <div
9
+ class="ui grid"
10
+ >
11
+ <div
12
+ class="sixteen wide column"
13
+ >
14
+ <a
15
+ class="ui primary right floated button"
16
+ href="/qualityControls/10/version/1/links/structures/new"
17
+ role="button"
18
+ >
19
+ links.actions.create
20
+ </a>
21
+ </div>
22
+ <div
23
+ class="sixteen wide column"
24
+ >
25
+ <div>
26
+ <div>
27
+ LinksSearch
28
+ </div>
29
+ <div
30
+ data-testid="columns-count"
31
+ >
32
+ 8
33
+ </div>
34
+ <div
35
+ data-testid="set-refetch-search"
36
+ >
37
+ function
38
+ </div>
39
+ <div
40
+ data-testid="default-filters"
41
+ >
42
+ {"source_id":10,"target_type":"data_structure","source_type":"quality_control"}
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ `;
@@ -110,6 +110,19 @@ exports[`<RelationTagForm /> matches the latest snapshot 1`] = `
110
110
  target_type.data_field
111
111
  </span>
112
112
  </div>
113
+ <div
114
+ aria-checked="false"
115
+ aria-selected="false"
116
+ class="item"
117
+ role="option"
118
+ style="pointer-events: all;"
119
+ >
120
+ <span
121
+ class="text"
122
+ >
123
+ target_type.quality_control
124
+ </span>
125
+ </div>
113
126
  </div>
114
127
  </div>
115
128
  </div>
@@ -0,0 +1,48 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<StructureLinkForm /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui hidden divider"
7
+ />
8
+ <div>
9
+ RelationTagsLoader
10
+ </div>
11
+ <div>
12
+ <div>
13
+ TagTypeDropdownSelector
14
+ </div>
15
+ <button>
16
+ relates_to
17
+ </button>
18
+ </div>
19
+ <div
20
+ class="ui hidden divider"
21
+ />
22
+ <div>
23
+ <div>
24
+ StructureSelector
25
+ </div>
26
+ <button>
27
+ Select Structure
28
+ </button>
29
+ </div>
30
+ <div
31
+ class="ui hidden divider"
32
+ />
33
+ <div
34
+ class="ui buttons"
35
+ >
36
+ <button
37
+ class="ui primary disabled button"
38
+ disabled=""
39
+ tabindex="-1"
40
+ >
41
+ actions.create
42
+ </button>
43
+ <button>
44
+ actions.cancel
45
+ </button>
46
+ </div>
47
+ </div>
48
+ `;
@@ -0,0 +1,53 @@
1
+ import useSWRMutations from "swr/mutation";
2
+ import { apiJsonPost, apiJsonDelete } from "@truedat/core/services/api";
3
+ import {
4
+ API_RELATION,
5
+ API_RELATIONS,
6
+ API_RELATIONS_INDEX_SEARCH,
7
+ API_RELATIONS_FILTERS,
8
+ } from "../api";
9
+ import { compile } from "path-to-regexp";
10
+
11
+ export const useRelations = (params) => {
12
+ const query = queryString(params);
13
+ return useSWRMutations(API_RELATIONS_INDEX_SEARCH, (url, { arg }) =>
14
+ apiJsonPost(`${url}${query}`, arg)
15
+ );
16
+ };
17
+
18
+ export const useRelationFilters = (params) => {
19
+ const query = queryString(params);
20
+ return useSWRMutations(API_RELATIONS_FILTERS, (url, { arg }) =>
21
+ apiJsonPost(`${url}${query}`, arg)
22
+ );
23
+ };
24
+
25
+ export const useCreateRelation = () => {
26
+ return useSWRMutations(
27
+ API_RELATIONS,
28
+ (url, { arg }) => apiJsonPost(url, arg),
29
+ {
30
+ onError: (error) => {
31
+ // Review! This is a workaround to prevent the middleware from dispatching logError in redux store
32
+ // Remove errors from response to prevent middleware from dispatching logError
33
+ // The error will still be available in the catch block
34
+ if (error?.response?.data?.errors) {
35
+ // Store original errors for our custom handling
36
+ error._originalErrors = error.response.data.errors;
37
+ // Remove errors so middleware doesn't dispatch logError
38
+ delete error.response.data.errors;
39
+ }
40
+ },
41
+ }
42
+ );
43
+ };
44
+
45
+ export const useDeleteRelation = () => {
46
+ return useSWRMutations(API_RELATION, (_url, { arg }) =>
47
+ apiJsonDelete(compile(API_RELATION)({ id: `${arg}` }))
48
+ );
49
+ };
50
+
51
+ const queryString = (params) => {
52
+ return params ? `?${new URLSearchParams(params).toString()}` : "";
53
+ };
@@ -25,4 +25,5 @@ export default {
25
25
  "target_type.implementations": "Implementations",
26
26
  "target_type.ingest": "Ingest",
27
27
  "target_type.data_field": "Structure",
28
+ "relations.create.failed.header": "Failed to create relation",
28
29
  };
@@ -26,4 +26,5 @@ export default {
26
26
  "target_type.implementations": "Implementaciones",
27
27
  "target_type.ingest": "Ingesta",
28
28
  "target_type.data_field": "Estructura",
29
+ "relations.create.failed.header": "Error al crear la relación",
29
30
  };
@@ -1,3 +1,4 @@
1
1
  export * from "./getStructureLinks";
2
2
  export * from "./getImplementationToConceptLinks";
3
3
  export * from "../services/relationGraphTraversal";
4
+ export * from "./messages";
@@ -0,0 +1,48 @@
1
+ import _ from "lodash/fp";
2
+
3
+ export const getRelationErrorAlertMessage = (error, formatMessage) => {
4
+ if (!error._originalErrors) {
5
+ return {
6
+ error: true,
7
+ content: error.message || "An error occurred",
8
+ };
9
+ }
10
+
11
+ const errors = error?._originalErrors;
12
+
13
+ if (errors) {
14
+ const errorMessages = _.flow(
15
+ _.toPairs,
16
+ _.map(([field, messages]) => ({
17
+ field,
18
+ message: Array.isArray(messages) ? messages[0] : messages,
19
+ }))
20
+ )(errors);
21
+
22
+ const content = errorMessages
23
+ .map(({ field, message }) =>
24
+ formatMessage({
25
+ id: `relations.create.failed.${message}`,
26
+ defaultMessage: `${field}: ${message}`,
27
+ })
28
+ )
29
+ .join(", ");
30
+
31
+ return {
32
+ error: true,
33
+ header: "relations.create.failed.header",
34
+ icon: "attention",
35
+ content,
36
+ };
37
+ }
38
+
39
+ const errorMessage =
40
+ data?.error?.message || data?.error || "An error occurred";
41
+
42
+ return {
43
+ error: true,
44
+ header: "relations.create.failed.header",
45
+ icon: "attention",
46
+ content: errorMessage,
47
+ };
48
+ };