@truedat/dq 8.11.3 → 8.11.6

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/dq",
3
- "version": "8.11.3",
3
+ "version": "8.11.6",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -55,42 +55,42 @@
55
55
  ]
56
56
  },
57
57
  "devDependencies": {
58
- "@testing-library/dom": "^10.4.1",
59
- "@testing-library/jest-dom": "^6.6.4",
60
- "@testing-library/react": "^16.3.3",
61
- "@testing-library/user-event": "^14.6.7",
62
- "@truedat/test": "8.11.3",
58
+ "@testing-library/dom": "^10.4.0",
59
+ "@testing-library/jest-dom": "^6.6.3",
60
+ "@testing-library/react": "^16.3.0",
61
+ "@testing-library/user-event": "^14.6.1",
62
+ "@truedat/test": "8.11.6",
63
63
  "identity-obj-proxy": "^3.0.0",
64
64
  "jest": "^29.7.0",
65
65
  "redux-saga-test-plan": "^4.0.6"
66
66
  },
67
67
  "dependencies": {
68
- "@apollo/client": "^3.13.9",
69
- "axios": "^1.15.2",
68
+ "@apollo/client": "^3.13.8",
69
+ "axios": "^1.15.0",
70
70
  "file-saver": "^2.0.5",
71
71
  "graphql": "^16.11.0",
72
72
  "is-hotkey": "^0.2.0",
73
73
  "is-url": "^1.2.4",
74
- "lodash": "^4.17.23",
74
+ "lodash": "^4.17.21",
75
75
  "moment": "^2.30.1",
76
76
  "path-to-regexp": "^8.2.0",
77
77
  "prop-types": "^15.8.1",
78
78
  "query-string": "^7.1.3",
79
- "react": "^19.2.8",
79
+ "react": "^19.2.7",
80
80
  "react-csv": "^2.2.2",
81
- "react-dom": "^19.2.8",
81
+ "react-dom": "^19.2.7",
82
82
  "react-dropzone": "^14.3.8",
83
83
  "react-hook-form": "^7.56.4",
84
- "react-intl": "^7.1.14",
85
- "react-moment": "^1.1.4",
84
+ "react-intl": "^7.1.11",
85
+ "react-moment": "^1.1.3",
86
86
  "react-redux": "^9.2.0",
87
- "react-router": "^8.3.1",
87
+ "react-router": "^8.3.0",
88
88
  "redux": "^5.0.1",
89
89
  "redux-saga": "^1.3.0",
90
90
  "redux-saga-routines": "^3.2.3",
91
91
  "reselect": "^5.1.1",
92
92
  "semantic-ui-react": "^3.0.0-beta.2",
93
- "swr": "^2.3.8"
93
+ "swr": "^2.3.3"
94
94
  },
95
- "gitHead": "c0d3c1ed5c9480e6207a6e696d8941043c34de2f"
95
+ "gitHead": "bfd7e4d934a88ec244c45032ca76103f236e9462"
96
96
  }
@@ -1,96 +1,96 @@
1
1
  import _ from "lodash/fp";
2
- import { lazy, useState } from "react";
3
- import { Button, Form, Header } from "semantic-ui-react";
2
+ import { useState, useEffect } from "react";
3
+ import { Button, Modal } from "semantic-ui-react";
4
4
  import PropTypes from "prop-types";
5
5
  import { useIntl } from "react-intl";
6
- import { TemplateSelector } from "@truedat/core/components";
7
6
  import { validateContent } from "@truedat/df/utils";
7
+ import { ExecutionTemplateSelector } from "./ExecutionTemplateSelector";
8
+ import { ExecutionWarning } from "./ExecutionWarning";
8
9
 
