@truedat/bg 6.0.2 → 6.0.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.
Files changed (27) hide show
  1. package/package.json +6 -6
  2. package/src/concepts/api.js +3 -0
  3. package/src/concepts/components/ConceptDetails.js +1 -3
  4. package/src/concepts/components/ConceptEdit.js +124 -110
  5. package/src/concepts/components/ConceptForm.js +154 -123
  6. package/src/concepts/components/ConceptRoutes.js +9 -0
  7. package/src/concepts/components/ConceptsActions.js +4 -1
  8. package/src/concepts/components/ConceptsUpdateButton.js +3 -2
  9. package/src/concepts/components/ConceptsUploadButton.js +21 -2
  10. package/src/concepts/components/ConceptsUploadEvents.js +24 -0
  11. package/src/concepts/components/ConceptsUploadEventsTable.js +166 -0
  12. package/src/concepts/components/__tests__/ConceptForm.spec.js +23 -1
  13. package/src/concepts/components/__tests__/ConceptsActions.spec.js +1 -0
  14. package/src/concepts/components/__tests__/ConceptsUploadButton.spec.js +35 -13
  15. package/src/concepts/components/__tests__/ConceptsUploadEventsTable.spec.js +98 -0
  16. package/src/concepts/components/__tests__/__snapshots__/ConceptsUploadButton.spec.js.snap +75 -24
  17. package/src/concepts/components/__tests__/__snapshots__/ConceptsUploadEventsTable.spec.js.snap +115 -0
  18. package/src/concepts/hooks/useUploadEvents.js +11 -0
  19. package/src/concepts/reducers/uploadConceptsFile.js +0 -1
  20. package/src/concepts/selectors/__tests__/getConceptUploadEventColumns.spec.js +20 -0
  21. package/src/concepts/selectors/getConceptUploadEventColumns.js +72 -0
  22. package/src/concepts/selectors/index.js +4 -0
  23. package/src/concepts/styles/conceptsUploadEventColumns.less +24 -0
  24. package/src/messages/en.js +45 -9
  25. package/src/messages/es.js +46 -9
  26. package/src/reducers/__tests__/bgMessage.spec.js +12 -9
  27. package/src/reducers/bgMessage.js +12 -9
