@truedat/bg 7.10.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/bg",
3
- "version": "7.10.2",
3
+ "version": "7.10.4",
4
4
  "description": "Truedat Web Business Glossary",
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.2",
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"
@@ -81,5 +81,5 @@
81
81
  "semantic-ui-react": "^3.0.0-beta.2",
82
82
  "swr": "^2.3.3"
83
83
  },
84
- "gitHead": "913c4f16345d35e38b0ed09c58d73b88955356f0"
84
+ "gitHead": "0dcedbfaed3964ee02a3d2c175e0f96e2fbc9316"
85
85
  }
@@ -20,8 +20,10 @@ const API_BUSINESS_CONCEPT_BULK_UPLOAD =
20
20
  "/api/business_concepts/bulk_upload_event";
21
21
  const API_BUSINESS_CONCEPT_SET_CONFIDENTIAL =
22
22
  "/api/business_concept_versions/:id/set_confidential";
23
+ const API_CONCEPT_SUGGESTIONS = "/api/business_concept_versions/suggestions"
23
24
 
24
25
  export {
26
+ API_CONCEPT_SUGGESTIONS,
25
27
  API_BUSINESS_CONCEPT_VERSION,
26
28
  API_BUSINESS_CONCEPT_VERSIONS_ACTIONS,
27
29
  API_BUSINESS_CONCEPT_VERSIONS_CSV,
@@ -0,0 +1,71 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useEffect } from "react";
3
+ import { Popup } from "semantic-ui-react";
4
+ import { useIntl } from "react-intl";
5
+ import { useParams } from "react-router";
6
+ import { useSelector } from "react-redux";
7
+ import PropTypes from "prop-types";
8
+ import { useConceptSuggestions } from "../hooks/useConcepts";
9
+ import { ConceptSelectorTable } from "../relations/components/ConceptSelector";
10
+
11
+ const SimilarityColumn = (similarity) => {
12
+ const { formatMessage } = useIntl();
13
+ return (
14
+ <Popup
15
+ content={formatMessage({ id: "suggestions.similarity.cosine.popup" })}
16
+ trigger={<span>{similarity.toFixed(3)}</span>}
17
+ />
18
+ );
19
+ };
20
+
21
+ const similarityColumnDefinition = {
22
+ name: "similarity",
23
+ fieldSelector: _.prop("similarity"),
24
+ fieldDecorator: SimilarityColumn,
25
+ width: 1,
26
+ };
27
+
28
+ export const ConceptSuggestions = ({
29
+ selectedConcept,
30
+ handleConceptSelected,
31
+ }) => {
32
+ const { id } = useParams();
33
+ const structureLinks = useSelector((state) => state.structureLinks);
34
+
35
+ const {
36
+ trigger: fetchSuggestions,
37
+ data,
38
+ } = useConceptSuggestions();
39
+
40
+ useEffect(() => {
41
+ if (id && structureLinks) {
42
+ fetchSuggestions({
43
+ resource: {
44
+ id,
45
+ type: "structures",
46
+ links: _.filter(
47
+ ({ resource_type }) => resource_type == "concept"
48
+ )(structureLinks),
49
+ },
50
+ });
51
+ }
52
+ }, [id, structureLinks]);
53
+
54
+ const concepts = data?.data?.data || [];
55
+
56
+ return (
57
+ <ConceptSelectorTable
58
+ concepts={concepts}
59
+ selectedConcept={selectedConcept}
60
+ handleConceptSelected={handleConceptSelected}
61
+ extraColumns={[similarityColumnDefinition]}
62
+ />
63
+ );
64
+ };
65
+
66
+ ConceptSuggestions.propTypes = {
67
+ handleSelectedStructure: PropTypes.func,
68
+ selectedStructure: PropTypes.object,
69
+ };
70
+
71
+ export default ConceptSuggestions;
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import { render, waitForLoad } from "@truedat/test/render";
3
+ import { useParams } from "react-router";
4
+ import { ConceptSuggestions } from "../ConceptSuggestions";
5
+ import { ConceptSelectorTable } from "../../relations/components/ConceptSelector";
6
+ import { useConceptSuggestions } from "../../hooks/useConcepts";
7
+
8
+ jest.mock("react-router", () => ({
9
+ ...jest.requireActual("react-router"),
10
+ useParams: jest.fn(),
11
+ }));
12
+
13
+ jest.mock("../../hooks/useConcepts", () => ({
14
+ useConceptSuggestions: jest.fn(),
15
+ }));
16
+
17
+ jest.mock("../../relations/components/ConceptSelector", () => {
18
+ return {
19
+ __esModule: true,
20
+ ConceptSelectorTable: jest.fn(() => <div>MockSearchResults</div>),
21
+ };
22
+ });
23
+
24
+ describe("ConceptSuggestions", () => {
25
+ const mockFetchSuggestions = jest.fn();
26
+
27
+ const structureLinks = [
28
+ { resource_type: "concept", id: "c1" },
29
+ { resource_type: "other", id: "not-c" }
30
+ ];
31
+
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+ useParams.mockReturnValue({ id: "123" });
35
+
36
+ useConceptSuggestions.mockReturnValue({
37
+ trigger: mockFetchSuggestions,
38
+ data: {
39
+ data: {
40
+ data: [
41
+ { id: "concept1", similarity: 0.89 },
42
+ { id: "concept2", similarity: 0.77 },
43
+ ],
44
+ },
45
+ },
46
+ isMutating: false,
47
+ });
48
+ });
49
+
50
+ const state = { structureLinks };
51
+
52
+ it("calls fetchSuggestions on mount with correct parameters", async () => {
53
+ const rendered = render(<ConceptSuggestions selectedConcept={null} handleConceptSelected={jest.fn()} />, { state });
54
+ await waitForLoad(rendered);
55
+ expect(mockFetchSuggestions).toHaveBeenCalledWith({
56
+ resource: {
57
+ id: "123",
58
+ type: "structures",
59
+ links: [{ resource_type: "concept", id: "c1" }],
60
+ },
61
+ });
62
+ });
63
+
64
+ it("renders ConceptSelectorTable with props", async () => {
65
+ const rendered = render(<ConceptSuggestions selectedConcept={{}} handleConceptSelected={jest.fn()} />, { state });
66
+ await waitForLoad(rendered);
67
+ expect(rendered.getByText("MockSearchResults")).toBeInTheDocument();
68
+ const props = ConceptSelectorTable.mock.calls[0][0];
69
+ expect(props.concepts).toEqual([
70
+ { id: "concept1", similarity: 0.89 },
71
+ { id: "concept2", similarity: 0.77 },
72
+ ]);
73
+ });
74
+ });
@@ -4,6 +4,7 @@ import FileSaver from "file-saver";
4
4
  import { apiJson, apiJsonPost, JSON_OPTS } from "@truedat/core/services/api";