9
- const DynamicForm = lazy(() => import("@truedat/df/components/DynamicForm"));
10
-
11
- export const ExecutionForm = ({ count, onSubmit, onCancel }) => {
10
+ export const ExecutionForm = ({ count, onSubmit, onCancel, open }) => {
12
11
  const { formatMessage } = useIntl();
13
12
  const [content, setContent] = useState({});
14
13
  const [template, setTemplate] = useState();
15
- const [templatesLoading, setTemplatesLoading] = useState(true);
16
-
17
- const handleContentChange = (content) => setContent(content);
14
+ const [templates, setTemplates] = useState();
18
15
 
19
- const handleTemplatesLoaded = ({ templates }) => {
20
- setTemplatesLoading(false);
21
- if (templates?.length === 1) {
22
- setTemplate(templates[0]);
16
+ useEffect(() => {
17
+ if (open) {
18
+ setTemplate(templates?.length === 1 ? templates[0] : undefined);
19
+ setContent({});
23
20
  }
24
- };
21
+ }, [open, templates]);
25
22
 
26
- const handleTemplateSelected = (e, { template }) => setTemplate(template);
23
+ const handleTemplatesLoaded = ({ templates }) =>
24
+ setTemplates(templates || []);
25
+
26
+ const handleTemplateSelected = (e, { template }) => {
27
+ setTemplate(template);
28
+ setContent({});
29
+ };
27
30
 
28
- const isInvalid = () =>
29
- template && !_.isEmpty(validateContent(template)(content));
31
+ const isExecuteDisabled =
32
+ !!template &&
33
+ !_.isEmpty(template ? validateContent(template)(content) : []);
30
34
 
31
35
  return (
32
36
  <>
33
- <Header
34
- as="h2"
35
- content={formatMessage({
36
- id: "implementations.actions.execution.confirmation.header",
37
- })}
38
- />
39
- <Form loading={templatesLoading}>
40
- <TemplateSelector
41
- scope="qe"
42
- selectedValue={template?.id}
43
- onChange={handleTemplateSelected}
44
- onLoad={handleTemplatesLoaded}
45
- clearable
46
- />
47
- {template?.id ? (
48
- <>
49
- <Header
50
- as="h3"
51
- content={formatMessage({
52
- id: "implementations.actions.execution.confirmation.legend",
37
+ <Modal.Header>
38
+ <div className="execution-popup-header">
39
+ <div>
40
+ {formatMessage({
41
+ id: "implementations.actions.execution.confirmation.header",
42
+ defaultMessage: "Implementations execution",
43
+ })}
44
+ <p className="subtitle">
45
+ {formatMessage({
46
+ id: "implementations.actions.execution.subtitle",
47
+ defaultMessage: "Select template for additional information",
53
48
  })}
54
- />
55
- <div
56
- style={{
57
- maxHeight: "calc(100vh - 20rem)",
58
- overflowY: "auto",
59
- }}
60
- >
61
- <DynamicForm
62
- onChange={handleContentChange}
63
- content={content}
64
- template={template}
65
- />
66
- </div>
67
- </>
68
- ) : null}
69
- <p>
70
- {formatMessage(
71
- {
72
- id:
73
- count === 1
74
- ? "implementation.actions.execution.confirmation.content"
75
- : "implementations.actions.execution.confirmation.content",
76
- },
77
- { implementations_count: count },
78
- )}
79
- </p>
80
- <div className="actions">
81
- <Button
82
- secondary
49
+ </p>
50
+ </div>
51
+ <button
52
+ type="button"
53
+ className="close-button"
83
54
  onClick={onCancel}
84
- content={formatMessage({ id: "actions.cancel" })}
85
- />
86
- <Button
87
- primary
88
- disabled={isInvalid()}
89
- onClick={() => onSubmit(content)}
90
- content={formatMessage({ id: "actions.create" })}
91
- />
55
+ aria-label={formatMessage({
56
+ id: "actions.close",
57
+ defaultMessage: "Close",
58
+ })}
59
+ >
60
+ &times;
61
+ </button>
92
62
  </div>
93
- </Form>
63
+ </Modal.Header>
64
+ <Modal.Content className={template ? "has-template" : undefined}>
65
+ <ExecutionTemplateSelector
66
+ templates={templates}
67
+ template={template}
68
+ content={content}
69
+ onTemplateSelected={handleTemplateSelected}
70
+ onTemplatesLoaded={handleTemplatesLoaded}
71
+ onContentChange={setContent}
72
+ />
73
+ {count !== 1 ? <ExecutionWarning count={count} /> : null}
74
+ </Modal.Content>
75
+ <Modal.Actions>
76
+ <Button
77
+ secondary
78
+ content={formatMessage({
79
+ id: "actions.cancel",
80
+ defaultMessage: "Cancel",
81
+ })}
82
+ onClick={onCancel}
83
+ />
84
+ <Button
85
+ primary
86
+ disabled={isExecuteDisabled}
87
+ content={formatMessage({
88
+ id: "actions.create",
89
+ defaultMessage: "Execute",
90
+ })}
91
+ onClick={() => onSubmit(template ? content : {})}
92
+ />
93
+ </Modal.Actions>
94
94
  </>
95
95
  );
96
96
  };
@@ -99,6 +99,7 @@ ExecutionForm.propTypes = {
99
99
  count: PropTypes.number,
100
100
  onSubmit: PropTypes.func,
101
101
  onCancel: PropTypes.func,
102
+ open: PropTypes.bool,
102
103
  };
103
104
 
104
105
  export default ExecutionForm;
@@ -2,7 +2,7 @@ import { useState } from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import { connect } from "react-redux";
4
4
  import { useIntl } from "react-intl";
5
- import { Button, Popup } from "semantic-ui-react";
5
+ import { Button, Modal } from "semantic-ui-react";
6
6
  import ExecutionForm from "./ExecutionForm";
7
7
 
8
8
  export const ExecutionPopup = ({
@@ -14,36 +14,34 @@ export const ExecutionPopup = ({
14
14
  const { formatMessage } = useIntl();
15
15
  const [open, setOpen] = useState(false);
16
16
 
17
+ const executeLabelId =
18
+ count === 1
19
+ ? "implementation.actions.do_execution"
20
+ : "implementations.actions.do_execution";
21
+
17
22
  return (
18
- <Popup
19
- on="click"
20
- basic
21
- flowing
22
- onOpen={() => setOpen(true)}
23
- onClose={() => false}
24
- open={open}
25
- position="bottom center"
26
- size="small"
27
- trigger={
28
- <Button
29
- secondary
30
- disabled={disabled}
31
- loading={executionGroupLoading}
32
- content={formatMessage({
33
- id:
34
- count === 1
35
- ? "implementation.actions.do_execution"
36
- : "implementations.actions.do_execution",
37
- })}
38
- />
39
- }
40
- >
41
- <ExecutionForm
42
- count={count}
43
- onSubmit={onSubmit}
44
- onCancel={() => setOpen(false)}
23
+ <div className="execution-popup-wrapper">
24
+ <Button
25
+ primary
26
+ disabled={disabled}
27
+ loading={executionGroupLoading}
28
+ content={formatMessage({ id: executeLabelId })}
29
+ onClick={() => setOpen(true)}
45
30
  />
46
- </Popup>
31
+ <Modal
32
+ className="execution-popup-modal"
33
+ open={open}
34
+ onClose={() => setOpen(false)}
35
+ size="small"
36
+ >
37
+ <ExecutionForm
38
+ count={count}
39
+ onSubmit={onSubmit}
40
+ onCancel={() => setOpen(false)}
41
+ open={open}
42
+ />
43
+ </Modal>
44
+ </div>
47
45
  );
48
46
  };
49
47
 
@@ -0,0 +1,108 @@
1
+ import { lazy } from "react";
2
+ import { Icon } from "semantic-ui-react";
3
+ import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
5
+ import _ from "lodash/fp";
6
+ import { TemplateSelector } from "@truedat/core/components";
7
+ import { validateContent, flattenFields, isRequired } from "@truedat/df/utils";
8
+
9
+ const DynamicForm = lazy(() => import("@truedat/df/components/DynamicForm"));
10
+
11
+ export const ExecutionTemplateSelector = ({
12
+ templates,
13
+ template,
14
+ content,
15
+ onTemplateSelected,
16
+ onTemplatesLoaded,
17
+ onContentChange,
18
+ }) => {
19
+ const { formatMessage } = useIntl();
20
+
21
+ const validationErrors = template ? validateContent(template)(content) : [];
22
+ const hasPendingMandatoryFields = !_.isEmpty(validationErrors);
23
+ const templateHasMandatoryFields = template
24
+ ? _.some((field) => isRequired(field, {}))(flattenFields(template))
25
+ : false;
26
+
27
+ const isSingleTemplate = templates?.length === 1;
28
+
29
+ return (
30
+ <div className="execution-popup-template-area">
31
+ {templates?.length !== 0 && !isSingleTemplate ? (
32
+ <div className="execution-popup-selector-label">
33
+ <span>
34
+ {formatMessage({
35
+ id: "implementations.actions.execution.template_label",
36
+ defaultMessage: "Select template",
37
+ })}
38
+ </span>
39
+ <span
40
+ className={
41
+ templateHasMandatoryFields
42
+ ? hasPendingMandatoryFields
43
+ ? "mandatory-badge pending"
44
+ : "mandatory-badge complete"
45
+ : "optional-badge"
46
+ }
47
+ >
48
+ {templateHasMandatoryFields
49
+ ? formatMessage({
50
+ id: "implementations.actions.execution.mandatory_fields",
51
+ defaultMessage: "Mandatory fields",
52
+ })
53
+ : formatMessage({
54
+ id: "implementations.actions.execution.optional",
55
+ defaultMessage: "optional",
56
+ })}
57
+ </span>
58
+ </div>
59
+ ) : null}
60
+ <TemplateSelector
61
+ scope="qe"
62
+ selectedValue={template?.id}
63
+ onChange={onTemplateSelected}
64
+ onLoad={onTemplatesLoaded}
65
+ clearable
66
+ hideLabel
67
+ />
68
+ {isSingleTemplate ? (
69
+ <div className="execution-popup-single-template-label">
70
+ {formatMessage({
71
+ id: "implementations.actions.execution.label.single",
72
+ defaultMessage:
73
+ "Complete the template for additional information (optional)",
74
+ })}
75
+ </div>
76
+ ) : null}
77
+ {template?.id ? (
78
+ <div className="execution-popup-template-fields">
79
+ <h4>
80
+ <Icon name="info circle" size="small" />
81
+ {formatMessage({
82
+ id: "implementations.actions.execution.confirmation.legend",
83
+ defaultMessage: "Template fields",
84
+ })}
85
+ </h4>
86
+ <div className="execution-popup-template-scroll">
87
+ <DynamicForm
88
+ onChange={onContentChange}
89
+ content={content}
90
+ template={template}
91
+ />
92
+ </div>
93
+ </div>
94
+ ) : null}
95
+ </div>
96
+ );
97
+ };
98
+
99
+ ExecutionTemplateSelector.propTypes = {
100
+ templates: PropTypes.array,
101
+ template: PropTypes.object,
102
+ content: PropTypes.object,
103
+ onTemplateSelected: PropTypes.func,
104
+ onTemplatesLoaded: PropTypes.func,
105
+ onContentChange: PropTypes.func,
106
+ };
107
+
108
+ export default ExecutionTemplateSelector;
@@ -0,0 +1,36 @@
1
+ import { Icon } from "semantic-ui-react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+
5
+ export const ExecutionWarning = ({ count }) => {
6
+ const { formatMessage } = useIntl();
7
+
8
+ return (
9
+ <div className="execution-popup-warning">
10
+ <Icon name="warning sign" color="yellow" size="large" />
11
+ <p>
12
+ {formatMessage(
13
+ {
14
+ id: "implementations.actions.execution.confirmation.warning",
15
+ defaultMessage:
16
+ "<b>{count}</b> implementations will be executed. Is this correct?",
17
+ },
18
+ {
19
+ count,
20
+ b: (chunks) => (
21
+ <strong key="count" className="count">
22
+ {chunks}
23
+ </strong>
24
+ ),
25
+ },
26
+ )}
27
+ </p>
28
+ </div>
29
+ );
30
+ };
31
+
32
+ ExecutionWarning.propTypes = {
33
+ count: PropTypes.number,
34
+ };
35
+
36
+ export default ExecutionWarning;
@@ -101,7 +101,7 @@ export const ImplementationActions = ({
101
101
  <>
102
102
  {actions?.edit ? (
103
103
  <Button
104
- primary
104
+ secondary
105
105
  content={formatMessage({ id: "actions.edit" })}
106
106
  as={Link}
107
107
  to={linkTo.IMPLEMENTATION_EDIT({ implementation_id: id })}
@@ -134,7 +134,9 @@ export const ImplementationActions = ({
134
134
  onClick={() => submitImplementation({ id })}
135
135
  />
136
136
  ) : null}
137
- {actions?.execute ? <ExecutionPopup onSubmit={handleExecute} /> : null}
137
+ {actions?.execute ? (
138
+ <ExecutionPopup count={1} onSubmit={handleExecute} />
139
+ ) : null}
138
140
  {_.isEmpty(secondaryActions) ? null : (
139
141
  <GroupActions availableActions={secondaryActions} />
140
142
  )}
@@ -23,7 +23,7 @@ export const RuleImplementation = ({ children, ruleImplementation }) => {
23
23
  </Header>
24
24
  </Grid.Column>
25
25
  <Grid.Column width={8} textAlign="right">
26
- <>
26
+ <div className="rule-implementation-actions">
27
27
  {deletedAt ? null : (
28
28
  <Subscription
29
29
  resource={ruleImplementation}
@@ -31,7 +31,7 @@ export const RuleImplementation = ({ children, ruleImplementation }) => {
31
31
  />
32
32
  )}
33
33
  <ImplementationActions />
34
- </>
34
+ </div>
35
35
  </Grid.Column>
36
36
  </Grid>
37
37
  <RuleImplementationTabs />
@@ -223,7 +223,7 @@ export const Subscription = ({
223
223
  <Button
224
224
  basic
225
225
  icon={<Icon name="eye" className={className} />}
226
- className="button icon group-actions"
226
+ className="button icon group-actions subscription-action-button"
227
227
  data-tooltip={buttonMesage}
228
228
  />
229
229
  }
@@ -1,24 +1,118 @@
1
1
  import userEvent from "@testing-library/user-event";
2
2
  import { render, waitForLoad } from "@truedat/test/render";
3
- import { multipleTemplatesMock } from "@truedat/test/mocks";
3
+ import {
4
+ multipleTemplatesMock,
5
+ singleTemplateMock,
6
+ templatesMockWith,
7
+ } from "@truedat/test/mocks";
4
8
  import { ExecutionForm } from "../ExecutionForm";
5
9
 
10
+ const variables = { scope: "qe" };
11
+ const optionalTemplate = {
12
+ __typename: "Template",
13
+ id: "optional",
14
+ name: "optional",
15
+ scope: "qe",
16
+ label: "Optional template",
17
+ content: [
18
+ {
19
+ name: "additional_information",
20
+ fields: [{ name: "notes", label: "Notes" }],
21
+ },
22
+ ],
23
+ };
24
+ const requiredTemplate = {
25
+ ...optionalTemplate,
26
+ id: "required",
27
+ label: "Required template",
28
+ content: [
29
+ {
30
+ name: "additional_information",
31
+ fields: [
32
+ { name: "notes", label: "Notes", type: "string", cardinality: "1" },
33
+ ],
34
+ },
35
+ ],
36
+ };
37
+
38
+ const renderForm = (props = {}, mocks = [multipleTemplatesMock(variables)]) =>
39
+ render(
40
+ <ExecutionForm
41
+ count={2}
42
+ onSubmit={jest.fn()}
43
+ onCancel={jest.fn()}
44
+ open
45
+ {...props}
46
+ />,
47
+ { mocks },
48
+ );
49
+
6
50
  describe("<ExecutionForm />", () => {
7
- const handleSubmit = jest.fn();
8
- const selectTemplate = jest.fn();
9
- const props = {
10
- count: 1,
11
- templates: [],
12
- handleSubmit,
13
- selectTemplate,
14
- };
15
- const renderOpts = {
16
- mocks: [multipleTemplatesMock({ scope: "qe" })],
17
- };
18
-
19
- it("matches the latest snapshot", async () => {
20
- const rendered = render(<ExecutionForm {...props} />, renderOpts);
51
+ it("shows the template selector and the bulk execution warning", async () => {
52
+ const rendered = renderForm();
21
53
  await waitForLoad(rendered);
54
+
55
+ expect(rendered.queryByRole("radio")).not.toBeInTheDocument();
56
+ expect(
57
+ rendered.getByText("Select template for additional information"),
58
+ ).toBeInTheDocument();
59
+ expect(rendered.getByText("Select template")).toBeInTheDocument();
60
+ expect(rendered.getByText("optional")).toHaveClass("optional-badge");
61
+ expect(rendered.getByRole("combobox")).toBeInTheDocument();
62
+ expect(rendered.getByText("2")).toHaveClass("count");
22
63
  expect(rendered.container).toMatchSnapshot();
23
64
  });
65
+
66
+ it("shows a single template directly without selector or info banner", async () => {
67
+ const rendered = renderForm({ count: 1 }, [singleTemplateMock(variables)]);
68
+ await waitForLoad(rendered);
69
+
70
+ expect(
71
+ rendered.getByText(
72
+ "Complete the template for additional information (optional)",
73
+ ),
74
+ ).toBeInTheDocument();
75
+ expect(rendered.queryByRole("combobox")).not.toBeInTheDocument();
76
+ expect(rendered.queryByText(/will be executed/i)).not.toBeInTheDocument();
77
+ });
78
+
79
+ it("enables execution when the template has no required fields", async () => {
80
+ const rendered = renderForm({ count: 1 }, [
81
+ templatesMockWith(variables, [optionalTemplate]),
82
+ ]);
83
+ await waitForLoad(rendered);
84
+
85
+ expect(rendered.getByRole("button", { name: "Execute" })).toBeEnabled();
86
+ });
87
+
88
+ it("marks a selected template as mandatory and switches from pending to complete", async () => {
89
+ const rendered = renderForm({ count: 1 }, [
90
+ templatesMockWith(variables, [requiredTemplate, optionalTemplate]),
91
+ ]);
92
+ await waitForLoad(rendered);
93
+ const user = userEvent.setup({ delay: null });
94
+
95
+ await user.click(rendered.getByRole("combobox"));
96
+ await user.click(
97
+ rendered.getByRole("option", { name: "Required template" }),
98
+ );
99
+
100
+ const executeButton = rendered.getByRole("button", { name: "Execute" });
101
+ expect(rendered.getByText("Mandatory fields")).toHaveClass(
102
+ "mandatory-badge",
103
+ "pending",
104
+ );
105
+ expect(executeButton).toBeDisabled();
106
+
107
+ await user.type(
108
+ rendered.container.querySelector('input[name="notes"]'),
109
+ "Additional information",
110
+ );
111
+
112
+ expect(rendered.getByText("Mandatory fields")).toHaveClass(
113
+ "mandatory-badge",
114
+ "complete",
115
+ );
116
+ expect(executeButton).toBeEnabled();
117
+ });
24
118
  });