@truedat/bg 5.2.1 → 5.2.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.2.3] 2023-02-23
4
+
5
+ ### Added
6
+
7
+ - [TD-4554] Concept links manager
8
+
3
9
  ## [5.1.1] 2023-02-10
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/bg",
3
- "version": "5.2.1",
3
+ "version": "5.2.3",
4
4
  "description": "Truedat Web Business Glossary",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.16.5",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "5.2.1",
37
+ "@truedat/test": "5.2.3",
38
38
  "babel-jest": "^28.1.0",
39
39
  "babel-plugin-dynamic-import-node": "^2.3.3",
40
40
  "babel-plugin-lodash": "^3.3.4",
@@ -86,8 +86,8 @@
86
86
  ]
87
87
  },
88
88
  "dependencies": {
89
- "@truedat/core": "5.2.1",
90
- "@truedat/df": "5.2.1",
89
+ "@truedat/core": "5.2.3",
90
+ "@truedat/df": "5.2.3",
91
91
  "decode-uri-component": "^0.2.2",
92
92
  "file-saver": "^2.0.5",
93
93
  "moment": "^2.29.4",
@@ -110,5 +110,5 @@
110
110
  "react-dom": ">= 16.8.6 < 17",
111
111
  "semantic-ui-react": ">= 2.0.3 < 2.2"
112
112
  },
113
- "gitHead": "1f9ce2ad2fb29d1d8dc5ccb8940a7f573e7127bc"
113
+ "gitHead": "fecedbbb623fc091ced1eebf41bdcea30f2800fa"
114
114
  }
@@ -6,13 +6,14 @@ import { connect } from "react-redux";
6
6
  import { Unauthorized } from "@truedat/core/components";
7
7
  import { useAuthorized } from "@truedat/core/hooks";
8
8
  import {
9
- CONCEPTS,
9
+ CONCEPT_EDIT,
10
+ CONCEPT_LINKS_MANAGEMENT,
11
+ CONCEPT_VERSION,
10
12
  CONCEPTS_BULK_UPDATE,
11
13
  CONCEPTS_NEW,
12
14
  CONCEPTS_PENDING,
13
15
  CONCEPTS_SUBSCOPED,
14
- CONCEPT_EDIT,
15
- CONCEPT_VERSION,
16
+ CONCEPTS,
16
17
  } from "@truedat/core/routes";
17
18
  import { useIntl } from "react-intl";
18
19
  import Concept from "./Concept";
@@ -21,6 +22,7 @@ import ConceptEdit from "./ConceptEdit";
21
22
  import ConceptFiltersLoader from "./ConceptFiltersLoader";
22
23
  import ConceptUserFiltersLoader from "./ConceptUserFiltersLoader";
23
24
  import ConceptForm from "./ConceptForm";
25
+ import ConceptsLinksManagement from "./ConceptsLinksManagement";
24
26
  import ConceptLoader from "./ConceptLoader";
25
27
  import Concepts from "./Concepts";
26
28
  import ConceptsBulkUpdate from "./ConceptsBulkUpdate";
@@ -50,6 +52,29 @@ export const ConceptRoutes = ({ concept, conceptLoaded, templatesLoaded }) => {
50
52
  };
51
53
  return (
52
54
  <>
55
+ <Route
56
+ exact
57
+ path={CONCEPT_LINKS_MANAGEMENT}
58
+ render={() =>
59
+ authorized ? (
60
+ <>
61
+ <RelationTagsLoader />
62
+ <ConceptsLinksManagement
63
+ header={formatMessage({
64
+ id: "concepts.header.linksManager",
65
+ })}
66
+ subheader={formatMessage({
67
+ id: "concepts.subheader.linksManager",
68
+ })}
69
+ icon="linkify"
70
+ />
71
+ </>
72
+ ) : (
73
+ <Unauthorized />
74
+ )
75
+ }
76
+ />
77
+
53
78
  <Route
54
79
  exact
55
80
  path={CONCEPTS_PENDING}
@@ -74,7 +99,6 @@ export const ConceptRoutes = ({ concept, conceptLoaded, templatesLoaded }) => {
74
99
  )
75
100
  }
76
101
  />
77
-
78
102
  <Route
79
103
  exact
80
104
  path={CONCEPTS_SUBSCOPED}
