@truedat/dq 8.12.12 → 8.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "8.12.12",
3
+ "version": "8.13.0",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -59,7 +59,7 @@
59
59
  "@testing-library/jest-dom": "^6.6.3",
60
60
  "@testing-library/react": "^16.3.0",
61
61
  "@testing-library/user-event": "^14.6.1",
62
- "@truedat/test": "8.12.12",
62
+ "@truedat/test": "8.13.0",
63
63
  "identity-obj-proxy": "^3.0.0",
64
64
  "jest": "^29.7.0",
65
65
  "redux-saga-test-plan": "^4.0.6"
@@ -92,5 +92,5 @@
92
92
  "semantic-ui-react": "^3.0.0-beta.2",
93
93
  "swr": "^2.3.3"
94
94
  },
95
- "gitHead": "b6e3413dae81b2e28e98a21cf371174caa30ab4b"
95
+ "gitHead": "b9d1c2133cea6293e6a5ce5d198a4cd49133b4c1"
96
96
  }
@@ -1,6 +1,6 @@
1
1
  import _ from "lodash/fp";
2
2
  import { useState, useEffect } from "react";
3
- import { Button, Modal } from "semantic-ui-react";
3
+ import { TrueModal } from "@truedat/core/components";
4
4
  import PropTypes from "prop-types";
5
5
  import { useIntl } from "react-intl";
6
6
  import { validateContent } from "@truedat/df/utils";
@@ -33,65 +33,51 @@ export const ExecutionForm = ({ count, onSubmit, onCancel, open }) => {
33
33
  !_.isEmpty(template ? validateContent(template)(content) : []);
34
34
 
35
35
  return (
36
- <>
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",
48
- })}
49
- </p>
50
- </div>
51
- <button
52
- type="button"
53
- className="close-button"
54
- onClick={onCancel}
55
- aria-label={formatMessage({
56
- id: "actions.close",
57
- defaultMessage: "Close",
58
- })}
59
- >
60
- &times;
61
- </button>
62
- </div>
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({
36
+ <TrueModal
37
+ className={`execution-popup-modal${template ? " has-template" : ""}`}
38
+ open={open}
39
+ onClose={onCancel}
40
+ size="small"
41
+ title={formatMessage({
42
+ id: "implementations.actions.execution.confirmation.header",
43
+ defaultMessage: "Implementations execution",
44
+ })}
45
+ subtitle={formatMessage({
46
+ id: "implementations.actions.execution.subtitle",
47
+ defaultMessage: "Select template for additional information",
48
+ })}
49
+ banner={count !== 1 ? <ExecutionWarning count={count} /> : null}
50
+ actions={[
51
+ {
52
+ key: "cancel",
53
+ secondary: true,
54
+ content: formatMessage({
79
55
  id: "actions.cancel",
80
56
  defaultMessage: "Cancel",
81
- })}
82
- onClick={onCancel}
83
- />
84
- <Button
85
- primary
86
- disabled={isExecuteDisabled}
87
- content={formatMessage({
57
+ }),
58
+ onClick: onCancel,
59
+ },
60
+ {
61
+ key: "execute",
62
+ primary: true,
63
+ disabled: isExecuteDisabled,
64
+ content: formatMessage({
88
65
  id: "actions.create",
89
66
  defaultMessage: "Execute",
90
- })}
91
- onClick={() => onSubmit(template ? content : {})}
92
- />
93
- </Modal.Actions>
94
- </>
67
+ }),
68
+ onClick: () => onSubmit(template ? content : {}),
69
+ },
70
+ ]}
71
+ >
72
+ <ExecutionTemplateSelector
73
+ templates={templates}
74
+ template={template}
75
+ content={content}
76
+ onTemplateSelected={handleTemplateSelected}
77
+ onTemplatesLoaded={handleTemplatesLoaded}
78
+ onContentChange={setContent}
79
+ />
80
+ </TrueModal>
95
81
  );
96
82
  };
97
83
 
@@ -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, Modal } from "semantic-ui-react";
5
+ import { Button } from "semantic-ui-react";
6
6
  import ExecutionForm from "./ExecutionForm";
7
7
 