5
5
 
6
6
  import {
7
+ API_CONCEPT_SUGGESTIONS,
7
8
  API_CONCEPT_FILTERS,
8
9
  API_CONCEPT_LINKS_DOWNLOAD,
9
10
  API_BUSINESS_CONCEPT_VERSIONS_SEARCH,
@@ -62,6 +63,12 @@ export const useConceptsUpload = () => {
62
63
  );
63
64
  };
64
65
 
66
+ export const useConceptSuggestions = () => {
67
+ return useSWRMutations(API_CONCEPT_SUGGESTIONS, (url, { arg }) => {
68
+ return apiJsonPost(url, arg);
69
+ });
70
+ };
71
+
65
72
  export const useConceptLinksUpload = () => {
66
73
  return useSWRMutations(API_CONCEPT_LINKS_UPLOAD, (url, { arg }) =>
67
74
  apiJsonPost(url, arg)
@@ -1,14 +1,7 @@
1
1
  import _ from "lodash/fp";
2
2
  import PropTypes from "prop-types";
3
3
  import { connect } from "react-redux";
4
- import {
5
- Button,
6
- Table,
7
- Message,
8
- Label,
9
- Icon,
10
- Segment,
11
- } from "semantic-ui-react";
4
+ import { Button, Table, Message, Label, Segment } from "semantic-ui-react";
12
5
  import { FormattedMessage, useIntl } from "react-intl";
13
6
  import SearchWidget from "@truedat/core/search/SearchWidget";
14
7
  import {
@@ -44,6 +37,7 @@ const ConceptSelectorRow = ({
44
37
  onClick,
45
38
  disabled,
46
39
  positive,
40
+ extraColumns = [],
47
41
  }) => {
48
42
  const { name, status, domain } = concept;
49
43
  return (
@@ -65,6 +59,12 @@ const ConceptSelectorRow = ({
65
59
  </Label>
66
60
  }
67
61
  />
62
+ {extraColumns.map((column, key) => (
63
+ <Table.Cell
64
+ key={key}
65
+ content={column.fieldDecorator(column.fieldSelector(concept))}
66
+ />
67
+ ))}
68
68
  </Table.Row>
69
69
  );
70
70
  };
@@ -77,20 +77,14 @@ ConceptSelectorRow.propTypes = {
77
77
  positive: PropTypes.bool,
78
78
  };
79
79
 
80
- export function ConceptSelectorContent({
80
+ export const ConceptSelectorTable = ({
81
81
  businessConceptId,
82
- handleConceptSelected,
82
+ concepts,
83
83
  selectedConcept,
84
- showTitle = true,
84
+ handleConceptSelected,
85
85
  links,
86
- }) {
87
- const { searchData, filterParams: searchParams } = useSearchContext();
88
- const { trigger: triggerDownload, isMutating: downloading } =
89
- useConceptLinksDownload();
90
- const { formatMessage } = useIntl();
91
- const concepts = searchData?.data;
92
- const conceptsActions = _.propOr({}, "_actions")(searchData);
93
-
86
+ extraColumns = [],
87
+ }) => {
94
88
  const selectedConceptIsFiltered = !_.reduce(
95
89
  (acc, c) => (selectedConcept && c.id == selectedConcept.id) || acc,
96
90
  false
@@ -104,7 +98,85 @@ export function ConceptSelectorContent({
104
98
  )(concept)
105
99
  : false;
106
100
  };
107
-
101
+
102
+ return !_.isEmpty(concepts) || selectedConcept ? (
103
+ <Table selectable size="small">
104
+ <Table.Header>
105
+ <Table.Row>
106
+ <Table.HeaderCell
107
+ content={<FormattedMessage id="concepts.props.name" />}
108
+ />
109
+ <Table.HeaderCell
110
+ content={<FormattedMessage id="concepts.props.domain" />}
111
+ />
112
+ <Table.HeaderCell
113
+ content={<FormattedMessage id="concepts.props.status" />}
114
+ />
115
+ {extraColumns.map((column, key) => (
116
+ <Table.HeaderCell
117
+ key={key}
118
+ content={
119
+ <FormattedMessage id={`concepts.props.${column.name}`} />
120
+ }
121
+ />
122
+ ))}
123
+ </Table.Row>
124
+ </Table.Header>
125
+ <Table.Body>
126
+ {concepts.map((s, i) => (
127
+ <ConceptSelectorRow
128
+ disabled={
129
+ businessConceptId == s.business_concept_id || isDisabled(s)
130
+ }
131
+ key={i}
132
+ concept={s}
133
+ active={selectedConcept && selectedConcept.id == s.id}
134
+ onClick={handleConceptSelected}
135
+ positive={isPositive(
136
+ links,
137
+ s.business_concept_id,
138
+ businessConceptId
139
+ )}
140
+ extraColumns={extraColumns}
141
+ />
142
+ ))}
143
+ {selectedConcept && selectedConceptIsFiltered && (
144
+ <ConceptSelectorRow
145
+ concept={selectedConcept}
146
+ extraColumns={extraColumns}
147
+ active
148
+ />
149
+ )}
150
+ </Table.Body>
151
+ </Table>
152
+ ) : (
153
+ <Message header={<FormattedMessage id="concepts.search.results.empty" />} />
154
+ );
155
+ };
156
+
157
+ ConceptSelectorTable.propTypes = {
158
+ businessConceptId: PropTypes.number,
159
+ concepts: PropTypes.array,
160
+ selectedConcept: PropTypes.object,
161
+ handleConceptSelected: PropTypes.func,
162
+ links: PropTypes.array,
163
+ extraColumns: PropTypes.array,
164
+ };
165
+
166
+ export function ConceptSelectorContent({
167
+ businessConceptId,
168
+ handleConceptSelected,
169
+ selectedConcept,
170
+ showTitle = true,
171
+ links,
172
+ }) {
173
+ const { formatMessage } = useIntl();
174
+ const { searchData, filterParams: searchParams } = useSearchContext();
175
+ const { trigger: triggerDownload, isMutating: downloading } =
176
+ useConceptLinksDownload();
177
+ const concepts = searchData?.data;
178
+ const conceptsActions = _.propOr({}, "_actions")(searchData);
179
+
108
180
  return (
109
181
  <>
110
182
  {showTitle && (
@@ -113,64 +185,34 @@ export function ConceptSelectorContent({
113
185
  </label>
114
186
  )}
115
187
  <Segment>
116
- {conceptsActions?.downloadLinks && (
117
- <>
118
- <Button
119
- icon="download"
120
- floated="right"
121
- secondary
122
- loading={downloading}
123
- onClick={() => triggerDownload(searchParams)}
124
- data-tooltip={formatMessage({
125
- id: "concepts.actions.downloadLinks.tooltip",
126
- })}
127
- />
188
+ {conceptsActions?.downloadLinks && (
189
+ <>
190
+ <Button
191
+ icon="download"
192
+ floated="right"
193
+ secondary
194
+ loading={downloading}
195
+ onClick={() => triggerDownload(searchParams)}
196
+ data-tooltip={formatMessage({
197
+ id: "concepts.actions.downloadLinks.tooltip",
198
+ })}
199
+ />
128
200
  <ConceptLinksUploadButton />
129
- </>
201
+ </>
130
202
  )}
131
203
  <SearchWidget />
132
- {!_.isEmpty(concepts) || selectedConcept ? (
133
- <Table selectable size="small">
134
- <Table.Header>
135
- <Table.Row>
136
- <Table.HeaderCell
137
- content={<FormattedMessage id="concepts.props.name" />}
138
- />
139
- <Table.HeaderCell
140
- content={<FormattedMessage id="concepts.props.domain" />}
141
- />
142
- <Table.HeaderCell
143
- content={<FormattedMessage id="concepts.props.status" />}
144
- />
145
- </Table.Row>
146
- </Table.Header>
147
- <Table.Body>
148
- {concepts.map((s, i) => (
149
- <ConceptSelectorRow
150
- disabled={
151
- businessConceptId == s.business_concept_id || isDisabled(s)
152
- }
153
- key={i}
154
- concept={s}
155
- active={selectedConcept && selectedConcept.id == s.id}
156
- onClick={handleConceptSelected}
157
- positive={isPositive(
158
- links,
159
- s.business_concept_id,
160
- businessConceptId
161
- )}
162
- />
163
- ))}
164
- {selectedConcept && selectedConceptIsFiltered && (
165
- <ConceptSelectorRow concept={selectedConcept} active />
166
- )}
167
- </Table.Body>
168
- </Table>
169
- ) : (
170
- <Message
171
- header={<FormattedMessage id="concepts.search.results.empty" />}
172
- />
173
- )}
204
+ <ConceptSelectorTable
205
+ concepts={concepts}
206
+ showTitle={showTitle}
207
+ downloading={downloading}
208
+ selectedConcept={selectedConcept}
209
+ handleConceptSelected={handleConceptSelected}
210
+ links={links}
211
+ businessConceptId={businessConceptId}
212
+ triggerDownload={triggerDownload}
213
+ downloadLinks={conceptsActions?.downloadLinks}
214
+ searchParams={searchParams}
215
+ />
174
216
  <ConceptsPagination size="small" />
175
217
  </Segment>
176
218
  </>
@@ -1,7 +1,8 @@
1
- import { waitFor } from "@testing-library/react";
1
+ import { waitFor, within } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { render, waitForLoad } from "@truedat/test/render";
4
4
  import ConceptSelector from "../ConceptSelector";
5
+ import { ConceptSelectorTable } from "../ConceptSelector";
5
6
 
6
7
  const concept = {
7
8
  id: 1,
@@ -116,3 +117,91 @@ describe("<ConceptSelector />", () => {
116
117
  );
117
118
  });
118
119
  });
120
+
121
+ describe("<ConceptSelectorTable />", () => {
122
+ jest.mock("react-intl", () => ({
123
+ FormattedMessage: ({ id }) => <span data-testid="fm">{id}</span>,
124
+ }));
125
+
126
+ const concept = {
127
+ id: 1,
128
+ name: "foo",
129
+ domain: { id: 1, name: "bar" },
130
+ status: "draft",
131
+ business_concept_id: 1,
132
+ last_change_at: "2020-01-01T00:00:00.000Z",
133
+ similarity: 0.5,
134
+ };
135
+
136
+ const links = [
137
+ {
138
+ target_id: 1,
139
+ target_type: "business_concept",
140
+ },
141
+ ];
142
+
143
+ const selectedConcept = { id: 2 };
144
+
145
+ const extraColumns = [
146
+ {
147
+ name: "similarity",
148
+ fieldSelector: (concept) => concept.similarity,
149
+ fieldDecorator: (similarity) => (
150
+ <span data-testid="similarity">{similarity}</span>
151
+ ),
152
+ },
153
+ ];
154
+
155
+ const props = {
156
+ businessConceptId: 1,
157
+ concepts: [concept],
158
+ selectedConcept,
159
+ handleConceptSelected: jest.fn(),
160
+ extraColumns,
161
+ links,
162
+ };
163
+
164
+ beforeEach(() => {
165
+ jest.clearAllMocks();
166
+ });
167
+
168
+ it("matches the latest snapshot", async () => {
169
+ const rendered = render(<ConceptSelectorTable {...props} />);
170
+ await waitForLoad(rendered);
171
+ expect(rendered.container).toMatchSnapshot();
172
+ });
173
+
174
+ test("renders Message when there are no concepts and no selectedConcept", () => {
175
+ const rendered = render(
176
+ <ConceptSelectorTable
177
+ {...{ ...props, concepts: [], selectedConcept: null }}
178
+ />
179
+ );
180
+ // It should render the Message with the empty results header id
181
+ expect(
182
+ rendered.getByText("concepts.search.results.empty")
183
+ ).toBeInTheDocument();
184
+ });
185
+
186
+ test("renders Table when there are concepts", async () => {
187
+ const rendered = render(
188
+ <ConceptSelectorTable {...{ ...props, selectedConcept: { id: 1 } }} />
189
+ );
190
+ await waitForLoad(rendered);
191
+
192
+ // Header labels via FormattedMessage
193
+ expect(rendered.getByText("concepts.props.name")).toBeInTheDocument();
194
+ expect(rendered.getByText("concepts.props.domain")).toBeInTheDocument();
195
+ expect(rendered.getByText("concepts.props.status")).toBeInTheDocument();
196
+ expect(rendered.getByText("concepts.props.similarity")).toBeInTheDocument();
197
+ // Rows exist
198
+ const rowgroups = rendered.getAllByRole("rowgroup");
199
+ const tbody = rowgroups[1];
200
+ const rows = within(tbody).getAllByRole("row");
201
+ expect(rows).toHaveLength(1);
202
+
203
+ expect(rows[0]).toHaveTextContent("bar");
204
+ expect(rows[0]).toHaveTextContent("draft");
205
+ expect(rows[0]).toHaveTextContent("0.5");
206
+ });
207
+ });
@@ -201,3 +201,102 @@ exports[`<ConceptSelector /> matches the latest snapshot 1`] = `
201
201
  </div>
202
202
  </div>
203
203
  `;
204
+
205
+ exports[`<ConceptSelectorTable /> matches the latest snapshot 1`] = `
206
+ <div>
207
+ <table
208
+ class="ui small selectable table"
209
+ >
210
+ <thead
211
+ class=""
212
+ >
213
+ <tr
214
+ class=""
215
+ >
216
+ <th
217
+ class=""
218
+ >
219
+ concepts.props.name
220
+ </th>
221
+ <th
222
+ class=""
223
+ >
224
+ concepts.props.domain
225
+ </th>
226
+ <th
227
+ class=""
228
+ >
229
+ concepts.props.status
230
+ </th>
231
+ <th
232
+ class=""
233
+ >
234
+ concepts.props.similarity
235
+ </th>
236
+ </tr>
237
+ </thead>
238
+ <tbody
239
+ class=""
240
+ >
241
+ <tr
242
+ class="disabled"
243
+ >
244
+ <td
245
+ class=""
246
+ >
247
+ foo
248
+ </td>
249
+ <td
250
+ class=""
251
+ >
252
+ bar
253
+ </td>
254
+ <td
255
+ class=""
256
+ >
257
+ <div
258
+ class="ui olive label"
259
+ >
260
+ draft
261
+ </div>
262
+ </td>
263
+ <td
264
+ class=""
265
+ >
266
+ <span
267
+ data-testid="similarity"
268
+ >
269
+ 0.5
270
+ </span>
271
+ </td>
272
+ </tr>
273
+ <tr
274
+ class="active"
275
+ >
276
+ <td
277
+ class=""
278
+ />
279
+ <td
280
+ class=""
281
+ />
282
+ <td
283
+ class=""
284
+ >
285
+ <div
286
+ class="ui label"
287
+ >
288
+ concepts.status.undefined
289
+ </div>
290
+ </td>
291
+ <td
292
+ class=""
293
+ >
294
+ <span
295
+ data-testid="similarity"
296
+ />
297
+ </td>
298
+ </tr>
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+ `;
@@ -13,6 +13,7 @@ export function* linkConceptSaga({ payload }) {
13
13
  "target_id",
14
14
  "target_type",
15
15
  "tag_ids",
16
+ "origin",
16
17
  ])(payload);
17
18
 
18
19
  const redirectUrl = _.prop("redirectUrl")(payload);