@truedat/dq 5.1.1 → 5.2.0

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.0] 2023-02-22
4
+
5
+ ### Added
6
+
7
+ - [TD-5471] Implementations CSV upload publish button
8
+
3
9
  ## [5.0.6] 2023-01-30
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "description": "Truedat Web Data Quality Module",
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.0.5",
37
+ "@truedat/test": "5.2.0",
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",
@@ -92,8 +92,8 @@
92
92
  },
93
93
  "dependencies": {
94
94
  "@apollo/client": "^3.7.1",
95
- "@truedat/core": "5.0.5",
96
- "@truedat/df": "5.0.5",
95
+ "@truedat/core": "5.2.0",
96
+ "@truedat/df": "5.2.0",
97
97
  "decode-uri-component": "^0.2.2",
98
98
  "graphql": "^15.5.3",
99
99
  "moment": "^2.29.4",
@@ -118,5 +118,5 @@
118
118
  "react-dom": ">= 16.8.6 < 17",
119
119
  "semantic-ui-react": ">= 2.0.3 < 2.2"
120
120
  },
121
- "gitHead": "28731e6c6de05ad746edb43d5a23f99c6e0a5779"
121
+ "gitHead": "20710c8ea4c857547cdaeac20dfada1bdf64e9d2"
122
122
  }
@@ -1,3 +1,4 @@
1
+ import _ from "lodash/fp";
1
2
  import React from "react";
2
3
  import PropTypes from "prop-types";
3
4
  import { connect } from "react-redux";
@@ -13,13 +14,29 @@ const uploadAction = {
13
14
  };
14
15
 