@@ -0,0 +1,326 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState, useEffect } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
5
+ import { connect } from "react-redux";
6
+ import { Header, Icon, Segment, Grid, Message } from "semantic-ui-react";
7
+ import {
8
+ makeTagOptionsSelector,
9
+ makeSearchQuerySelector,
10
+ } from "@truedat/core/selectors";
11
+ import {
12
+ clearStructures,
13
+ fetchStructures,
14
+ clearStructureFilters,
15
+ fetchStructureFilters,
16
+ resetStructureFilters,
17
+ } from "@truedat/dd/routines";
18
+ import { createRelation, deleteRelation } from "@truedat/core/routines";
19
+ import ConceptSelector from "../relations/components/ConceptSelector";
20
+ import { fetchConcepts } from "../routines";
21
+
22
+ const TagTypeDropdownSelector = React.lazy(() =>
23
+ import("@truedat/lm/components/TagTypeDropdownSelector")
24
+ );
25
+
26
+ const StructureSelector = React.lazy(() =>
27
+ import("@truedat/dd/components/StructureSelector")
28
+ );
29
+
30
+ const linksDefaultFilters = {
31
+ current: [true],
32
+ status: ["pending_approval", "draft", "rejected", "published"],
33
+ };
34
+
35
+ const STATUS_PENDING = "pending";
36
+ const STATUS_CREATED = "created";
37
+ const STATUS_DELETED = "deleted";
38
+
39
+ const LinkedMessage = ({
40
+ selectedConcept,
41
+ selectedStructure,
42
+ selectedTag,
43
+ status,
44
+ }) => {
45
+ const { formatMessage } = useIntl();
46
+
47
+ const messagesMap = {
48
+ [STATUS_PENDING]: formatMessage({ id: "concept.events.relation_creating" }),
49
+ [STATUS_DELETED]: formatMessage({ id: "concept.events.relation_deleted" }),
50
+ [STATUS_CREATED]: formatMessage({ id: "concept.events.relation_created" }),
51
+ };
52
+
53
+ return (
54
+ <Message success={status !== STATUS_PENDING} floating>
55
+ <Message.Header>{messagesMap[status]}</Message.Header>
56
+ <Message.List>
57
+ {selectedConcept ? (
58
+ <Message.Item>
59
+ {formatMessage({ id: "conceptRelations.concept" })}
60
+ {": "} {selectedConcept.name}
61
+ </Message.Item>
62
+ ) : null}
63
+ {selectedStructure ? (
64
+ <Message.Item>
65
+ {formatMessage({ id: "conceptRelations.structure" })}
66
+ {": "}
67
+ {selectedStructure.name}
68
+ </Message.Item>
69
+ ) : null}
70
+ {selectedTag ? (
71
+ <Message.Item>
72
+ {formatMessage({ id: "conceptRelations.tag" })}
73
+ {": "}
74
+ {formatMessage({
75
+ id: selectedTag.text,
76
+ defaultMessage: selectedTag.text,
77
+ })}
78
+ </Message.Item>
79
+ ) : null}
80
+ </Message.List>
81
+ </Message>
82
+ );
83
+ };
84
+
85
+ LinkedMessage.propTypes = {
86
+ selectedConcept: PropTypes.object,
87
+ selectedStructure: PropTypes.object,
88
+ selectedTag: PropTypes.object,
89
+ status: PropTypes.string,
90
+ };
91
+
92
+ export const ConceptsLinksManagement = ({
93
+ header,
94
+ subheader,
95
+ icon,
96
+ createRelation,
97
+ deleteRelation,
98
+ creatingRelation,
99
+ deletingRelation,
100
+ fetchConcepts,
101
+ conceptPayload,
102
+ tagOptions,
103
+ selectedRelationTags,
104
+ clearStructures,
105
+ clearStructureFilters,
106
+ fetchStructureFilters,
107
+ resetStructureFilters,
108
+ fetchStructures,
109
+ structurePayload,
110
+ }) => {
111
+ const { formatMessage } = useIntl();
112
+ const [selectedConcept, setSelectedConcept] = useState(null);
113
+ const [selectedStructure, setSelectedStructure] = useState(null);
114
+
115
+ const [selectedTag, setselectedTag] = useState(null);
116
+ const [status, setStatus] = useState(STATUS_PENDING);
117
+ const [links, setLinks] = useState([]);
118
+
119
+ const isStructureLinked = (selectedStructure, links) => {
120
+ return _.find(
121
+ (link) =>
122
+ _.propEq("resource_id", selectedStructure.id.toString())(link) &&
123
+ _.propEq("resource_type", "data_structure")(link)
124
+ )(links);
125
+ };
126
+
127
+ const handleConceptSelected = (selectedConcept) => {
128
+ setStatus(STATUS_PENDING);
129
+ setLinks(selectedConcept.links);
130
+ setSelectedStructure(null);
131
+ setSelectedConcept(selectedConcept);
132
+ clearStructures();
133
+ clearStructureFilters();
134
+ fetchStructureFilters();
135
+ resetStructureFilters();
136
+ fetchStructures({ ...structurePayload, size: 7, page: 0 });
137
+ fetchConcepts({
138
+ ...conceptPayload,
139
+ filters: { ...conceptPayload.filters, ...linksDefaultFilters },
140
+ size: 7,
141
+ });
142
+ };
143
+
144
+ const handleStructureSelected = (structure) => {
145
+ setSelectedStructure(structure);
146
+ if (selectedConcept && structure) {
147
+ const isLinked = isStructureLinked(structure, links);
148
+ if (isLinked) {
149
+ deleteRelation({ id: isLinked.id });
150
+ } else {
151
+ const structureLink = {
152
+ source_id: selectedConcept.business_concept_id,
153
+ source_type: "business_concept",
154
+ target_id: structure.id,
155
+ target_type: "data_structure",
156
+ tag_ids: selectedRelationTags ? selectedRelationTags : [],
157
+ };
158
+ createRelation(structureLink);
159
+ }
160
+ fetchConcepts({
161
+ ...conceptPayload,
162
+ filters: { ...conceptPayload.filters, ...linksDefaultFilters },
163
+ size: 7,
164
+ });
165
+ }
166
+ };
167
+
168
+ useEffect(() => {
169
+ if (status !== STATUS_PENDING) setSelectedStructure(null);
170
+
171
+ setStatus(STATUS_PENDING);
172
+ if (selectedRelationTags) {
173
+ const [id] = selectedRelationTags;
174
+ const tag = _.find(_.propEq("value", id))(tagOptions);
175
+ setselectedTag(tag);
176
+ }
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ }, [selectedRelationTags]);
179
+
180
+ useEffect(() => {
181
+ if (deletingRelation?.id) {
182
+ setLinks(_.filter(_.negate(_.propEq("id", deletingRelation.id)))(links));
183
+ setStatus(STATUS_DELETED);
184
+ }
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [deletingRelation]);
187
+
188
+ useEffect(() => {
189
+ if (creatingRelation) {
190
+ setLinks([
191
+ ...links,
192
+ {
193
+ id: creatingRelation.id.toString(),
194
+ resource_id: creatingRelation.target_id.toString(),
195
+ resource_type: "data_structure",
196
+ },
197
+ ]);
198
+ setStatus(STATUS_CREATED);
199
+ }
200
+ // eslint-disable-next-line react-hooks/exhaustive-deps
201
+ }, [creatingRelation]);
202
+
203
+ return (
204
+ <>
205
+ <Segment>
206
+ <Header as="h2">
207
+ <Icon circular name={icon} />
208
+ <Header.Content>
209
+ {header}
210
+ <Header.Subheader>{subheader}</Header.Subheader>
211
+ </Header.Content>
212
+ </Header>
213
+ <Segment attached="bottom">
214
+ <Grid>
215
+ <Grid.Row>
216
+ <Grid.Column width={8}>
217
+ {!_.isEmpty(tagOptions) && (
218
+ <TagTypeDropdownSelector options={tagOptions} />
219
+ )}
220
+ </Grid.Column>
221
+ <Grid.Column width={8}>
222
+ {selectedConcept || selectedTag ? (
223
+ <LinkedMessage
224
+ selectedStructure={selectedStructure}
225
+ selectedConcept={selectedConcept}
226
+ selectedTag={selectedTag}
227
+ status={status}
228
+ />
229
+ ) : null}
230
+ </Grid.Column>
231
+ </Grid.Row>
232
+ <Grid.Row>
233
+ <Grid.Column width={8}>
234
+ <Segment>
235
+ <Header as="h4">
236
+ <Header.Content>
237
+ {" "}
238
+ {formatMessage({ id: "conceptRelations.concepts" })}
239
+ </Header.Content>
240
+ </Header>
241
+ <ConceptSelector
242
+ showTitle={false}
243
+ selectedConcept={selectedConcept}
244
+ handleConceptSelected={handleConceptSelected}
245
+ tagOptions={tagOptions}
246
+ />
247
+ </Segment>
248
+ </Grid.Column>
249
+ <Grid.Column width={8}>
250
+ <Segment>
251
+ <Header as="h4">
252
+ <Header.Content>
253
+ {formatMessage({ id: "conceptRelations.structures" })}
254
+ </Header.Content>
255
+ </Header>
256
+ {selectedConcept ? (
257
+ <StructureSelector
258
+ selectedStructure={selectedStructure}
259
+ onSelect={handleStructureSelected}
260
+ links={links}
261
+ placeToTop={false}
262
+ />
263
+ ) : null}
264
+ </Segment>
265
+ </Grid.Column>
266
+ </Grid.Row>
267
+ </Grid>
268
+ </Segment>
269
+ </Segment>
270
+ </>
271
+ );
272
+ };
273
+
274
+ ConceptsLinksManagement.propTypes = {
275
+ header: PropTypes.string,
276
+ icon: PropTypes.string,
277
+ subheader: PropTypes.string,
278
+ fetchConcepts: PropTypes.func,
279
+ conceptPayload: PropTypes.object,
280
+ structurePayload: PropTypes.object,
281
+ fetchStructures: PropTypes.func,
282
+ clearStructures: PropTypes.func,
283
+ clearStructureFilters: PropTypes.func,
284
+ fetchStructureFilters: PropTypes.func,
285
+ resetStructureFilters: PropTypes.func,
286
+ createRelation: PropTypes.func,
287
+ deleteRelation: PropTypes.func,
288
+ selectedRelationTags: PropTypes.array,
289
+ tagOptions: PropTypes.array,
290
+ creatingRelation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
291
+ deletingRelation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
292
+ };
293
+
294
+ const makeMapStateToProps = () => {
295
+ const getTagOptions = makeTagOptionsSelector("data_field");
296
+ const searchQuerySelector = makeSearchQuerySelector(
297
+ "conceptQuery",
298
+ "conceptActiveFilters"
299
+ );
300
+ const structureSearchQuerySelector = makeSearchQuerySelector(
301
+ "structureQuery",
302
+ "structureActiveFilters",
303
+ "structureDateFilter"
304
+ );
305
+ const mapStateToProps = (state, props) => ({
306
+ conceptPayload: searchQuerySelector(state, props),
307
+ selectedRelationTags: state.selectedRelationTags,
308
+ structurePayload: structureSearchQuerySelector(state, props),
309
+ tagOptions: getTagOptions(state),
310
+ creatingRelation: state.creatingRelation,
311
+ deletingRelation: state.relations,
312
+ });
313
+
314
+ return mapStateToProps;
315
+ };
316
+
317
+ export default connect(makeMapStateToProps, {
318
+ fetchConcepts,
319
+ createRelation,
320
+ deleteRelation,
321
+ fetchStructures,
322
+ clearStructures,
323
+ clearStructureFilters,
324
+ fetchStructureFilters,
325
+ resetStructureFilters,
326
+ })(ConceptsLinksManagement);
@@ -0,0 +1,84 @@
1
+ import React, { Suspense } from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import { waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import ConceptsLinksManagement from "../ConceptsLinksManagement";
6
+ import en from "../../../messages/en";
7
+
8
+ const messages = {
9
+ en: {
10
+ ...en,
11
+ "concepts.status.undefined": "undefined",
12
+ "search.save_filters": "save filters",
13
+ "search.clear_filters": "clear filters",
14
+ "search.applied_filters": "apply filters",
15
+ "structures.not_found.body": "",
16
+ },
17
+ };
18
+
19
+ const link = {
20
+ id: 745,
21
+ resource_id: 4569480,
22
+ resource_type: "data_structure",
23
+ };
24
+
25
+ const concept = {
26
+ _actions: { can_create_structure_link: true },
27
+ id: 1,
28
+ business_concept_id: 1111,
29
+ name: "foo",
30
+ domain: { id: 1, name: "bar" },
31
+ status: "draft",
32
+ business_concept_id: 1,
33
+ links: [link],
34
+ };
35
+ const structure = {
36
+ id: 4569480,
37
+ name: "Contrato",
38
+ path: ["Trabajadores"],
39
+ system: {
40
+ external_id: "micro",
41
+ id: 68,
42
+ name: "System",
43
+ },
44
+ };
45
+
46
+ const props = {
47
+ header: "header",
48
+ subheader: "subheader",
49
+ icon: "linkify",
50
+ };
51
+
52
+ describe("<ConceptsLinksManagement />", () => {
53
+ const renderOpts = {
54
+ messages,
55
+ state: {
56
+ loading: false,
57
+ links: [link],
58
+ concepts: [concept],
59
+ structures: [structure],
60
+ structuresRows: [structure],
61
+ conceptActiveFilters: { filter: "some" },
62
+ },
63
+ };
64
+
65
+ it("matches the latest snapshot", () => {
66
+ const { container } = render(
67
+ <ConceptsLinksManagement {...props} />,
68
+ renderOpts
69
+ );
70
+ expect(container).toMatchSnapshot();
71
+ });
72
+
73
+ it("select a concept and shows LinkedMessage and structure search table", async () => {
74
+ const { queryByText } = render(
75
+ <Suspense fallback={null}>
76
+ <ConceptsLinksManagement {...props} />
77
+ </Suspense>,
78
+ renderOpts
79
+ );
80
+
81
+ userEvent.click(await queryByText(/foo/));
82
+ expect(queryByText("Adding a link to a structure")).toBeInTheDocument();
83
+ });
84
+ });
@@ -0,0 +1,225 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<ConceptsLinksManagement /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui segment"
7
+ >
8
+ <h2
9
+ class="ui header"
10
+ >
11
+ <i
12
+ aria-hidden="true"
13
+ class="linkify circular icon"
14
+ />
15
+ <div
16
+ class="content"
17
+ >
18
+ header
19
+ <div
20
+ class="sub header"
21
+ >
22
+ subheader
23
+ </div>
24
+ </div>
25
+ </h2>
26
+ <div
27
+ class="ui bottom attached segment"
28
+ >
29
+ <div
30
+ class="ui grid"
31
+ >
32
+ <div
33
+ class="row"
34
+ >
35
+ <div
36
+ class="eight wide column"
37
+ />
38
+ <div
39
+ class="eight wide column"
40
+ />
41
+ </div>
42
+ <div
43
+ class="row"
44
+ >
45
+ <div
46
+ class="eight wide column"
47
+ >
48
+ <div
49
+ class="ui segment"
50
+ >
51
+ <h4
52
+ class="ui header"
53
+ >
54
+ <div
55
+ class="content"
56
+ >
57
+
58
+ Concepts
59
+ </div>
60
+ </h4>
61
+ <div
62
+ class="ui segment"
63
+ >
64
+ <div
65
+ class="ui action left icon input"
66
+ >
67
+ <input
68
+ placeholder="Search concepts..."
69
+ type="text"
70
+ value=""
71
+ />
72
+ <i
73
+ aria-hidden="true"
74
+ class="search link icon"
75
+ />
76
+ <div
77
+ aria-expanded="false"
78
+ class="ui button floating labeled scrolling dropdown icon"
79
+ role="listbox"
80
+ tabindex="0"
81
+ >
82
+ <div
83
+ aria-atomic="true"
84
+ aria-live="polite"
85
+ class="divider text"
86
+ role="alert"
87
+ >
88
+ Filters
89
+ </div>
90
+ <i
91
+ aria-hidden="true"
92
+ class="filter icon"
93
+ />
94
+ <div
95
+ class="menu transition"
96
+ >
97
+ <div
98
+ class="item"
99
+ role="option"
100
+ >
101
+ <em>
102
+ (reset filters)
103
+ </em>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ <div
109
+ class="selectedFilters"
110
+ >
111
+ <div
112
+ class="appliedFilters"
113
+ >
114
+ apply filters
115
+ </div>
116
+ <div
117
+ aria-expanded="false"
118
+ class="ui floating item scrolling dropdown"
119
+ role="listbox"
120
+ tabindex="0"
121
+ >
122
+ <div
123
+ class="ui label"
124
+ >
125
+ filter
126
+ <i
127
+ aria-hidden="true"
128
+ class="delete icon"
129
+ />
130
+ </div>
131
+ <div
132
+ class="menu transition dimmable"
133
+ />
134
+ </div>
135
+ <a
136
+ class="resetFilters"
137
+ >
138
+ clear filters
139
+ </a>
140
+ <a
141
+ class="resetFilters"
142
+ >
143
+ save filters
144
+ </a>
145
+ </div>
146
+ <table
147
+ class="ui small selectable table"
148
+ >
149
+ <thead
150
+ class=""
151
+ >
152
+ <tr
153
+ class=""
154
+ >
155
+ <th
156
+ class=""
157
+ >
158
+ Term
159
+ </th>
160
+ <th
161
+ class=""
162
+ >
163
+ Domain
164
+ </th>
165
+ <th
166
+ class=""
167
+ >
168
+ Status
169
+ </th>
170
+ </tr>
171
+ </thead>
172
+ <tbody
173
+ class=""
174
+ >
175
+ <tr
176
+ class=""
177
+ >
178
+ <td
179
+ class=""
180
+ >
181
+ foo
182
+ </td>
183
+ <td
184
+ class=""
185
+ >
186
+ bar
187
+ </td>
188
+ <td
189
+ class=""
190
+ >
191
+ <div
192
+ class="ui olive label"
193
+ >
194
+ Draft
195
+ </div>
196
+ </td>
197
+ </tr>
198
+ </tbody>
199
+ </table>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ <div
204
+ class="eight wide column"
205
+ >
206
+ <div
207
+ class="ui segment"
208
+ >
209
+ <h4
210
+ class="ui header"
211
+ >
212
+ <div
213
+ class="content"
214
+ >
215
+ Structures
216
+ </div>
217
+ </h4>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ `;
@@ -26,7 +26,7 @@ const ConceptSelectorRow = ({ concept, active, onClick, disabled }) => {
26
26
  onClick={() => onClick && onClick(concept)}
27
27
  >
28
28
  <Table.Cell content={name} />
29
- <Table.Cell content={domain.name} />
29
+ <Table.Cell content={domain?.name} />
30
30
  <Table.Cell
31
31
  content={
32
32
  <Label color={mapStatusColor[status]}>
@@ -65,6 +65,15 @@ export const ConceptSelector = ({
65
65
  false
66
66
  )(concepts);
67
67
 
68
+ const isDisabled = (concept) => {
69
+ return concept
70
+ ? !_.flow(
71
+ _.pathOr({}, "_actions"),
72
+ _.propOr(true, "can_create_structure_link")
73
+ )(concept)
74
+ : false;
75
+ };
76
+
68
77
  return (
69
78
  <>
70
79
  {loadConcepts && (
@@ -98,7 +107,9 @@ export const ConceptSelector = ({
98
107
  <Table.Body>
99
108
  {concepts.map((s, i) => (
100
109
  <ConceptSelectorRow
101
- disabled={businessConceptId == s.business_concept_id}
110
+ disabled={
111
+ businessConceptId == s.business_concept_id || isDisabled(s)
112
+ }
102
113
  key={i}
103
114
  concept={s}
104
115
  active={selectedConcept && selectedConcept.id == s.id}
@@ -1,6 +1,25 @@
1
1
  import React from "react";
2
- import { shallow } from "enzyme";
2
+ import { render } from "@truedat/test/render";
3
+ import { waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
3
5
  import { ConceptSelector } from "../ConceptSelector";
6
+ import en from "../../../../messages/en";
7
+
8
+ const messages = {
9
+ en: {
10
+ ...en,
11
+ "concepts.status.undefined": "undefined",
12
+ "search.save_filters": "save filters",
13
+ "search.clear_filters": "clear filters",
14
+ "search.applied_filters": "apply filters",
15
+ },
16
+ };
17
+ const renderOpts = {
18
+ messages,
19
+ state: {
20
+ conceptActiveFilters: { filter: "some" },
21
+ },
22
+ };
4
23
 
5
24
  describe("<ConceptSelector />", () => {
6
25
  const handleConceptSelected = jest.fn();
@@ -8,46 +27,62 @@ describe("<ConceptSelector />", () => {
8
27
  id: 1,
9
28
  name: "foo",
10
29
  domain: { id: 1, name: "bar" },
11
- status: "draft"
30
+ status: "draft",
31
+ business_concept_id: 1,
12
32
  };
13
33
  const props = {
14
34
  conceptsLoading: false,
15
35
  concepts: [concept],
16
36
  selectedConcept: {},
17
- handleConceptSelected
37
+ handleConceptSelected,
38
+ defaultFilters: {},
18
39
  };
19
40
  it("matches the latest snapshot", () => {
20
- const wrapper = shallow(<ConceptSelector {...props} />);
21
- expect(wrapper).toMatchSnapshot();
41
+ const { container } = render(<ConceptSelector {...props} />, renderOpts);
42
+ expect(container).toMatchSnapshot();
22
43
  });
23
44
 
24
45
  it("renders title when specified", () => {
25
- const withTitle = shallow(<ConceptSelector {...props} />);
26
- const withoutTitle = shallow(
27
- <ConceptSelector {...{ ...props, showTitle: false }} />
46
+ const { queryByText } = render(<ConceptSelector {...props} />, renderOpts);
47
+ expect(queryByText("Related concept")).toBeInTheDocument();
48
+ });
49
+
50
+ it("renders no title when is not specified", () => {
51
+ const { queryByText } = render(
52
+ <ConceptSelector {...{ ...props, showTitle: false }} />,
53
+ renderOpts
28
54
  );
29
- expect(withTitle.find("label").length).toBe(1);
30
- expect(withoutTitle.find("label").length).toBe(0);
55
+ expect(queryByText("Related concept")).not.toBeInTheDocument();
31
56
  });
32
57
 
33
- it("renders Loader when specified", () => {
34
- const withLoader = shallow(<ConceptSelector {...props} />);
35
- const withoutLoader = shallow(
36
- <ConceptSelector {...{ ...props, loadConcepts: false }} />
58
+ it("calls handleConceptSelected on concept row click", async () => {
59
+ const { queryByText } = render(
60
+ <ConceptSelector {...{ ...props, selectedConcept: null }} />,
61
+ renderOpts
62
+ );
63
+ userEvent.click(await queryByText(/foo/));
64
+ await waitFor(() => {
65
+ expect(handleConceptSelected.mock.calls.length).toBe(1);
66
+ });
67
+ expect(handleConceptSelected).toHaveBeenCalledWith(
68
+ expect.objectContaining(concept)
37
69
  );
38
- expect(withLoader.find("Connect(ConceptsLoader)").length).toBe(1);
39
- expect(withoutLoader.find("Connect(ConceptsLoader)").length).toBe(0);
40
70
  });
41
71
 
42
- it("calls handleConceptSelected on concept row click", () => {
43
- const wrapper = shallow(
44
- <ConceptSelector {...{ ...props, selectedConcept: null }} />
72
+ it("renders disable when cannot create structure link", () => {
73
+ const conceptWithActions = {
74
+ id: 1,
75
+ name: "foo",
76
+ domain: { id: 1, name: "bar" },
77
+ status: "draft",
78
+ business_concept_id: 1,
79
+ _actions: { can_create_structure_link: false },
80
+ };
81
+
82
+ const { container } = render(
83
+ <ConceptSelector {...{ ...props, concepts: [conceptWithActions] }} />,
84
+ renderOpts
45
85
  );
46
- const ConceptSelectorRow = wrapper.find("ConceptSelectorRow");
47
- expect(ConceptSelectorRow.length).toBe(1);
48
- ConceptSelectorRow.dive()
49
- .find("TableRow")
50
- .simulate("click");
51
- expect(handleConceptSelected).toBeCalledWith(concept);
86
+ expect(container.querySelector("tbody > tr")).toHaveClass("disabled");
52
87
  });
53
88
  });
@@ -1,100 +1,168 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`<ConceptSelector /> matches the latest snapshot 1`] = `
4
- <Fragment>
5
- <Connect(ConceptsLoader)
6
- defaultFilters={
7
- Object {
8
- "current": Array [
9
- true,
10
- ],
11
- "status": Array [
12
- "pending_approval",
13
- "draft",
14
- "rejected",
15
- "published",
16
- ],
17
- }
18
- }
19
- pageSize={7}
20
- />
21
- <Connect(FiltersLoader) />
22
- <Connect(ConceptUserFiltersLoader) />
4
+ <div>
23
5
  <label>
24
- <MemoizedFormattedMessage
25
- id="conceptRelations.relatedConcept"
26
- />
6
+ Related concept
27
7
  </label>
28
- <Segment>
29
- <Connect(ConceptsSearch) />
30
- <ConceptSelectedFilters />
31
- <Table
32
- as="table"
33
- selectable={true}
34
- size="small"
8
+ <div
9
+ class="ui segment"
10
+ >
11
+ <div
12
+ class="ui action left icon input"
35
13
  >
36
- <TableHeader
37
- as="thead"
14
+ <input
15
+ placeholder="Search concepts..."
16
+ type="text"
17
+ value=""
18
+ />
19
+ <i
20
+ aria-hidden="true"
21
+ class="search link icon"
22
+ />
23
+ <div
24
+ aria-expanded="false"
25
+ class="ui button floating labeled scrolling dropdown icon"
26
+ role="listbox"
27
+ tabindex="0"
38
28
  >
39
- <TableRow
40
- as="tr"
41
- cellAs="td"
29
+ <div
30
+ aria-atomic="true"
31
+ aria-live="polite"
32
+ class="divider text"
33
+ role="alert"
42
34
  >
43
- <TableHeaderCell
44
- as="th"
45
- content={
46
- <Memo(MemoizedFormattedMessage)
47
- id="concepts.props.name"
48
- />
49
- }
35
+ Filters
36
+ </div>
37
+ <i
38
+ aria-hidden="true"
39
+ class="filter icon"
40
+ />
41
+ <div
42
+ class="menu transition"
43
+ >
44
+ <div
45
+ class="item"
46
+ role="option"
47
+ >
48
+ <em>
49
+ (reset filters)
50
+ </em>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ <div
56
+ class="selectedFilters"
57
+ >
58
+ <div
59
+ class="appliedFilters"
60
+ >
61
+ apply filters
62
+ </div>
63
+ <div
64
+ aria-expanded="false"
65
+ class="ui floating item scrolling dropdown"
66
+ role="listbox"
67
+ tabindex="0"
68
+ >
69
+ <div
70
+ class="ui label"
71
+ >
72
+ filter
73
+ <i
74
+ aria-hidden="true"
75
+ class="delete icon"
50
76
  />
51
- <TableHeaderCell
52
- as="th"
53
- content={
54
- <Memo(MemoizedFormattedMessage)
55
- id="concepts.props.domain"
56
- />
57
- }
77
+ </div>
78
+ <div
79
+ class="menu transition dimmable"
80
+ />
81
+ </div>
82
+ <a
83
+ class="resetFilters"
84
+ >
85
+ clear filters
86
+ </a>
87
+ <a
88
+ class="resetFilters"
89
+ >
90
+ save filters
91
+ </a>
92
+ </div>
93
+ <table
94
+ class="ui small selectable table"
95
+ >
96
+ <thead
97
+ class=""
98
+ >
99
+ <tr
100
+ class=""
101
+ >
102
+ <th
103
+ class=""
104
+ >
105
+ Term
106
+ </th>
107
+ <th
108
+ class=""
109
+ >
110
+ Domain
111
+ </th>
112
+ <th
113
+ class=""
114
+ >
115
+ Status
116
+ </th>
117
+ </tr>
118
+ </thead>
119
+ <tbody
120
+ class=""
121
+ >
122
+ <tr
123
+ class=""
124
+ >
125
+ <td
126
+ class=""
127
+ >
128
+ foo
129
+ </td>
130
+ <td
131
+ class=""
132
+ >
133
+ bar
134
+ </td>
135
+ <td
136
+ class=""
137
+ >
138
+ <div
139
+ class="ui olive label"
140
+ >
141
+ Draft
142
+ </div>
143
+ </td>
144
+ </tr>
145
+ <tr
146
+ class="active"
147
+ >
148
+ <td
149
+ class=""
58
150
  />
59
- <TableHeaderCell
60
- as="th"
61
- content={
62
- <Memo(MemoizedFormattedMessage)
63
- id="concepts.props.status"
64
- />
65
- }
151
+ <td
152
+ class=""
66
153
  />
67
- </TableRow>
68
- </TableHeader>
69
- <TableBody
70
- as="tbody"
71
- >
72
- <ConceptSelectorRow
73
- active={false}
74
- concept={
75
- Object {
76
- "domain": Object {
77
- "id": 1,
78
- "name": "bar",
79
- },
80
- "id": 1,
81
- "name": "foo",
82
- "status": "draft",
83
- }
84
- }
85
- disabled={true}
86
- key="0"
87
- onClick={[MockFunction]}
88
- />
89
- <ConceptSelectorRow
90
- active={true}
91
- concept={Object {}}
92
- />
93
- </TableBody>
94
- </Table>
95
- <Connect(Pagination)
96
- size="small"
97
- />
98
- </Segment>
99
- </Fragment>
154
+ <td
155
+ class=""
156
+ >
157
+ <div
158
+ class="ui label"
159
+ >
160
+ undefined
161
+ </div>
162
+ </td>
163
+ </tr>
164
+ </tbody>
165
+ </table>
166
+ </div>
167
+ </div>
100
168
  `;
@@ -43,9 +43,10 @@ export default {
43
43
  "concept.events.new_concept_draft":
44
44
  "created a new draft form a published concept",
45
45
  "concept.events.update_concept_draft": "updated the draft",
46
- "concept.events.relation_created": "added a link to a structure",
47
- "concept.events.relation_deleted": "removed a link to a structure",
48
- "concept.events.relation_deprecated": "deprecated link to a structure",
46
+ "concept.events.relation_creating": "Adding a link to a structure",
47
+ "concept.events.relation_created": "Added a link to a structure",
48
+ "concept.events.relation_deleted": "Removed a link to a structure",
49
+ "concept.events.relation_deprecated": "Deprecated link to a structure",
49
50
  "concept.props.in_progress": "In progress",
50
51
  "concept.props.last_update_at": "Update",
51
52
  "concept.props.last_update_by": "User",
@@ -89,6 +90,11 @@ export default {
89
90
  "This concept is not currently linked to any concept",
90
91
  "conceptRelations.relatedConcept": "Related concept",
91
92
  "conceptRelations.relationType": "Relation type",
93
+ "conceptRelations.concept": "Concept",
94
+ "conceptRelations.concepts": "Concepts",
95
+ "conceptRelations.structure": "Structure",
96
+ "conceptRelations.structures": "Structures",
97
+ "conceptRelations.tag": "Tag",
92
98
  "conceptRelations.tags": "Tags",
93
99
  "concepts.actions.bulk_update.popup":
94
100
  "You should select only one type in filters",
@@ -131,6 +137,7 @@ export default {
131
137
  "concepts.header.edit.bulk.concepts_count":
132
138
  'Selected concepts: "{concepts_count}"',
133
139
  "concepts.header.edit.bulk.info_message": "Only modify fields to be updated",
140
+ "concepts.header.linksManager": "Concepts link manager",
134
141
  "concepts.header.manage": "Concepts Management",
135
142
  "concepts.props.completeness": "Completeness",
136
143
  "concepts.props.description": "Description",
@@ -164,6 +171,7 @@ export default {
164
171
  "concepts.status.published": "Published",
165
172
  "concepts.status.rejected": "Rejected",
166
173
  "concepts.status.versioned": "Versioned",
174
+ "concepts.subheader.linksManager": "Query links business concepts",
167
175
  "concepts.subheader.manage": "Concept Versions in Progress",
168
176
  "concepts.subheader.view": "Query business concepts",
169
177
  "concepts.summary": "Summary",
@@ -45,9 +45,10 @@ export default {
45
45
  "concept.events.new_concept_draft":
46
46
  "creó un nuevo borrador a partir de la publicación existente",
47
47
  "concept.events.update_concept_draft": "actualizó el borrador",
48
- "concept.events.relation_created": "Nueva relación",
49
- "concept.events.relation_deleted": "Relación eliminada",
50
- "concept.events.relation_deprecated": "Relación deprecada",
48
+ "concept.events.relation_creating": "Creando un nuevo vínculo",
49
+ "concept.events.relation_created": "Nuevo vínculo creado",
50
+ "concept.events.relation_deleted": "Vínculo eliminado",
51
+ "concept.events.relation_deprecated": "Vínculo deprecado",
51
52
  "concept.props.in_progress": "Sin completar",
52
53
  "concept.props.last_update_at": "Modificación",
53
54
  "concept.props.last_update_by": "Usuario",
@@ -88,6 +89,11 @@ export default {
88
89
  "Este concepto no tiene conceptos relacionados actualmente",
89
90
  "conceptRelations.relatedConcept": "Concepto relacionado",
90
91
  "conceptRelations.relationType": "Tipo de relación",
92
+ "conceptRelations.concept": "Concepto",
93
+ "conceptRelations.concepts": "Conceptos",
94
+ "conceptRelations.structure": "Estructura",
95
+ "conceptRelations.structures": "Estructuras",
96
+ "conceptRelations.tag": "Etiqueta",
91
97
  "conceptRelations.tags": "Etiquetas",
92
98
  "concepts.actions.bulk_update.popup":
93
99
  "Seleccione un único tipo en los filtros",
@@ -130,6 +136,7 @@ export default {
130
136
  'Conceptos seleccionados: "{concepts_count}"',
131
137
  "concepts.header.edit.bulk.info_message":
132
138
  "Rellenar únicamente los campos que se quieran actualizar",
139
+ "concepts.header.linksManager": "Gestion de vínculos para conceptos",
133
140
  "concepts.header.manage": "Gestión de Conceptos",
134
141
  "concepts.props.completeness": "Completitud",
135
142
  "concepts.props.description": "Descripción",
@@ -164,6 +171,8 @@ export default {
164
171
  "concepts.status.published": "Publicado",
165
172
  "concepts.status.rejected": "Rechazado",
166
173
  "concepts.status.versioned": "Versionado",
174
+ "concepts.subheader.linksManager":
175
+ "Consultar vínculos de conceptos de negocio",
167
176
  "concepts.subheader.manage": "Versiones de Conceptos en Progreso",
168
177
  "concepts.subheader.view": "Consultar conceptos de negocio",
169
178
  "concepts.summary": "Resumen",