@@ -10,6 +10,7 @@ import {
10
10
  CONCEPT_LINKS_MANAGEMENT,
11
11
  CONCEPT_VERSION,
12
12
  CONCEPTS_BULK_UPDATE,
13
+ CONCEPTS_BULK_UPLOAD_EVENTS,
13
14
  CONCEPTS_NEW,
14
15
  CONCEPTS_PENDING,
15
16
  CONCEPTS_DEPRECATED,
@@ -29,6 +30,7 @@ import Concepts from "./Concepts";
29
30
  import ConceptsBulkUpdate from "./ConceptsBulkUpdate";
30
31
  import ConceptsLoader from "./ConceptsLoader";
31
32
  import ConceptSubscriptionLoader from "./ConceptSubscriptionLoader";
33
+ import ConceptsUploadEvents from "./ConceptsUploadEvents";
32
34
 
33
35
  const RelationTagsLoader = React.lazy(() =>
34
36
  import("@truedat/lm/components/RelationTagsLoader")
@@ -56,6 +58,13 @@ export const ConceptRoutes = ({ concept, conceptLoaded, templatesLoaded }) => {
56
58
  };
57
59
  return (
58
60
  <>
61
+ <Route
62
+ exact
63
+ path={CONCEPTS_BULK_UPLOAD_EVENTS}
64
+ render={() =>
65
+ authorized ? <ConceptsUploadEvents /> : <Unauthorized />
66
+ }
67
+ />
59
68
  <Route
60
69
  exact
61
70
  path={CONCEPT_LINKS_MANAGEMENT}
@@ -18,6 +18,7 @@ export const ConceptsActions = ({
18
18
  hidden,
19
19
  upload,
20
20
  update,
21
+ canAutoPublish,
21
22
  }) => {
22
23
  const { formatMessage, locale } = useIntl();
23
24
 
@@ -42,7 +43,7 @@ export const ConceptsActions = ({
42
43
  id: "concepts.actions.download.tooltip",
43
44
  })}
44
45
  />
45
- {upload ? <ConceptsUploadButton /> : null}
46
+ {upload ? <ConceptsUploadButton canAutoPublish={canAutoPublish} /> : null}
46
47
  {update ? <ConceptsUpdateButton /> : null}
47
48
  </div>
48
49
  );
@@ -56,6 +57,7 @@ ConceptsActions.propTypes = {
56
57
  downloadConcepts: PropTypes.func,
57
58
  upload: PropTypes.bool,
58
59
  update: PropTypes.bool,
60
+ canAutoPublish: PropTypes.bool,
59
61
  };
60
62
 
61
63
  ConceptsActions.defaultProps = {
@@ -71,6 +73,7 @@ const mapStateToProps = ({
71
73
  _.isEmpty(conceptActions) && conceptsActions?.create ? CONCEPTS_NEW : null,
72
74
  conceptsDownloading: conceptsDownloading,
73
75
  hidden: _.isEmpty(conceptsActions),
76
+ canAutoPublish: conceptsActions?.autoPublish && true,
74
77
  });
75
78
 
76
79
  export default connect(mapStateToProps, { downloadConcepts, uploadConcepts })(
@@ -58,8 +58,9 @@ const mapStateToProps = ({
58
58
  updateUrl:
59
59
  conceptCount !== 0 &&
60
60
  !conceptsLoading &&
61
- existingTemplate(conceptFilters, conceptActiveFilters) &&
62
- CONCEPTS_BULK_UPDATE,
61
+ existingTemplate(conceptFilters, conceptActiveFilters)
62
+ ? CONCEPTS_BULK_UPDATE
63
+ : null,
63
64
  });
64
65
 
65
66
  export default connect(mapStateToProps)(ConceptsUpdateButton);
@@ -11,11 +11,29 @@ const uploadAction = {
11
11
  method: "POST",
12
12
  href: API_BUSINESS_CONCEPT_VERSIONS_UPLOAD,
13
13
  };
14
-
15
- export const ConceptsUploadButton = ({ uploadConcepts, loading }) => {
14
+ export const ConceptsUploadButton = ({
15
+ uploadConcepts,
16
+ loading,
17
+ canAutoPublish,
18
+ }) => {
16
19
  const { formatMessage, locale } = useIntl();
20
+ const extraAction = {
21
+ key: "yesWithAutoPublish",
22
+ primary: true,
23
+ content: formatMessage({ id: "uploadModal.accept.publish" }),
24
+ onClick: (data) => {
25
+ data.append("auto_publish", canAutoPublish);
26
+ return uploadConcepts({
27
+ action: "upload",
28
+ data,
29
+ lang: locale,
30
+ ...uploadAction,
31
+ });
32
+ },
33
+ };
17
34
  return (
18
35
  <UploadModal
36
+ extraAction={canAutoPublish && extraAction}
19
37
  icon="upload"
20
38
  trigger={
21
39
  <Button
@@ -50,6 +68,7 @@ export const ConceptsUploadButton = ({ uploadConcepts, loading }) => {
50
68
  ConceptsUploadButton.propTypes = {
51
69
  uploadConcepts: PropTypes.func,
52
70
  loading: PropTypes.bool,
71
+ canAutoPublish: PropTypes.bool,
53
72
  };
54
73
 
55
74
  const mapStateToProps = ({ uploadConceptsFile: { loading } }) => ({
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+
3
+ import { Header, Icon, Segment } from "semantic-ui-react";
4
+ import { FormattedMessage } from "react-intl";
5
+ import ConceptsUploadEventsTable from "./ConceptsUploadEventsTable";
6
+
7
+ export const ConceptsUploadEvents = () => (
8
+ <Segment>
9
+ <Header as="h2">
10
+ <Icon circular name="cogs" />
11
+ <Header.Content>
12
+ <FormattedMessage id="concepts.upload.header" />
13
+ <Header.Subheader>
14
+ <FormattedMessage id="concepts.upload.subheader" />
15
+ </Header.Subheader>
16
+ </Header.Content>
17
+ </Header>
18
+ <Segment attached="bottom">
19
+ <ConceptsUploadEventsTable />
20
+ </Segment>
21
+ </Segment>
22
+ );
23
+
24
+ export default ConceptsUploadEvents;
@@ -0,0 +1,166 @@
1
+ import React, { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { connect } from "react-redux";
4
+
5
+ import _ from "lodash/fp";
6
+
7
+ import { FormattedMessage } from "react-intl";
8
+ import { Table } from "semantic-ui-react";
9
+ import { useIntl } from "react-intl";
10
+ import { columnDecorator } from "@truedat/core/services";
11
+
12
+ import { useUploadEvents } from "../hooks/useUploadEvents";
13
+ import { getConceptUploadEventColumns } from "../selectors";
14
+
15
+ import "../styles/conceptsUploadEventColumns.less";
16
+
17
+ const getColumnsWithData = (conceptsResults, columns) =>
18
+ _.filter((column) =>
19
+ _.any(_.flow(columnDecorator(column), _.negate(_.isEmpty)))(conceptsResults)
20
+ )(columns);
21
+
22
+ const messagesImp = (data) => {
23
+ return _.has("errors")(data)
24
+ ? _.flow(
25
+ _.propOr([], "errors"),
26
+ _.map((error) => ({
27
+ id: error.body.message,
28
+ context: {
29
+ ...error.body.context,
30
+ },
31
+ defaultMessage: "concepts.upload.failed.success.errors",
32
+ }))
33
+ )(data)
34
+ : [];
35
+ };
36
+
37
+ const ConceptsUploadEventsTable = ({ getColumns }) => {
38
+ const { formatMessage } = useIntl();
39
+ const [eventExpandedIndex, setEventExpandedIndex] = useState(null);
40
+
41
+ const { data, loading } = useUploadEvents();
42
+
43
+ const events =
44
+ data && !loading
45
+ ? _.reduce(
46
+ (acc, event) => ({
47
+ ...acc,
48
+ [`${event.task_reference}_${event.status}`]: {
49
+ ...event,
50
+ response: {
51
+ ...event.response,
52
+ translatedHeader: !event.response
53
+ ? ""
54
+ : formatMessage(
55
+ {
56
+ id: _.isEmpty(event.response.errors)
57
+ ? "concepts.upload.success.header"
58
+ : "concepts.upload.success.header_with_errors",
59
+ },
60
+ {
61
+ count_created: event.response.created.length,
62
+ count_updated: event.response.updated.length,
63
+ count_errors: event.response.errors.length,
64
+ }
65
+ ),
66
+ translatedErrors: _.map((error) =>
67
+ formatMessage({ id: error.id }, error.context)
68
+ )(messagesImp(event.response)),
69
+ },
70
+ expanded: false,
71
+ },
72
+ }),
73
+ {},
74
+ data.data
75
+ )
76
+ : [];
77
+
78
+ const columns = getColumnsWithData(events, getColumns());
79
+
80
+ const toggleExpanded = (eventKey) =>
81
+ eventExpandedIndex === eventKey
82
+ ? setEventExpandedIndex(null)
83
+ : setEventExpandedIndex(eventKey);
84
+
85
+ return (
86
+ <>
87
+ {!_.isEmpty(events) && (
88
+ <Table sortable selectable>
89
+ <Table.Header>
90
+ <Table.Row>
91
+ {columns.map((column, i) => {
92
+ return (
93
+ <Table.HeaderCell
94
+ key={i}
95
+ width={column.width}
96
+ content={formatMessage({
97
+ id: `concepts.upload.props.${column.name}`,
98
+ defaultMessage: column.name,
99
+ })}
100
+ />
101
+ );
102
+ })}
103
+ </Table.Row>
104
+ </Table.Header>
105
+ <Table.Body>
106
+ {_.map(
107
+ (eventKey) => (
108
+ <Table.Row
109
+ className={
110
+ eventKey === eventExpandedIndex ? "expanded" : "contracted"
111
+ }
112
+ key={eventKey}
113
+ onClick={() => toggleExpanded(eventKey)}
114
+ >
115
+ {eventKey !== eventExpandedIndex ? (
116
+ columns &&
117
+ columns.map((column, key) => (
118
+ <Table.Cell
119
+ key={key}
120
+ textAlign={column.textAlign}
121
+ content={columnDecorator(column)({
122
+ ...events[eventKey],
123
+ formatMessage,
124
+ })}
125
+ />
126
+ ))
127
+ ) : (
128
+ <Table.Cell colSpan={columns.length}>
129
+ {events[eventKey].file_hash ? (
130
+ <>
131
+ <FormattedMessage
132
+ id={`concepts.upload.props.file_hash`}
133
+ />
134
+ :&nbsp;{events[eventKey].file_hash}
135
+ </>
136
+ ) : null}
137
+ {columns.map((column, key) => (
138
+ <div key={key}>
139
+ {columnDecorator(column)({
140
+ ...events[eventKey],
141
+ formatMessage,
142
+ })}
143
+ </div>
144
+ ))}
145
+ </Table.Cell>
146
+ )}
147
+ </Table.Row>
148
+ ),
149
+ Object.keys(events)
150
+ )}
151
+ </Table.Body>
152
+ </Table>
153
+ )}
154
+ </>
155
+ );
156
+ };
157
+
158
+ ConceptsUploadEventsTable.propTypes = {
159
+ getColumns: PropTypes.func,
160
+ };
161
+
162
+ const mapStateToProps = (state) => ({
163
+ getColumns: () => getConceptUploadEventColumns(state),
164
+ });
165
+
166
+ export default connect(mapStateToProps)(ConceptsUploadEventsTable);
@@ -9,6 +9,27 @@ import {
9
9
  } from "@truedat/test/mocks";
10
10
  import ConceptForm from "../ConceptForm";
11
11
 
12
+ jest.mock("@truedat/ai/hooks/useSuggestions", () => {
13
+ const originalModule = jest.requireActual("@truedat/ai/hooks/useSuggestions");
14
+
15
+ return {
16
+ __esModule: true,
17
+ ...originalModule,
18
+ useAvailabilityCheck: () => ({
19
+ trigger: jest.fn(
20
+ () =>
21
+ new Promise(() => ({
22
+ data: {
23
+ data: {
24
+ status: "ok",
25
+ },
26
+ },
27
+ }))
28
+ ),
29
+ }),
30
+ };
31
+ });
32
+
12
33
  const state = {
13
34
  conceptActions: { create: {} },
14
35
  conceptActionLoading: "",
@@ -39,13 +60,14 @@ describe("<ConceptForm />", () => {
39
60
  });
40
61
 
41
62
  it("renders dynamic fields when template is selected", async () => {
42
- const { findByText, getByText, queryByText } = render(
63
+ const { findByText, getByText, queryByText, getAllByRole } = render(
43
64
  <ConceptForm />,
44
65
  renderOpts
45
66
  );
46
67
  await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument());
47
68
  await waitFor(() => expect(queryByText(/fooDomain/)).toBeInTheDocument());
48
69
  userEvent.click(await findByText("fooDomain"));
70
+ userEvent.type(getAllByRole("textbox")[1], "name");
49
71
  userEvent.click(await findByText("template1"));
50
72
  expect(getByText("field1")).toBeInTheDocument();
51
73
  });
@@ -16,6 +16,7 @@ describe("<ConceptsActions />", () => {
16
16
  en: {
17
17
  "concepts.actions.create": "Create",
18
18
  "concepts.actions.download.tooltip": "Download",
19
+ "concepts.actions.upload.tooltip": "concepts.actions.upload.tooltip",
19
20
  },
20
21
  },
21
22
  state: {
@@ -1,23 +1,45 @@
1
1
  // only admin user
2
2
  import React from "react";
3
- import { intl } from "@truedat/test/intl-stub";
4
- import { shallow } from "enzyme";
5
- import { ConceptsUploadButton } from "../ConceptsUploadButton";
3
+ import { render } from "@truedat/test/render";
4
+ import { screen, waitFor } from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
6
 
7
- // workaround for enzyme issue with React.useContext
8
- // see https://github.com/airbnb/enzyme/issues/2176#issuecomment-532361526
9
- jest.spyOn(React, "useContext").mockImplementation(() => intl);
7
+ import { ConceptsUploadButton } from "../ConceptsUploadButton";
10
8
 
11
9
  jest.mock("@truedat/core/hooks", () => ({
12
- useAuthorized: jest.fn(() => true)
10
+ useAuthorized: jest.fn(() => true),
13
11
  }));
14
12
 
13
+ const props = {
14
+ uploadConcepts: jest.fn(),
15
+ loading: false,
16
+ canAutoPublish: false,
17
+ };
18
+
15
19
  describe("<ConceptsUploadButton />", () => {
16
- it("matches the latest snapshot", () => {
17
- const uploadConcepts = jest.fn();
18
- const loading = false;
19
- const props = { uploadConcepts, loading };
20
- const wrapper = shallow(<ConceptsUploadButton {...props} />);
21
- expect(wrapper).toMatchSnapshot();
20
+ it("matches the latest snapshot", async () => {
21
+ const { getByRole } = render(<ConceptsUploadButton {...props} />);
22
+ userEvent.click(getByRole("button"));
23
+ await waitFor(() =>
24
+ expect(screen.getByRole("presentation")).toBeInTheDocument()
25
+ );
26
+
27
+ expect(screen.getByRole("presentation")).toMatchSnapshot();
28
+ });
29
+
30
+ it("autopublish button is displayed if canAutoPublish is true", async () => {
31
+ const customProps = {
32
+ ...props,
33
+ canAutoPublish: true,
34
+ };
35
+ const { getByRole } = render(<ConceptsUploadButton {...customProps} />);
36
+ userEvent.click(getByRole("button"));
37
+ await waitFor(() =>
38
+ expect(screen.getByRole("presentation")).toBeInTheDocument()
39
+ );
40
+
41
+ expect(
42
+ screen.getByRole("button", { name: /publish/i })
43
+ ).toBeInTheDocument();
22
44
  });
23
45
  });
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { render } from "@truedat/test/render";
4
+ import ConceptsUploadEventsTable from "../ConceptsUploadEventsTable";
5
+
6
+ const events = [
7
+ {
8
+ file_hash: "99E400B91D164F8F0A39F400CA323C8F",
9
+ inserted_at: "2022-04-23T15:53:24.638484Z",
10
+ message: null,
11
+ response: {
12
+ errors: [],
13
+ created: [4825521, 4825561, 4825593],
14
+ updated: [4825522, 4825562, 4825594],
15
+ },
16
+ status: "COMPLETED",
17
+ task_reference: "0.3225053307.1609564164.100549",
18
+ user_id: 467,
19
+ },
20
+ {
21
+ file_hash: "47D90FDF1AD967BD7DBBDAE28664278E",
22
+ inserted_at: "2022-04-23T15:14:48.770275Z",
23
+ message: null,
24
+ response: {
25
+ errors: [
26
+ {
27
+ error_type: "test_field_error",
28
+ body: {
29
+ message: "concepts.upload.failed.invalid_field_value",
30
+ context: {
31
+ field: "gdpr",
32
+ type: "grpd_template",
33
+ row: 4,
34
+ error: "Missing value",
35
+ },
36
+ },
37
+ },
38
+ {
39
+ error_type: "test_forbidden_update",
40
+ body: {
41
+ message: "concepts.upload.failed.forbidden_update",
42
+ context: {
43
+ type: "test_template",
44
+ row: 2,
45
+ domain: "forbidden_domain",
46
+ },
47
+ },
48
+ },
49
+ {
50
+ error_type: "test_name_not_available",
51
+ body: {
52
+ message: "concepts.upload.failed.name_not_available",
53
+ context: {
54
+ type: "test_template",
55
+ row: 7,
56
+ name: "test_name_not_available",
57
+ },
58
+ },
59
+ },
60
+ ],
61
+ created: [],
62
+ updated: [],
63
+ },
64
+ status: "COMPLETED",
65
+ task_reference: "0.3225053307.1609564162.101463",
66
+ user_id: 467,
67
+ },
68
+ ];
69
+
70
+ jest.mock("../../hooks/useUploadEvents.js", () => {
71
+ const originalModule = jest.requireActual("../../hooks/useUploadEvents.js");
72
+
73
+ return {
74
+ __esModule: true,
75
+ ...originalModule,
76
+ useUploadEvents: jest.fn(() => ({
77
+ data: {
78
+ data: events,
79
+ },
80
+ loading: false,
81
+ })),
82
+ };
83
+ });
84
+
85
+ describe("<ConceptsUploadEventsTable />", () => {
86
+ it("matches the latest snapshot", () => {
87
+ const { container } = render(<ConceptsUploadEventsTable />);
88
+ expect(container).toMatchSnapshot();
89
+ });
90
+
91
+ it("toggles row horizontal view when row is clicked", () => {
92
+ const { getAllByRole } = render(<ConceptsUploadEventsTable />);
93
+ const dataRow = getAllByRole("row")[1];
94
+ expect(dataRow).toHaveClass("contracted");
95
+ userEvent.click(dataRow);
96
+ expect(dataRow).toHaveClass("expanded");
97
+ });
98
+ });
@@ -1,29 +1,80 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`<ConceptsUploadButton /> matches the latest snapshot 1`] = `
4
- <UploadModal
5
- content={
6
- <Memo(MemoizedFormattedMessage)
7
- id="concepts.actions.upload.confirmation.content"
8
- />
9
- }
10
- handleSubmit={[Function]}
11
- header={
12
- <Memo(MemoizedFormattedMessage)
13
- id="concepts.actions.upload.confirmation.header"
14
- />
15
- }
4
+ <div
5
+ class="ui small modal transition visible active"
16
6
  icon="upload"
17
- param="business_concepts"
18
- trigger={
19
- <Button
20
- as="button"
21
- data-tooltip="concepts.actions.upload.tooltip"
22
- floated="right"
23
- icon="upload"
24
- loading={false}
25
- secondary={true}
26
- />
27
- }
28
- />
7
+ role="presentation"
8
+ >
9
+ <i
10
+ aria-hidden="true"
11
+ class="close icon"
12
+ />
13
+ <div
14
+ class="header"
15
+ >
16
+ Confirm bulk upload
17
+ </div>
18
+ <div
19
+ class="content"
20
+ >
21
+ <div
22
+ aria-disabled="false"
23
+ class=""
24
+ data-testid="fileDropZone"
25
+ style="position: relative; width: 100%; margin-bottom: 10px; border: 1px dashed gray; border-radius: 5px;"
26
+ >
27
+ <div
28
+ style="padding: 20px; width: 100%; height: 100%;"
29
+ >
30
+ <div
31
+ class="ui center aligned container"
32
+ >
33
+ <i
34
+ aria-hidden="true"
35
+ class="cloud upload huge icon"
36
+ />
37
+ <p>
38
+ Drag an drop file or click to select file
39
+ </p>
40
+ <button
41
+ class="ui secondary button"
42
+ >
43
+ <i
44
+ aria-hidden="true"
45
+ class="upload icon"
46
+ />
47
+ Upload file
48
+ </button>
49
+ </div>
50
+ </div>
51
+ <input
52
+ autocomplete="off"
53
+ multiple=""
54
+ style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; opacity: 0.00001; pointer-events: none;"
55
+ type="file"
56
+ />
57
+ </div>
58
+ <h2>
59
+ Selected file
60
+ </h2>
61
+ <ul />
62
+ </div>
63
+ <div
64
+ class="actions"
65
+ >
66
+ <button
67
+ class="ui secondary button"
68
+ >
69
+ Cancel
70
+ </button>
71
+ <button
72
+ class="ui primary disabled button"
73
+ disabled=""
74
+ tabindex="-1"
75
+ >
76
+ Upload and update
77
+ </button>
78
+ </div>
79
+ </div>
29
80
  `;