@truedat/lm 7.10.3 → 7.10.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 +3 -3
- package/src/components/ConceptSuggestionLinkForm.js +2 -0
- package/src/components/StructureLinks.js +15 -0
- package/src/components/StructureSuggestionLinkForm.js +79 -0
- package/src/components/__tests__/ConceptSuggestionLinkForm.spec.js +5 -0
- package/src/components/__tests__/StructureSuggestionLinkForm.spec.js +127 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/lm",
|
|
3
|
-
"version": "7.10.
|
|
3
|
+
"version": "7.10.4",
|
|
4
4
|
"description": "Truedat Link Manager",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@testing-library/jest-dom": "^6.6.3",
|
|
49
49
|
"@testing-library/react": "^16.3.0",
|
|
50
50
|
"@testing-library/user-event": "^14.6.1",
|
|
51
|
-
"@truedat/test": "7.10.
|
|
51
|
+
"@truedat/test": "7.10.4",
|
|
52
52
|
"identity-obj-proxy": "^3.0.0",
|
|
53
53
|
"jest": "^29.7.0",
|
|
54
54
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"semantic-ui-react": "^3.0.0-beta.2",
|
|
83
83
|
"swr": "^2.3.3"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "0dcedbfaed3964ee02a3d2c175e0f96e2fbc9316"
|
|
86
86
|
}
|
|
@@ -11,6 +11,7 @@ import { Button } from "semantic-ui-react";
|
|
|
11
11
|
|
|
12
12
|
import { createRelation, clearSelectedRelationTags } from "../routines";
|
|
13
13
|
import TagTypeDropdownSelector from "./TagTypeDropdownSelector";
|
|
14
|
+
import RelationTagsLoader from "./RelationTagsLoader";
|
|
14
15
|
|
|
15
16
|
const StructureSuggestions = React.lazy(
|
|
16
17
|
() => import("@truedat/dd/components/StructureSuggestions")
|
|
@@ -55,6 +56,7 @@ export const ConceptSuggestionLinkForm = () => {
|
|
|
55
56
|
return (
|
|
56
57
|
<>
|
|
57
58
|
<Divider hidden />
|
|
59
|
+
<RelationTagsLoader />
|
|
58
60
|
{!_.isEmpty(tagOptions) && (
|
|
59
61
|
<>
|
|
60
62
|
<TagTypeDropdownSelector options={tagOptions} />
|
|
@@ -15,6 +15,7 @@ export const StructureLinks = ({
|
|
|
15
15
|
structure,
|
|
16
16
|
structureLinks,
|
|
17
17
|
canCreateLink,
|
|
18
|
+
canSuggestConceptLink,
|
|
18
19
|
}) => {
|
|
19
20
|
const structureLinkColumnsWithTags = [
|
|
20
21
|
{
|
|
@@ -75,6 +76,18 @@ export const StructureLinks = ({
|
|
|
75
76
|
to={linkTo.STRUCTURE_LINKS_NEW({ id })}
|
|
76
77
|
content={<FormattedMessage id="links.actions.create" />}
|
|
77
78
|
/>
|
|
79
|
+
{canSuggestConceptLink && (
|
|
80
|
+
<Grid.Column width={16}>
|
|
81
|
+
<Button
|
|
82
|
+
floated="right"
|
|
83
|
+
icon="lightbulb outline"
|
|
84
|
+
secondary
|
|
85
|
+
as={Link}
|
|
86
|
+
to={linkTo.STRUCTURE_LINKS_CONCEPTS_SUGGEST({ id })}
|
|
87
|
+
content={<FormattedMessage id="links.actions.suggest" />}
|
|
88
|
+
/>
|
|
89
|
+
</Grid.Column>
|
|
90
|
+
)}
|
|
78
91
|
</Grid.Column>
|
|
79
92
|
)}
|
|
80
93
|
{!_.isEmpty(structureLinks) && (
|
|
@@ -94,6 +107,7 @@ StructureLinks.propTypes = {
|
|
|
94
107
|
structure: PropTypes.object,
|
|
95
108
|
canCreateLink: PropTypes.bool,
|
|
96
109
|
structureLinks: PropTypes.array,
|
|
110
|
+
canSuggestConceptLink: PropTypes.bool,
|
|
97
111
|
};
|
|
98
112
|
|
|
99
113
|
const mapStateToProps = (state) => ({
|
|
@@ -102,6 +116,7 @@ const mapStateToProps = (state) => ({
|
|
|
102
116
|
getStructureToConceptLinks(state)
|
|
103
117
|
),
|
|
104
118
|
canCreateLink: _.has("create_link")(state.structureActions),
|
|
119
|
+
canSuggestConceptLink: _.has("suggest_concept_link")(state.structureActions),
|
|
105
120
|
});
|
|
106
121
|
|
|
107
122
|
export default connect(mapStateToProps)(StructureLinks);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import React, { useState, useEffect } from "react";
|
|
3
|
+
import { Divider } from "semantic-ui-react";
|
|
4
|
+
import { useParams } from "react-router";
|
|
5
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
6
|
+
import { useIntl } from "react-intl";
|
|
7
|
+
import { makeTagOptionsSelector } from "@truedat/core/selectors";
|
|
8
|
+
import { HistoryBackButton } from "@truedat/core/components";
|
|
9
|
+
import { linkTo } from "@truedat/core/routes";
|
|
10
|
+
import { Button } from "semantic-ui-react";
|
|
11
|
+
|
|
12
|
+
import { linkConcept, clearSelectedRelationTags } from "../routines";
|
|
13
|
+
import TagTypeDropdownSelector from "./TagTypeDropdownSelector";
|
|
14
|
+
import RelationTagsLoader from "./RelationTagsLoader";
|
|
15
|
+
|
|
16
|
+
const ConceptSuggestions = React.lazy(() => import("@truedat/bg/concepts/components/ConceptSuggestions"));
|
|
17
|
+
|
|
18
|
+
const selectTagOptions = makeTagOptionsSelector("data_field");
|
|
19
|
+
|
|
20
|
+
export const StructureSuggestionLinkForm = () => {
|
|
21
|
+
const { id: targetId } = useParams();
|
|
22
|
+
const selectedRelationTags = useSelector((state) => state?.selectedRelationTags);
|
|
23
|
+
const tagOptions = useSelector(selectTagOptions);
|
|
24
|
+
const conceptLinking = useSelector((state) => state?.conceptLinking)
|
|
25
|
+
const dispatch = useDispatch();
|
|
26
|
+
const [selectedConcept, setSelectedConcept] = useState(null);
|
|
27
|
+
const { formatMessage } = useIntl();
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
return () => {
|
|
31
|
+
dispatch(clearSelectedRelationTags.trigger());
|
|
32
|
+
};
|
|
33
|
+
}, [clearSelectedRelationTags, dispatch]);
|
|
34
|
+
|
|
35
|
+
const handleSubmit = () => {
|
|
36
|
+
const link = {
|
|
37
|
+
redirectUrl: linkTo.STRUCTURE_LINKS({ id: targetId }),
|
|
38
|
+
source_id: selectedConcept?.business_concept_id,
|
|
39
|
+
source_type: "business_concept",
|
|
40
|
+
target_id: targetId,
|
|
41
|
+
target_type: "data_structure",
|
|
42
|
+
tag_ids: selectedRelationTags ? selectedRelationTags : [],
|
|
43
|
+
origin: "suggested",
|
|
44
|
+
};
|
|
45
|
+
dispatch(linkConcept.trigger(link));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const disabled = !(selectedConcept && selectedRelationTags);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<Divider hidden />
|
|
53
|
+
<RelationTagsLoader />
|
|
54
|
+
{!_.isEmpty(tagOptions) && (
|
|
55
|
+
<>
|
|
56
|
+
<TagTypeDropdownSelector options={tagOptions} />
|
|
57
|
+
<Divider hidden />
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
<ConceptSuggestions
|
|
61
|
+
handleConceptSelected={setSelectedConcept}
|
|
62
|
+
selectedConcept={selectedConcept}
|
|
63
|
+
/>
|
|
64
|
+
<Divider hidden />
|
|
65
|
+
<Button.Group>
|
|
66
|
+
<Button
|
|
67
|
+
primary
|
|
68
|
+
loading={conceptLinking}
|
|
69
|
+
content={formatMessage({ id: "actions.create" })}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
onClick={handleSubmit}
|
|
72
|
+
/>
|
|
73
|
+
<HistoryBackButton content={formatMessage({ id: "actions.cancel" })} />
|
|
74
|
+
</Button.Group>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default StructureSuggestionLinkForm;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { StructureSuggestionLinkForm } from "../StructureSuggestionLinkForm";
|
|
4
|
+
import { useDispatch } from "react-redux";
|
|
5
|
+
import { useParams } from "react-router";
|
|
6
|
+
import { linkConcept, clearSelectedRelationTags } from "../../routines";
|
|
7
|
+
|
|
8
|
+
jest.mock("react-redux", () => ({
|
|
9
|
+
...jest.requireActual("react-redux"),
|
|
10
|
+
useDispatch: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock("react-router", () => ({
|
|
14
|
+
...jest.requireActual("react-router"),
|
|
15
|
+
useParams: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
jest.mock("react-intl", () => ({
|
|
20
|
+
...jest.requireActual("react-intl"),
|
|
21
|
+
useIntl: () => ({
|
|
22
|
+
formatMessage: ({ id }) => id,
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// Mock components
|
|
28
|
+
jest.mock("@truedat/bg/concepts/components/ConceptSuggestions", () =>
|
|
29
|
+
jest.fn((props) => {
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<div>MockConceptSuggestions</div>
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => props.handleConceptSelected({ business_concept_id: "concept123" })}
|
|
35
|
+
>
|
|
36
|
+
Select Concept
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
jest.mock("../TagTypeDropdownSelector", () =>
|
|
44
|
+
jest.fn(() => <div>MockTagTypeDropdownSelector</div>)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
jest.mock("../RelationTagsLoader", () =>
|
|
48
|
+
jest.fn(() => <div>RelationTagsLoader</div>)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
jest.mock("@truedat/core/components", () => ({
|
|
52
|
+
HistoryBackButton: (props) => <button>{props.content}</button>,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
jest.mock("@truedat/core/routes", () => ({
|
|
56
|
+
linkTo: { STRUCTURE_LINKS: jest.fn(() => "/mocked/redirect/url"), },
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
describe("StructureSuggestionLinkForm", () => {
|
|
60
|
+
const mockDispatch = jest.fn();
|
|
61
|
+
const selectedRelationTags = ["tag1"];
|
|
62
|
+
const relationTags = [{
|
|
63
|
+
id: "tag1",
|
|
64
|
+
value: {
|
|
65
|
+
type: "tag1",
|
|
66
|
+
label: "tag1",
|
|
67
|
+
target_type: "data_field",
|
|
68
|
+
},
|
|
69
|
+
}]
|
|
70
|
+
const conceptLinking = false;
|
|
71
|
+
const state = { selectedRelationTags, relationTags, conceptLinking };
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
|
|
76
|
+
useParams.mockReturnValue({ id: "s1" });
|
|
77
|
+
useDispatch.mockReturnValue(mockDispatch);
|
|
78
|
+
|
|
79
|
+
clearSelectedRelationTags.trigger = jest.fn(() => ({ type: "CLEAR_TAGS" }));
|
|
80
|
+
linkConcept.trigger = jest.fn((payload) => ({
|
|
81
|
+
type: "LINK_CONCEPT",
|
|
82
|
+
payload,
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders concepet suggestions and tag dropdown", async () => {
|
|
87
|
+
render(<StructureSuggestionLinkForm />, { state });
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(screen.getByText("MockConceptSuggestions")).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByText("actions.create")).toBeDisabled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("dispatches linkConcept on submit", async () => {
|
|
96
|
+
render(<StructureSuggestionLinkForm />, { state });
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByText("Select Concept"));
|
|
99
|
+
|
|
100
|
+
const createBtn = screen.getByText("actions.create");
|
|
101
|
+
expect(createBtn).not.toBeDisabled();
|
|
102
|
+
|
|
103
|
+
fireEvent.click(createBtn);
|
|
104
|
+
|
|
105
|
+
expect(linkConcept.trigger).toHaveBeenCalledWith({
|
|
106
|
+
redirectUrl: "/mocked/redirect/url",
|
|
107
|
+
source_id: "concept123",
|
|
108
|
+
source_type: "business_concept",
|
|
109
|
+
target_id: "s1",
|
|
110
|
+
target_type: "data_structure",
|
|
111
|
+
tag_ids: ["tag1"],
|
|
112
|
+
origin: "suggested",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(mockDispatch).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({ type: "LINK_CONCEPT" })
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("dispatches clearSelectedRelationTags on unmount", () => {
|
|
121
|
+
const { unmount } = render(<StructureSuggestionLinkForm />, { state });
|
|
122
|
+
unmount();
|
|
123
|
+
|
|
124
|
+
expect(clearSelectedRelationTags.trigger).toHaveBeenCalled();
|
|
125
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: "CLEAR_TAGS" });
|
|
126
|
+
});
|
|
127
|
+
});
|