15
16
  export const ImplementationsUploadButton = ({
17
+ canAutoPublish,
16
18
  uploadImplementations,
17
19
  loading,
18
20
  }) => {
19
21
  const { formatMessage } = useIntl();
22
+ const extraAction = {
23
+ key: "yesWithAutoPublish",
24
+ primary: true,
25
+ content: formatMessage({ id: "uploadModal.accept.publish" }),
26
+ onClick: (formData) => {
27
+ // UploadModal is a Modal, not a Form, but sends FormData
28
+ formData.append("auto_publish", true);
29
+ return uploadImplementations({
30
+ action: "upload",
31
+ formData,
32
+ ...uploadAction,
33
+ });
34
+ },
35
+ };
20
36
  return (
21
37
  <UploadModal
22
38
  icon="upload"
39
+ extraAction={canAutoPublish ? extraAction : undefined}
23
40
  trigger={
24
41
  <Dropdown.Item
25
42
  icon="upload"
@@ -36,24 +53,31 @@ export const ImplementationsUploadButton = ({
36
53
  <FormattedMessage id="ruleImplementations.actions.upload.confirmation.content" />
37
54
  }
38
55
  param="implementations"
39
- handleSubmit={(data) =>
40
- uploadImplementations({
56
+ // handleSubmit is the onClick for the "yes" action
57
+ handleSubmit={(formData) => {
58
+ return uploadImplementations({
41
59
  action: "upload",
42
- data,
60
+ formData,
43
61
  ...uploadAction,
44
- })
45
- }
62
+ });
63
+ }}
46
64
  />
47
65
  );
48
66
  };
49
67
 
50
68
  ImplementationsUploadButton.propTypes = {
69
+ canAutoPublish: PropTypes.bool,
51
70
  uploadImplementations: PropTypes.func,
52
71
  loading: PropTypes.bool,
53
72
  };
54
73
 
55
- const mapStateToProps = ({ uploadingImplementationsFile }) => ({
74
+ const mapStateToProps = ({
75
+ uploadingImplementationsFile,
76
+ implementationsActions,
77
+ }) => ({
78
+ canAutoPublish: !_.isEmpty(implementationsActions?.autoPublish),
56
79
  loading: uploadingImplementationsFile,
80
+ implementationsActions,
57
81
  });
58
82
 
59
83
  export default connect(mapStateToProps, { uploadImplementations })(
@@ -1,23 +1,132 @@
1
1
  import React from "react";
2
2
  import { render } from "@truedat/test/render";
3
+ import { waitFor, fireEvent } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
3
5
  import { ImplementationsUploadButton } from "../ImplementationsUploadButton";
4
6
 
5
7
  describe("<ImplementationsUploadButton />", () => {
8
+ const uploadImplementationsMock = jest.fn();
6
9
  const props = {
7
- uploadRules: jest.fn(),
8
- upload: false,
10
+ uploadImplementations: uploadImplementationsMock,
11
+ loading: false,
9
12
  };
10
- const renderOpts = {
11
- messages: {
12
- en: { "ruleImplementations.actions.upload.tooltip": "Upload Rules" },
13
- },
13
+
14
+ // See https://react-dropzone.js.org/ "Testing" section
15
+ const mockData = (files) => {
16
+ return {
17
+ dataTransfer: {
18
+ files,
19
+ items: files.map((file) => ({
20
+ kind: "file",
21
+ type: file.type,
22
+ getAsFile: () => file,
23
+ })),
24
+ types: ["Files"],
25
+ },
26
+ };
14
27
  };
15
28
 
16
- it("matches the latest snapshot", () => {
17
- const { contanier } = render(
18
- <ImplementationsUploadButton {...props} />,
19
- renderOpts
29
+ it("shows the modal", async () => {
30
+ const { getByRole, getByText } = render(
31
+ <ImplementationsUploadButton {...{ ...props, canAutoPublish: true }} />
32
+ );
33
+
34
+ await waitFor(() => {
35
+ userEvent.click(getByRole("option", { name: "Upload Implementations" }));
36
+ });
37
+
38
+ const uploadButton = getByRole("button", { name: "Upload and update" });
39
+ expect(uploadButton).toBeDisabled();
40
+ const publishButton = getByRole("button", { name: "Upload and publish" });
41
+ expect(publishButton).toBeDisabled();
42
+
43
+ expect(getByText("Upload Implementations")).toBeInTheDocument();
44
+ expect(
45
+ getByText("Drag an drop file or click to select file")
46
+ ).toBeInTheDocument();
47
+ });
48
+
49
+ it("false canAutoPublish makes the modal not show the publish button; update using an implementations CSV", async () => {
50
+ const { getByRole, queryByRole, getByTestId } = render(
51
+ <ImplementationsUploadButton {...{ ...props, canAutoPublish: false }} />
52
+ );
53
+
54
+ await waitFor(() => {
55
+ userEvent.click(getByRole("option", { name: "Upload Implementations" }));
56
+ });
57
+
58
+ const updateButton = getByRole("button", { name: "Upload and update" });
59
+ expect(updateButton).toBeDisabled();
60
+
61
+ const publishButton = queryByRole("button", { name: "Upload and publish" });
62
+ expect(publishButton).not.toBeInTheDocument();
63
+
64
+ const fakeFile = new File(["implementations"], "implementations.csv", {
65
+ type: "text/csv",
66
+ });
67
+ const formData = new FormData();
68
+ formData.append("implementations", fakeFile);
69
+ const dropZone = getByTestId("fileDropZone");
70
+ const data = mockData([fakeFile]);
71
+
72
+ await waitFor(() => {
73
+ fireEvent.drop(dropZone, data);
74
+ });
75
+
76
+ expect(updateButton).toBeEnabled();
77
+
78
+ await waitFor(() => {
79
+ userEvent.click(updateButton);
80
+ });
81
+
82
+ expect(uploadImplementationsMock).toHaveBeenCalledWith({
83
+ action: "upload",
84
+ formData,
85
+ href: "/api/rule_implementations/upload",
86
+ method: "POST",
87
+ });
88
+ });
89
+
90
+ it("true canAutoPublish makes the modal show the publish button; publishing an implementations CSV puts auto_publish FormData to true", async () => {
91
+ const { getByRole, getByTestId } = render(
92
+ <ImplementationsUploadButton {...{ ...props, canAutoPublish: true }} />
20
93
  );
21
- expect(contanier).toMatchSnapshot();
94
+
95
+ await waitFor(() => {
96
+ userEvent.click(getByRole("option", { name: "Upload Implementations" }));
97
+ });
98
+
99
+ const updateButton = getByRole("button", { name: "Upload and update" });
100
+ expect(updateButton).toBeDisabled();
101
+
102
+ const publishButton = getByRole("button", { name: "Upload and publish" });
103
+ expect(updateButton).toBeDisabled();
104
+
105
+ const fakeFile = new File(["implementations"], "implementations.csv", {
106
+ type: "text/csv",
107
+ });
108
+ const formData = new FormData();
109
+ formData.append("implementations", fakeFile);
110
+ formData.append("auto_publish", true);
111
+ const dropZone = getByTestId("fileDropZone");
112
+ const data = mockData([fakeFile]);
113
+
114
+ await waitFor(() => {
115
+ fireEvent.drop(dropZone, data);
116
+ });
117
+
118
+ expect(updateButton).toBeEnabled();
119
+ expect(publishButton).toBeEnabled();
120
+
121
+ await waitFor(() => {
122
+ userEvent.click(publishButton);
123
+ });
124
+
125
+ expect(uploadImplementationsMock).toHaveBeenCalledWith({
126
+ action: "upload",
127
+ formData,
128
+ href: "/api/rule_implementations/upload",
129
+ method: "POST",
130
+ });
22
131
  });
23
132
  });
@@ -0,0 +1,158 @@
1
+ import React from "react";
2
+ import { Dropdown } from "semantic-ui-react";
3
+ import { render } from "@truedat/test/render";
4
+ import { waitFor, fireEvent } from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
+ import { UploadModal } from "@truedat/core/components";
7
+
8
+ describe("<ImplementationsUploadButton />", () => {
9
+ const trigger = <Dropdown.Item icon="upload" text="Trigger element" />;
10
+ const onClickExtraAction = jest.fn();
11
+ const onClickYesAction = jest.fn();
12
+
13
+ // See https://react-dropzone.js.org/ "Testing" section
14
+ const mockData = (files) => {
15
+ return {
16
+ dataTransfer: {
17
+ files,
18
+ items: files.map((file) => ({
19
+ kind: "file",
20
+ type: file.type,
21
+ getAsFile: () => file,
22
+ })),
23
+ types: ["Files"],
24
+ },
25
+ };
26
+ };
27
+
28
+ const extraAction = {
29
+ key: "yesWithAutoPublish",
30
+ primary: true,
31
+ content: "Extra action button",
32
+ onClick: onClickExtraAction,
33
+ };
34
+
35
+ it("shows the modal", async () => {
36
+ const { getByRole, getByText } = render(
37
+ <UploadModal
38
+ {...{
39
+ trigger,
40
+ extraAction,
41
+ header: "header",
42
+ content: "content",
43
+ param: "implementations",
44
+ handleSubmit: onClickYesAction,
45
+ }}
46
+ />
47
+ );
48
+
49
+ await waitFor(() => {
50
+ userEvent.click(getByRole("option", { name: "Trigger element" }));
51
+ });
52
+
53
+ const yesActionButton = getByRole("button", { name: "Upload and update" });
54
+ expect(yesActionButton).toBeDisabled();
55
+
56
+ const extraActionButton = getByRole("button", {
57
+ name: "Extra action button",
58
+ });
59
+ expect(extraActionButton).toBeDisabled();
60
+
61
+ expect(getByText("Trigger element")).toBeInTheDocument();
62
+ expect(
63
+ getByText("Drag an drop file or click to select file")
64
+ ).toBeInTheDocument();
65
+ });
66
+
67
+ it("clicks yes action button", async () => {
68
+ const { getByRole, getByTestId, queryByRole } = render(
69
+ <UploadModal
70
+ {...{
71
+ trigger,
72
+ header: "header",
73
+ content: "content",
74
+ param: "implementations",
75
+ handleSubmit: onClickYesAction,
76
+ }}
77
+ />
78
+ );
79
+
80
+ await waitFor(() => {
81
+ userEvent.click(getByRole("option", { name: "Trigger element" }));
82
+ });
83
+
84
+ const yesActionButton = getByRole("button", { name: "Upload and update" });
85
+ expect(yesActionButton).toBeDisabled();
86
+
87
+ const extraActionButton = queryByRole("button", {
88
+ name: "Extra action button",
89
+ });
90
+ expect(extraActionButton).not.toBeInTheDocument();
91
+
92
+ const fakeFile = new File(["implementations"], "implementations.csv", {
93
+ type: "text/csv",
94
+ });
95
+
96
+ const dropZone = getByTestId("fileDropZone");
97
+ const data = mockData([fakeFile]);
98
+
99
+ await waitFor(() => {
100
+ fireEvent.drop(dropZone, data);
101
+ });
102
+
103
+ expect(yesActionButton).toBeEnabled();
104
+
105
+ await waitFor(() => {
106
+ userEvent.click(yesActionButton);
107
+ });
108
+
109
+ expect(onClickYesAction).toHaveBeenCalled();
110
+ });
111
+
112
+ it("clicks extra action button", async () => {
113
+ const { getByRole, getByTestId } = render(
114
+ <UploadModal
115
+ {...{
116
+ trigger,
117
+ extraAction,
118
+ header: "header",
119
+ content: "content",
120
+ param: "implementations",
121
+ handleSubmit: onClickYesAction,
122
+ }}
123
+ />
124
+ );
125
+
126
+ await waitFor(() => {
127
+ userEvent.click(getByRole("option", { name: "Trigger element" }));
128
+ });
129
+
130
+ const yesActionButton = getByRole("button", { name: "Upload and update" });
131
+ expect(yesActionButton).toBeDisabled();
132
+
133
+ const extraActionButton = getByRole("button", {
134
+ name: "Extra action button",
135
+ });
136
+ expect(extraActionButton).toBeDisabled();
137
+
138
+ const fakeFile = new File(["implementations"], "implementations.csv", {
139
+ type: "text/csv",
140
+ });
141
+
142
+ const dropZone = getByTestId("fileDropZone");
143
+ const data = mockData([fakeFile]);
144
+
145
+ await waitFor(() => {
146
+ fireEvent.drop(dropZone, data);
147
+ });
148
+
149
+ expect(yesActionButton).toBeEnabled();
150
+ expect(extraActionButton).toBeEnabled();
151
+
152
+ await waitFor(() => {
153
+ userEvent.click(extraActionButton);
154
+ });
155
+
156
+ expect(onClickExtraAction).toHaveBeenCalled();
157
+ });
158
+ });
@@ -29,9 +29,11 @@ describe("sagas: uploadImplementationsRequestsSaga", () => {
29
29
 
30
30
  describe("sagas: uploadImplementationsSaga", () => {
31
31
  const body = { implementations: "content" };
32
- const payload = { data: body };
33
- const data = { message: [1, 2] };
32
+ const payload = { formData: body };
33
+
34
34
  it("should put a access action when a response is returned", () => {
35
+ const implementationIds = [1, 2];
36
+ const data = { message: implementationIds };
35
37
  expect(() => {
36
38
  testSaga(uploadImplementationsSaga, { payload })
37
39
  .next()
@@ -5,7 +5,7 @@ import { API_RULE_IMPLEMENTATIONS_UPLOAD } from "../api";
5
5
 
6
6
  export function* uploadImplementationsSaga({ payload }) {
7
7
  try {
8
- const { data: body } = payload;
8
+ const { formData: body } = payload;
9
9
  yield put(uploadImplementations.request());
10
10
  const { data } = yield call(
11
11
  apiJsonPost,
@@ -1,3 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`<ImplementationsUploadButton /> matches the latest snapshot 1`] = `undefined`;