8
8
  export const ExecutionPopup = ({
@@ -28,19 +28,14 @@ export const ExecutionPopup = ({
28
28
  content={formatMessage({ id: executeLabelId })}
29
29
  onClick={() => setOpen(true)}
30
30
  />
31
- <Modal
32
- className="execution-popup-modal"
33
- open={open}
34
- onClose={() => setOpen(false)}
35
- size="small"
36
- >
31
+ {open ? (
37
32
  <ExecutionForm
38
33
  count={count}
39
34
  onSubmit={onSubmit}
40
35
  onCancel={() => setOpen(false)}
41
36
  open={open}
42
37
  />
43
- </Modal>
38
+ ) : null}
44
39
  </div>
45
40
  );
46
41
  };
@@ -1,4 +1,3 @@
1
- import { Icon } from "semantic-ui-react";
2
1
  import PropTypes from "prop-types";
3
2
  import { useIntl } from "react-intl";
4
3
 
@@ -6,26 +5,23 @@ export const ExecutionWarning = ({ count }) => {
6
5
  const { formatMessage } = useIntl();
7
6
 
8
7
  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>
8
+ <p className="execution-popup-warning">
9
+ {formatMessage(
10
+ {
11
+ id: "implementations.actions.execution.confirmation.warning",
12
+ defaultMessage:
13
+ "<b>{count}</b> implementations will be executed. Is this correct?",
14
+ },
15
+ {
16
+ count,
17
+ b: (chunks) => (
18
+ <strong key="count" className="count">
19
+ {chunks}
20
+ </strong>
21
+ ),
22
+ },
23
+ )}
24
+ </p>
29
25
  );
30
26
  };
31
27
 
@@ -1,3 +1,4 @@
1
+ import { screen } from "@testing-library/react";
1
2
  import userEvent from "@testing-library/user-event";
2
3
  import { render, waitForLoad } from "@truedat/test/render";
3
4
  import {
@@ -60,7 +61,7 @@ describe("<ExecutionForm />", () => {
60
61
  expect(rendered.getByText("optional")).toHaveClass("optional-badge");
61
62
  expect(rendered.getByRole("combobox")).toBeInTheDocument();
62
63
  expect(rendered.getByText("2")).toHaveClass("count");
63
- expect(rendered.container).toMatchSnapshot();
64
+ expect(screen.getByRole("dialog")).toMatchSnapshot();
64
65
  });
65
66
 
66
67
  it("shows a single template directly without selector or info banner", async () => {
@@ -90,7 +91,7 @@ describe("<ExecutionForm />", () => {
90
91
  templatesMockWith(variables, [requiredTemplate, optionalTemplate]),
91
92
  ]);
92
93
  await waitForLoad(rendered);
93
- const user = userEvent.setup({ delay: null });
94
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
94
95
 
95
96
  await user.click(rendered.getByRole("combobox"));
96
97
  await user.click(
@@ -105,7 +106,7 @@ describe("<ExecutionForm />", () => {
105
106
  expect(executeButton).toBeDisabled();
106
107
 
107
108
  await user.type(
108
- rendered.container.querySelector('input[name="notes"]'),
109
+ screen.getByRole("dialog").querySelector('input[name="notes"]'),
109
110
  "Additional information",
110
111
  );
111
112
 
@@ -115,4 +116,37 @@ describe("<ExecutionForm />", () => {
115
116
  );
116
117
  expect(executeButton).toBeEnabled();
117
118
  });
119
+ it("submits an empty object without a template", async () => {
120
+ const onSubmit = jest.fn();
121
+ const rendered = renderForm({ onSubmit });
122
+ await waitForLoad(rendered);
123
+ await userEvent
124
+ .setup({ advanceTimers: jest.advanceTimersByTime })
125
+ .click(screen.getByRole("button", { name: "Execute" }));
126
+ expect(onSubmit).toHaveBeenCalledTimes(1);
127
+ expect(onSubmit).toHaveBeenCalledWith({});
128
+ });
129
+
130
+ it("submits template content and clears it when the selection is removed", async () => {
131
+ const onSubmit = jest.fn();
132
+ const rendered = renderForm({ onSubmit }, [
133
+ templatesMockWith(variables, [requiredTemplate, optionalTemplate]),
134
+ ]);
135
+ await waitForLoad(rendered);
136
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
137
+ await user.click(screen.getByRole("combobox"));
138
+ await user.click(screen.getByRole("option", { name: "Required template" }));
139
+ await user.type(
140
+ screen.getByRole("dialog").querySelector('input[name="notes"]'),
141
+ "Additional information",
142
+ );
143
+ await user.click(screen.getByRole("button", { name: "Execute" }));
144
+ expect(onSubmit).toHaveBeenLastCalledWith(
145
+ expect.objectContaining({ notes: expect.anything() }),
146
+ );
147
+ await user.click(screen.getByRole("combobox").querySelector(".clear.icon"));
148
+ await user.click(screen.getByRole("button", { name: "Execute" }));
149
+ expect(onSubmit).toHaveBeenCalledTimes(2);
150
+ expect(onSubmit).toHaveBeenLastCalledWith({});
151
+ });
118
152
  });
@@ -1,20 +1,84 @@
1
- import { renderWithIntl as render } from "@truedat/test/renderWithIntl";
1
+ import { screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { render } from "@truedat/test/render";
4
+ import { templatesMockWith } from "@truedat/test/mocks";
2
5
  import { ExecutionPopup } from "../ExecutionPopup";
3
6
 
4
- describe("<ExecutionPopup />", () => {
5
- const onSubmit = jest.fn();
6
- const props = { count: 1, onSubmit };
7
- const renderOpts = {
7
+ const template = {
8
+ __typename: "Template",
9
+ id: "required",
10
+ name: "required",
11
+ scope: "qe",
12
+ label: "Required template",
13
+ content: [
14
+ {
15
+ name: "additional_information",
16
+ fields: [
17
+ { name: "notes", label: "Notes", type: "string", cardinality: "1" },
18
+ ],
19
+ },
20
+ ],
21
+ };
22
+
23
+ const renderPopup = (props = {}) =>
24
+ render(<ExecutionPopup count={2} onSubmit={jest.fn()} {...props} />, {
25
+ mocks: [
26
+ templatesMockWith({ scope: "qe" }, [template]),
27
+ templatesMockWith({ scope: "qe" }, [template]),
28
+ ],
8
29
  messages: {
9
30
  en: {
10
- "implementation.actions.do_execution": "do_execution",
11
- "implementations.actions.do_execution": "do_execution",
31
+ "implementations.actions.do_execution": "Execute implementations",
12
32
  },
13
33
  },
14
- };
34
+ });
35
+
36
+ const setupUser = () =>
37
+ userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
15
38
 
39
+ describe("<ExecutionPopup />", () => {
16
40
  it("matches the latest snapshot", () => {
17
- const { container } = render(<ExecutionPopup {...props} />, renderOpts);
41
+ const { container } = render(
42
+ <ExecutionPopup count={1} onSubmit={jest.fn()} />,
43
+ {
44
+ messages: {
45
+ en: { "implementation.actions.do_execution": "do_execution" },
46
+ },
47
+ },
48
+ );
18
49
  expect(container).toMatchSnapshot();
19
50
  });
51
+ it.each(["Cancel", "Close", "Escape"])(
52
+ "discards form content on %s and reopening",
53
+ async (closeMethod) => {
54
+ const onSubmit = jest.fn();
55
+ renderPopup({ onSubmit });
56
+ const user = setupUser();
57
+ await user.click(
58
+ screen.getByRole("button", { name: "Execute implementations" }),
59
+ );
60
+ await user.type(await screen.findByRole("textbox"), "Original content");
61
+ expect(screen.getByRole("button", { name: "Execute" })).toBeEnabled();
62
+ if (closeMethod === "Escape") {
63
+ await user.keyboard("{Escape}");
64
+ } else {
65
+ await user.click(screen.getByRole("button", { name: closeMethod }));
66
+ }
67
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
68
+ await user.click(
69
+ screen.getByRole("button", { name: "Execute implementations" }),
70
+ );
71
+ expect(await screen.findByRole("textbox")).toHaveValue("");
72
+ expect(screen.getByRole("button", { name: "Execute" })).toBeDisabled();
73
+ expect(onSubmit).not.toHaveBeenCalled();
74
+ },
75
+ );
76
+
77
+ it("does not open when its trigger is disabled", async () => {
78
+ renderPopup({ disabled: true });
79
+ await setupUser().click(
80
+ screen.getByRole("button", { name: "Execute implementations" }),
81
+ );
82
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
83
+ });
20
84
  });
@@ -11,12 +11,6 @@ describe("<ExecutionWarning />", () => {
11
11
  expect(rendered.getByText("5")).toHaveClass("count");
12
12
  });
13
13
 
14
- it("renders the warning icon", () => {
15
- const rendered = render(<ExecutionWarning count={3} />);
16
-
17
- expect(rendered.container.querySelector(".warning")).toBeInTheDocument();
18
- });
19
-
20
14
  it("renders the correct CSS class", () => {
21
15
  const rendered = render(<ExecutionWarning count={2} />);
22
16
 
@@ -1,32 +1,38 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`<ExecutionForm /> shows the template selector and the bulk execution warning 1`] = `
4
- <div>
4
+ <div
5
+ aria-labelledby="_r_0_"
6
+ class="ui small modal transition visible active true-modal execution-popup-modal"
7
+ role="dialog"
8
+ >
9
+ <button
10
+ aria-label="Close"
11
+ class="true-modal-close"
12
+ type="button"
13
+ >
14
+ ×
15
+ </button>
5
16
  <div
6
17
  class="header"
7
18
  >
8
19
  <div
9
- class="execution-popup-header"
20
+ class="true-modal-heading"
10
21
  >
11
- <div>
22
+ <span
23
+ id="_r_0_"
24
+ >
12
25
  Implementations execution
13
- <p
14
- class="subtitle"
15
- >
16
- Select template for additional information
17
- </p>
18
- </div>
19
- <button
20
- aria-label="Close"
21
- class="close-button"
22
- type="button"
26
+ </span>
27
+ <p
28
+ class="true-modal-subtitle"
23
29
  >
24
- ×
25
- </button>
30
+ Select template for additional information
31
+ </p>
26
32
  </div>
27
33
  </div>
28
34
  <div
29
- class="content"
35
+ class="true-modal-content content"
30
36
  >
31
37
  <div
32
38
  class="execution-popup-template-area"
@@ -111,14 +117,18 @@ exports[`<ExecutionForm /> shows the template selector and the bulk execution wa
111
117
  </div>
112
118
  </div>
113
119
  </div>
114
- <div
115
- class="execution-popup-warning"
116
- >
117
- <i
118
- aria-hidden="true"
119
- class="yellow warning sign large icon"
120
- />
121
- <p>
120
+ </div>
121
+ <div
122
+ class="true-modal-banner warning"
123
+ >
124
+ <i
125
+ aria-hidden="true"
126
+ class="warning sign icon"
127
+ />
128
+ <div>
129
+ <p
130
+ class="execution-popup-warning"
131
+ >
122
132
  <strong
123
133
  class="count"
124
134
  >
@@ -129,15 +139,17 @@ exports[`<ExecutionForm /> shows the template selector and the bulk execution wa
129
139
  </div>
130
140
  </div>
131
141
  <div
132
- class="actions"
142
+ class="actions true-modal-actions"
133
143
  >
134
144
  <button
135
145
  class="ui secondary button"
146
+ type="button"
136
147
  >
137
148
  Cancel
138
149
  </button>
139
150
  <button
140
151
  class="ui primary button"
152
+ type="button"
141
153
  >
142
154
  Execute
143
155
  </button>
@@ -2,21 +2,15 @@
2
2
 
3
3
  exports[`<ExecutionWarning /> matches snapshot 1`] = `
4
4
  <div>
5
- <div
5
+ <p
6
6
  class="execution-popup-warning"
7
7
  >
8
- <i
9
- aria-hidden="true"
10
- class="yellow warning sign large icon"
11
- />
12
- <p>
13
- <strong
14
- class="count"
15
- >
16
- 2
17
- </strong>
18
- implementations will be executed. Is this correct?
19
- </p>
20
- </div>
8
+ <strong
9
+ class="count"
10
+ >
11
+ 2
12
+ </strong>
13
+ implementations will be executed. Is this correct?
14
+ </p>
21
15
  </div>
22
16
  `;
@@ -1,114 +1,31 @@
1
1
  @execution-popup-border-radius: 0.375rem;
2
2
  @execution-popup-padding: 0.75rem;
3
- @execution-popup-gap: 0.625rem;
4
3
  @execution-popup-border: #e2e8f0;
5
4
  @execution-popup-text-secondary: #475569;
6
5
  @execution-popup-text-light: #64748b;
7
6
  @execution-popup-bg-light: #f8fafc;
8
- @execution-popup-warning-bg: #fffbeb;
9
- @execution-popup-warning-border: #fde68a;
10
- @execution-popup-warning-text: #78350f;
11
7
  @execution-popup-badge-optional-bg: #f1f5f9;
12
8
  @execution-popup-badge-mandatory-pending-bg: #fee2e2;
13
9
  @execution-popup-badge-mandatory-pending-text: #881337;
14
10
  @execution-popup-badge-mandatory-complete-bg: #dcfce7;
15
11
  @execution-popup-badge-mandatory-complete-text: #166534;
16
12
 
17
- .ui.modal.execution-popup-modal {
13
+ .ui.modal.true-modal.execution-popup-modal {
18
14
  width: min(48rem, calc(100vw - 2rem));
19
- min-height: 24rem;
20
- max-height: calc(100vh - 2rem);
21
- padding: 0 !important;
22
- border: 0 !important;
23
- border-radius: 1rem !important;
24
- overflow: visible;
25
- box-shadow:
26
- 0 20px 25px -5px rgba(0, 0, 0, 0.2),
27
- 0 8px 10px -6px rgba(0, 0, 0, 0.15);
15
+ min-height: min(24rem, calc(100dvh - 2rem));
28
16
 
29
- > .header {
30
- padding: 1.5rem 1.5rem 1rem;
31
- border-radius: 1rem 1rem 0 0;
32
- border-bottom: 0 !important;
33
- background: @primaryColor;
34
- color: #f8fafc;
35
-
36
- .execution-popup-header {
37
- color: #f8fafc;
38
-
39
- .subtitle {
40
- color: #cbd5e1;
41
- }
42
-
43
- .close-button {
44
- color: #e2e8f0;
45
-
46
- &:hover {
47
- color: #ffffff;
48
- }
49
- }
50
- }
51
- }
52
-
53
- > .content {
54
- display: flex;
55
- flex-direction: column;
56
- min-height: 16rem;
57
- max-height: calc(100vh - 16rem);
58
- overflow: visible;
59
- padding: 1.625rem 1.5rem 2rem;
60
-
61
- &.has-template {
62
- padding-bottom: 1.25rem;
63
- }
64
- }
65
-
66
- > .actions {
67
- display: flex;
68
- justify-content: flex-end;
69
- align-items: center;
70
- gap: 0.75rem;
71
- padding: 1rem 1.5rem;
72
- border-radius: 0 0 calc(1rem - 1px) calc(1rem - 1px);
73
- border-top: 1px solid @execution-popup-border;
74
- background: @execution-popup-bg-light;
75
-
76
- .button {
77
- margin-left: 0;
78
- }
17
+ &.has-template > .content {
18
+ padding-bottom: 1.25rem;
79
19
  }
80
- }
81
-
82
- .execution-popup-header {
83
- display: flex;
84
- justify-content: space-between;
85
- align-items: flex-start;
86
20
 
87
- .subtitle {
88
- font-size: 0.75rem;
89
- font-weight: 400;
90
- color: #94a3b8;
91
- margin: 0.15rem 0 0;
92
- }
93
-
94
- .close-button {
95
- flex-shrink: 0;
96
- padding: 6px;
97
- border: none;
98
- border-radius: 0.5rem;
99
- background: none;
100
- color: #94a3b8;
101
- cursor: pointer;
102
- font-size: 1.25rem;
103
- line-height: 1;
104
-
105
- &:hover {
106
- color: #334155;
107
- }
21
+ .execution-popup-warning {
22
+ margin: 0;
23
+ font-size: 0.875rem;
24
+ line-height: 1.625;
108
25
 
109
- &:focus-visible {
110
- outline: 2px solid @primaryColor;
111
- outline-offset: 2px;
26
+ .count {
27
+ font-weight: 700;
28
+ font-size: 1rem;
112
29
  }
113
30
  }
114
31
  }
@@ -182,7 +99,8 @@
182
99
  width: 100%;
183
100
 
184
101
  .menu {
185
- z-index: 1001;
102
+ position: static;
103
+ margin-top: 0.5rem;
186
104
  max-height: min(18rem, calc(100vh - 12rem));
187
105
  overflow-y: auto;
188
106
  }
@@ -207,38 +125,6 @@
207
125
  align-items: center;
208
126
  gap: 0.375rem;
209
127
  }
210
-
211
- .execution-popup-template-scroll {
212
- max-height: calc(100vh - 24rem);
213
- overflow-y: auto;
214
- }
215
- }
216
-
217
- .execution-popup-warning {
218
- margin-top: auto;
219
- padding: @execution-popup-padding;
220
- background: @execution-popup-warning-bg;
221
- border: 1px solid @execution-popup-warning-border;
222
- border-radius: @execution-popup-border-radius;
223
- display: flex;
224
- align-items: center;
225
- gap: @execution-popup-gap;
226
-
227
- > .icon {
228
- margin: 0;
229
- }
230
-
231
- p {
232
- font-size: 0.875rem;
233
- color: @execution-popup-warning-text;
234
- line-height: 1.625;
235
- margin: 0;
236
-
237
- .count {
238
- font-weight: 700;
239
- font-size: 1rem;
240
- }
241
- }
242
128
  }
243
129
 
244
130
  .rule-implementation-actions {