@truedat/bg 5.8.1 → 5.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/bg",
3
- "version": "5.8.1",
3
+ "version": "5.8.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.8.1",
37
+ "@truedat/test": "5.8.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,9 +86,9 @@
86
86
  ]
87
87
  },
88
88
  "dependencies": {
89
- "@truedat/core": "5.8.1",
90
- "@truedat/df": "5.8.1",
91
- "@truedat/lm": "5.8.1",
89
+ "@truedat/core": "5.8.3",
90
+ "@truedat/df": "5.8.3",
91
+ "@truedat/lm": "5.8.3",
92
92
  "decode-uri-component": "^0.2.2",
93
93
  "file-saver": "^2.0.5",
94
94
  "moment": "^2.29.4",
@@ -111,5 +111,5 @@
111
111
  "react-dom": ">= 16.8.6 < 17",
112
112
  "semantic-ui-react": ">= 2.0.3 < 2.2"
113
113
  },
114
- "gitHead": "aacafb9e014ac8d1193a5dc2a37c92ea36c3d816"
114
+ "gitHead": "36771348dbfbdb680f5694454d7549bca927cbaa"
115
115
  }
@@ -0,0 +1,55 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Form, Dropdown } from "semantic-ui-react";
5
+ import { FormattedMessage, useIntl } from "react-intl";
6
+
7
+ const formatOption = (formatMessage) => (options) =>
8
+ _.flow(
9
+ _.map(({ value, text }) =>
10
+ _.isNil(text) || text == ""
11
+ ? { value, text }
12
+ : {
13
+ value,
14
+ text: formatMessage({
15
+ id: `conceptRelations.${text}`,
16
+ defaultMessage: formatMessage({ id: text, defaultMessage: text }),
17
+ }),
18
+ }
19
+ )
20
+ )(options);
21
+
22
+ export const ConceptLinksDropdownSelector = ({
23
+ defaultOption,
24
+ options,
25
+ handleChange,
26
+ }) => {
27
+ const { formatMessage } = useIntl();
28
+
29
+ return (
30
+ <Form.Field>
31
+ <label>
32
+ <b>
33
+ <FormattedMessage id="conceptRelations.linkType" />
34
+ </b>
35
+ </label>
36
+ <div>
37
+ <Dropdown
38
+ className="concept-link-dropdown"
39
+ value={defaultOption}
40
+ selection
41
+ options={formatOption(formatMessage)(options)}
42
+ onChange={(_e, { value }) => handleChange(value)}
43
+ />
44
+ </div>
45
+ </Form.Field>
46
+ );
47
+ };
48
+
49
+ ConceptLinksDropdownSelector.propTypes = {
50
+ setSelectedLinkType: PropTypes.func,
51
+ options: PropTypes.array,
52
+ defaultOption: PropTypes.string,
53
+ };
54
+
55
+ export default ConceptLinksDropdownSelector;
@@ -18,6 +18,12 @@ import {
18
18
  import { createRelation, deleteRelation } from "@truedat/core/routines";
19
19
  import ConceptSelector from "../relations/components/ConceptSelector";
20
20
  import { fetchConcepts } from "../routines";
21
+ import { pickConceptAttrs } from "../relations/components/ConceptRelationForm";
22
+ import ConceptLinksDropdownSelector from "./ConceptLinksDropdownSelector";
23
+ import ConceptUserFiltersLoader from "./ConceptUserFiltersLoader";
24
+ const RelationsLoader = React.lazy(() =>
25
+ import("@truedat/lm/components/RelationsLoader")
26
+ );
21
27
 
22
28
  const TagTypeDropdownSelector = React.lazy(() =>
23
29
  import("@truedat/lm/components/TagTypeDropdownSelector")
@@ -32,12 +38,19 @@ const linksDefaultFilters = {
32
38
  status: ["pending_approval", "draft", "rejected", "published"],
33
39
  };
34
40
 
41
+ const linkOptions = [
42
+ { value: "structures", text: "structures" },
43
+ { value: "concepts", text: "concepts" },
44
+ ];
45
+
35
46
  const STATUS_PENDING = "pending";
36
47
  const STATUS_CREATED = "created";
37
48
  const STATUS_DELETED = "deleted";
38
49
 
39
50
  const LinkedMessage = ({
40
- selectedConcept,
51
+ selectedSourceConcept,
52
+ selectedTargetConcept,
53
+ selectedLinkType,
41
54
  selectedStructure,
42
55
  selectedTag,
43
56
  status,
@@ -54,27 +67,41 @@ const LinkedMessage = ({
54
67
  <Message success={status !== STATUS_PENDING} floating>
55
68
  <Message.Header>{messagesMap[status]}</Message.Header>
56
69
  <Message.List>
57
- {selectedConcept ? (
70
+ {selectedSourceConcept ? (
58
71
  <Message.Item>
59
- {formatMessage({ id: "conceptRelations.concept" })}
60
- {": "} {selectedConcept.name}
72
+ {formatMessage({ id: "conceptRelations.from" })}
73
+ {": "} {selectedSourceConcept.name}
61
74
  </Message.Item>
62
75
  ) : null}
63
76
  {selectedStructure ? (
64
77
  <Message.Item>
65
- {formatMessage({ id: "conceptRelations.structure" })}
78
+ {formatMessage({ id: "conceptRelations.to" })}
66
79
  {": "}
67
80
  {selectedStructure.name}
68
81
  </Message.Item>
82
+ ) : selectedTargetConcept ? (
83
+ <Message.Item>
84
+ {formatMessage({ id: "conceptRelations.to" })}
85
+ {": "}
86
+ {selectedTargetConcept.name}
87
+ </Message.Item>
69
88
  ) : null}
70
89
  {selectedTag ? (
71
90
  <Message.Item>
72
91
  {formatMessage({ id: "conceptRelations.tag" })}
73
92
  {": "}
74
- {formatMessage({
75
- id: selectedTag.text,
76
- defaultMessage: selectedTag.text,
77
- })}
93
+ {selectedLinkType === "structures"
94
+ ? formatMessage({
95
+ id: selectedTag.text,
96
+ defaultMessage: selectedTag.text,
97
+ })
98
+ : formatMessage({
99
+ id: `source.${selectedTag.text}`,
100
+ defaultMessage: formatMessage({
101
+ id: selectedTag.text,
102
+ defaultMessage: selectedTag.text,
103
+ }),
104
+ })}
78
105
  </Message.Item>
79
106
  ) : null}
80
107
  </Message.List>
@@ -83,8 +110,10 @@ const LinkedMessage = ({
83
110
  };
84
111
 
85
112
  LinkedMessage.propTypes = {
86
- selectedConcept: PropTypes.object,
113
+ selectedSourceConcept: PropTypes.object,
87
114
  selectedStructure: PropTypes.object,
115
+ selectedTargetConcept: PropTypes.object,
116
+ selectedLinkType: PropTypes.string,
88
117
  selectedTag: PropTypes.object,
89
118
  status: PropTypes.string,
90
119
  };
@@ -95,11 +124,11 @@ export const ConceptsLinksManagement = ({
95
124
  icon,
96
125
  createRelation,
97
126
  deleteRelation,
98
- creatingRelation,
99
- deletingRelation,
127
+ conceptLinks,
128
+ tagStructureOptions,
129
+ tagConceptOptions,
100
130
  fetchConcepts,
101
131
  conceptPayload,
102
- tagOptions,
103
132
  selectedRelationTags,
104
133
  clearStructures,
105
134
  clearStructureFilters,
@@ -109,97 +138,128 @@ export const ConceptsLinksManagement = ({
109
138
  structurePayload,
110
139
  }) => {
111
140
  const { formatMessage } = useIntl();
112
- const [selectedConcept, setSelectedConcept] = useState(null);
141
+ const [selectedLinkType, setSelectedLinkType] = useState("structures");
142
+ const [selectedSourceConcept, setSelectedSourceConcept] = useState(null);
143
+ const [selectedTargetConcept, setSelectedTargetConcept] = useState(null);
113
144
  const [selectedStructure, setSelectedStructure] = useState(null);
114
145
 
146
+ const [tagOptions, setTagOptions] = useState(tagStructureOptions);
115
147
  const [selectedTag, setselectedTag] = useState(null);
148
+
116
149
  const [status, setStatus] = useState(STATUS_PENDING);
117
- const [links, setLinks] = useState([]);
150
+ const [links, setLinks] = useState(null);
151
+
152
+ useEffect(() => {
153
+ if (selectedLinkType == "concepts") {
154
+ setTagOptions(tagConceptOptions);
155
+ setSelectedStructure(null);
156
+ } else if (selectedLinkType == "structures") {
157
+ setSelectedTargetConcept(null);
158
+ const filters = conceptPayload.filters;
159
+ const payload = {
160
+ ...conceptPayload,
161
+ filters: { ...filters, ...linksDefaultFilters },
162
+ size: 7,
163
+ };
164
+ fetchConcepts(payload);
165
+ setTagOptions(tagStructureOptions);
166
+ }
167
+ // eslint-disable-next-line react-hooks/exhaustive-deps
168
+ }, [selectedLinkType, tagStructureOptions]);
118
169
 
119
- const isStructureLinked = (selectedStructure, links) => {
170
+ useEffect(() => {
171
+ if (status !== STATUS_PENDING) setSelectedStructure(null);
172
+
173
+ setStatus(STATUS_PENDING);
174
+ if (selectedRelationTags) {
175
+ const [id] = selectedRelationTags;
176
+ const tag = _.find(_.propEq("value", id))(tagOptions);
177
+ setselectedTag(tag);
178
+ }
179
+ // eslint-disable-next-line react-hooks/exhaustive-deps
180
+ }, [selectedRelationTags]);
181
+
182
+ useEffect(() => {
183
+ setLinks(conceptLinks);
184
+ }, [conceptLinks]);
185
+
186
+ const isLinked = (conceptLinks, id, targetType) => {
120
187
  return _.find(
121
188
  (link) =>
122
- _.propEq("resource_id", selectedStructure.id.toString())(link) &&
123
- _.propEq("resource_type", "data_structure")(link)
124
- )(links);
189
+ _.propEq("target_id", id)(link) &&
190
+ _.propEq("target_type", targetType)(link)
191
+ )(conceptLinks);
125
192
  };
126
193
 
127
- const handleConceptSelected = (selectedConcept) => {
194
+ const handleSourceConceptSelected = (selectedSourceConcept) => {
128
195
  setStatus(STATUS_PENDING);
129
- setLinks(selectedConcept.links);
196
+ setSelectedSourceConcept(selectedSourceConcept);
130
197
  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
- });
198
+ setSelectedTargetConcept(null);
199
+
200
+ if (selectedLinkType == "structures") {
201
+ clearStructures();
202
+ clearStructureFilters();
203
+ fetchStructureFilters();
204
+ resetStructureFilters();
205
+ fetchStructures({ ...structurePayload, size: 7, page: 0 });
206
+ }
207
+ };
208
+
209
+ const handleTargetConceptSelected = (concept) => {
210
+ setSelectedTargetConcept(concept);
211
+ if (selectedSourceConcept && concept) {
212
+ const isConceptLinked = isLinked(
213
+ conceptLinks,
214
+ concept.business_concept_id,
215
+ "business_concept"
216
+ );
217
+ if (isConceptLinked) {
218
+ setStatus(STATUS_DELETED);
219
+ deleteRelation({ id: isConceptLinked.id });
220
+ } else {
221
+ const conceptLink = {
222
+ source_id: selectedSourceConcept.business_concept_id,
223
+ source_type: "business_concept",
224
+ target_id: concept.business_concept_id,
225
+ target_type: "business_concept",
226
+ tag_ids: selectedRelationTags ? selectedRelationTags : [],
227
+ context: {
228
+ target: pickConceptAttrs(concept),
229
+ source: pickConceptAttrs(selectedSourceConcept),
230
+ },
231
+ };
232
+ setStatus(STATUS_CREATED);
233
+ createRelation(conceptLink);
234
+ }
235
+ }
142
236
  };
143
237
 
144
238
  const handleStructureSelected = (structure) => {
145
239
  setSelectedStructure(structure);
146
- if (selectedConcept && structure) {
147
- const isLinked = isStructureLinked(structure, links);
148
- if (isLinked) {
149
- deleteRelation({ id: isLinked.id });
240
+ if (selectedSourceConcept && structure) {
241
+ const isStructureLinked = isLinked(
242
+ conceptLinks,
243
+ structure.id,
244
+ "data_structure"
245
+ );
246
+ if (isStructureLinked) {
247
+ setStatus(STATUS_DELETED);
248
+ deleteRelation({ id: isStructureLinked.id });
150
249
  } else {
151
250
  const structureLink = {
152
- source_id: selectedConcept.business_concept_id,
251
+ source_id: selectedSourceConcept.business_concept_id,
153
252
  source_type: "business_concept",
154
253
  target_id: structure.id,
155
254
  target_type: "data_structure",
156
255
  tag_ids: selectedRelationTags ? selectedRelationTags : [],
157
256
  };
257
+ setStatus(STATUS_CREATED);
158
258
  createRelation(structureLink);
159
259
  }
160
- fetchConcepts({
161
- ...conceptPayload,
162
- filters: { ...conceptPayload.filters, ...linksDefaultFilters },
163
- size: 7,
164
- });
165
260
  }
166
261
  };
167
262
 
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
263
  return (
204
264
  <>
205
265
  <Segment>
@@ -213,16 +273,28 @@ export const ConceptsLinksManagement = ({
213
273
  <Segment attached="bottom">
214
274
  <Grid>
215
275
  <Grid.Row>
216
- <Grid.Column width={8}>
276
+ <Grid.Column width={4}>
277
+ {
278
+ <ConceptLinksDropdownSelector
279
+ handleChange={setSelectedLinkType}
280
+ defaultOption={selectedLinkType}
281
+ options={linkOptions}
282
+ />
283
+ }
284
+ </Grid.Column>
285
+ <Grid.Column width={4}>
217
286
  {!_.isEmpty(tagOptions) && (
218
287
  <TagTypeDropdownSelector options={tagOptions} />
219
288
  )}
220
289
  </Grid.Column>
290
+
221
291
  <Grid.Column width={8}>
222
- {selectedConcept || selectedTag ? (
292
+ {selectedSourceConcept || selectedTag ? (
223
293
  <LinkedMessage
224
294
  selectedStructure={selectedStructure}
225
- selectedConcept={selectedConcept}
295
+ selectedTargetConcept={selectedTargetConcept}
296
+ selectedLinkType={selectedLinkType}
297
+ selectedSourceConcept={selectedSourceConcept}
226
298
  selectedTag={selectedTag}
227
299
  status={status}
228
300
  />
@@ -240,8 +312,8 @@ export const ConceptsLinksManagement = ({
240
312
  </Header>
241
313
  <ConceptSelector
242
314
  showTitle={false}
243
- selectedConcept={selectedConcept}
244
- handleConceptSelected={handleConceptSelected}
315
+ selectedConcept={selectedSourceConcept}
316
+ handleConceptSelected={handleSourceConceptSelected}
245
317
  tagOptions={tagOptions}
246
318
  />
247
319
  </Segment>
@@ -250,16 +322,47 @@ export const ConceptsLinksManagement = ({
250
322
  <Segment>
251
323
  <Header as="h4">
252
324
  <Header.Content>
253
- {formatMessage({ id: "conceptRelations.structures" })}
325
+ {formatMessage({
326
+ id:
327
+ selectedLinkType == "structures"
328
+ ? "conceptRelations.structures"
329
+ : "conceptRelations.concepts",
330
+ })}
254
331
  </Header.Content>
255
332
  </Header>
256
- {selectedConcept ? (
257
- <StructureSelector
258
- selectedStructure={selectedStructure}
259
- onSelect={handleStructureSelected}
260
- links={links}
261
- placeToTop={false}
262
- />
333
+ {selectedSourceConcept && selectedLinkType == "structures" ? (
334
+ <>
335
+ <ConceptUserFiltersLoader />
336
+ <RelationsLoader
337
+ resource_id={selectedSourceConcept.business_concept_id}
338
+ resource_type="business_concept"
339
+ target_type="data_structure"
340
+ />
341
+ <StructureSelector
342
+ selectedStructure={selectedStructure}
343
+ onSelect={handleStructureSelected}
344
+ links={conceptLinks}
345
+ placeToTop={false}
346
+ />
347
+ </>
348
+ ) : selectedSourceConcept &&
349
+ selectedLinkType == "concepts" ? (
350
+ <>
351
+ <RelationsLoader
352
+ resource_id={selectedSourceConcept.business_concept_id}
353
+ resource_type="business_concept"
354
+ target_type="business_concept"
355
+ />
356
+ <ConceptSelector
357
+ selectedConcept={selectedTargetConcept}
358
+ handleConceptSelected={handleTargetConceptSelected}
359
+ businessConceptId={
360
+ selectedSourceConcept.business_concept_id
361
+ }
362
+ links={links}
363
+ showTitle={false}
364
+ />
365
+ </>
263
366
  ) : null}
264
367
  </Segment>
265
368
  </Grid.Column>
@@ -286,17 +389,18 @@ ConceptsLinksManagement.propTypes = {
286
389
  createRelation: PropTypes.func,
287
390
  deleteRelation: PropTypes.func,
288
391
  selectedRelationTags: PropTypes.array,
289
- tagOptions: PropTypes.array,
290
- creatingRelation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
291
- deletingRelation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
392
+ tagStructureOptions: PropTypes.array,
393
+ tagConceptOptions: PropTypes.array,
394
+ conceptLinks: PropTypes.object,
292
395
  };
293
396
 
294
397
  const makeMapStateToProps = () => {
295
- const getTagOptions = makeTagOptionsSelector("data_field");
398
+ const getTagStructureOptions = makeTagOptionsSelector("data_field");
296
399
  const searchQuerySelector = makeSearchQuerySelector(
297
400
  "conceptQuery",
298
401
  "conceptActiveFilters"
299
402
  );
403
+ const getTagConceptOptions = makeTagOptionsSelector("business_concept");
300
404
  const structureSearchQuerySelector = makeSearchQuerySelector(
301
405
  "structureQuery",
302
406
  "structureActiveFilters",
@@ -306,9 +410,9 @@ const makeMapStateToProps = () => {
306
410
  conceptPayload: searchQuerySelector(state, props),
307
411
  selectedRelationTags: state.selectedRelationTags,
308
412
  structurePayload: structureSearchQuerySelector(state, props),
309
- tagOptions: getTagOptions(state),
310
- creatingRelation: state.creatingRelation,
311
- deletingRelation: state.relations,
413
+ tagStructureOptions: getTagStructureOptions(state),
414
+ tagConceptOptions: getTagConceptOptions(state),
415
+ conceptLinks: state.relations,
312
416
  });
313
417
 
314
418
  return mapStateToProps;
@@ -1,6 +1,5 @@
1
1
  import React, { Suspense } from "react";
2
2
  import { render } from "@truedat/test/render";
3
- import { waitFor } from "@testing-library/react";
4
3
  import userEvent from "@testing-library/user-event";
5
4
  import ConceptsLinksManagement from "../ConceptsLinksManagement";
6
5
  import en from "../../../messages/en";
@@ -33,7 +33,71 @@ exports[`<ConceptsLinksManagement /> matches the latest snapshot 1`] = `
33
33
  class="row"
34
34
  >
35
35
  <div
36
- class="eight wide column"
36
+ class="four wide column"
37
+ >
38
+ <div
39
+ class="field"
40
+ >
41
+ <label>
42
+ <b>
43
+ Link type
44
+ </b>
45
+ </label>
46
+ <div>
47
+ <div
48
+ aria-expanded="false"
49
+ class="ui selection dropdown concept-link-dropdown"
50
+ role="listbox"
51
+ tabindex="0"
52
+ >
53
+ <div
54
+ aria-atomic="true"
55
+ aria-live="polite"
56
+ class="divider text"
57
+ role="alert"
58
+ >
59
+ Structures
60
+ </div>
61
+ <i
62
+ aria-hidden="true"
63
+ class="dropdown icon"
64
+ />
65
+ <div
66
+ class="menu transition"
67
+ >
68
+ <div
69
+ aria-checked="true"
70
+ aria-selected="true"
71
+ class="active selected item"
72
+ role="option"
73
+ style="pointer-events: all;"
74
+ >
75
+ <span
76
+ class="text"
77
+ >
78
+ Structures
79
+ </span>
80
+ </div>
81
+ <div
82
+ aria-checked="false"
83
+ aria-selected="false"
84
+ class="item"
85
+ role="option"
86
+ style="pointer-events: all;"
87
+ >
88
+ <span
89
+ class="text"
90
+ >
91
+ Concepts
92
+ </span>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ <div
100
+ class="four wide column"
37
101
  />
38
102
  <div
39
103
  class="eight wide column"