@truedat/dq 4.58.3 → 4.58.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.
@@ -0,0 +1,314 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<NewBasicRuleImplementation /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui divided grid full-height"
7
+ style=""
8
+ >
9
+ <div
10
+ class="eleven wide column"
11
+ >
12
+ <div
13
+ class="row"
14
+ >
15
+ <h2
16
+ class="ui header"
17
+ >
18
+ <div
19
+ class="content"
20
+ >
21
+ New Implementation
22
+ </div>
23
+ </h2>
24
+ <div
25
+ class="ui hidden divider"
26
+ />
27
+ </div>
28
+ <div
29
+ class="stretched row"
30
+ >
31
+ <form
32
+ class="ui form rule"
33
+ >
34
+ <div
35
+ class="field"
36
+ >
37
+ <label>
38
+ Implementation Key
39
+ <i
40
+ aria-hidden="true"
41
+ class="question circle outline icon rule-form-popup"
42
+ />
43
+ </label>
44
+ <div
45
+ class="required field"
46
+ >
47
+ <div
48
+ class="ui input"
49
+ >
50
+ <input
51
+ autocomplete="off"
52
+ name="implementation_key"
53
+ placeholder="Rule Implementation Key"
54
+ required=""
55
+ type="text"
56
+ value=""
57
+ />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div
62
+ class="field"
63
+ />
64
+ <div
65
+ class="ui segment"
66
+ >
67
+ <div
68
+ class="field"
69
+ >
70
+ <label
71
+ class="rule-form-label"
72
+ >
73
+ Result Type
74
+ <span>
75
+ *
76
+ </span>
77
+ <i
78
+ aria-hidden="true"
79
+ class="question circle outline icon rule-form-popup"
80
+ />
81
+ </label>
82
+ <div
83
+ class="inline fields"
84
+ >
85
+ <div
86
+ class="field"
87
+ >
88
+ <div
89
+ class="ui checked radio checkbox"
90
+ >
91
+ <input
92
+ checked=""
93
+ class="hidden"
94
+ name="result_type"
95
+ readonly=""
96
+ tabindex="0"
97
+ type="radio"
98
+ value="percentage"
99
+ />
100
+ <label>
101
+ Percentage
102
+ </label>
103
+ </div>
104
+ </div>
105
+ <div
106
+ class="field"
107
+ >
108
+ <div
109
+ class="ui radio checkbox"
110
+ >
111
+ <input
112
+ class="hidden"
113
+ name="result_type"
114
+ readonly=""
115
+ tabindex="0"
116
+ type="radio"
117
+ value="deviation"
118
+ />
119
+ <label>
120
+ Deviation
121
+ </label>
122
+ </div>
123
+ </div>
124
+ <div
125
+ class="field"
126
+ >
127
+ <div
128
+ class="ui radio checkbox"
129
+ >
130
+ <input
131
+ class="hidden"
132
+ name="result_type"
133
+ readonly=""
134
+ tabindex="0"
135
+ type="radio"
136
+ value="errors_number"
137
+ />
138
+ <label>
139
+ Error count
140
+ </label>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ <div
146
+ class="field"
147
+ >
148
+ <label
149
+ class="rule-form-label"
150
+ >
151
+ Threshold
152
+ <span>
153
+ *
154
+ </span>
155
+ <i
156
+ aria-hidden="true"
157
+ class="question circle outline icon rule-form-popup"
158
+ />
159
+ <div
160
+ class="ui left pointing label"
161
+ >
162
+ Required field is empty
163
+ </div>
164
+ </label>
165
+ <div
166
+ class="field"
167
+ >
168
+ <div
169
+ class="ui input"
170
+ >
171
+ <input
172
+ autocomplete="off"
173
+ name="minimum"
174
+ placeholder="Threshold value"
175
+ type="text"
176
+ value=""
177
+ />
178
+ </div>
179
+ </div>
180
+ </div>
181
+ <div
182
+ class="field"
183
+ >
184
+ <label
185
+ class="rule-form-label"
186
+ >
187
+ Goal
188
+ <span>
189
+ *
190
+ </span>
191
+ <i
192
+ aria-hidden="true"
193
+ class="question circle outline icon rule-form-popup"
194
+ />
195
+ <div
196
+ class="ui left pointing label"
197
+ >
198
+ Required field is empty
199
+ </div>
200
+ </label>
201
+ <div
202
+ class="field"
203
+ >
204
+ <div
205
+ class="ui input"
206
+ >
207
+ <input
208
+ autocomplete="off"
209
+ name="goal"
210
+ placeholder="Goal value"
211
+ type="text"
212
+ value=""
213
+ />
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ <div
219
+ class="required field"
220
+ >
221
+ <label>
222
+ Template
223
+ <div
224
+ class="ui left pointing label"
225
+ >
226
+ Empty required field
227
+ </div>
228
+ </label>
229
+ <div
230
+ class="field"
231
+ >
232
+ <div
233
+ aria-busy="false"
234
+ aria-expanded="false"
235
+ class="ui search selection dropdown"
236
+ name="template"
237
+ role="combobox"
238
+ >
239
+ <input
240
+ aria-autocomplete="list"
241
+ autocomplete="off"
242
+ class="search"
243
+ tabindex="0"
244
+ type="text"
245
+ value=""
246
+ />
247
+ <div
248
+ aria-atomic="true"
249
+ aria-live="polite"
250
+ class="divider default text"
251
+ role="alert"
252
+ >
253
+ Select a template...
254
+ </div>
255
+ <i
256
+ aria-hidden="true"
257
+ class="dropdown icon"
258
+ />
259
+ <div
260
+ class="menu transition"
261
+ role="listbox"
262
+ >
263
+ <div
264
+ aria-checked="false"
265
+ aria-selected="true"
266
+ class="selected item"
267
+ role="option"
268
+ style="pointer-events: all;"
269
+ >
270
+ <span
271
+ class="text"
272
+ >
273
+ template1
274
+ </span>
275
+ </div>
276
+ <div
277
+ aria-checked="false"
278
+ aria-selected="false"
279
+ class="item"
280
+ role="option"
281
+ style="pointer-events: all;"
282
+ >
283
+ <span
284
+ class="text"
285
+ >
286
+ template2
287
+ </span>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ <button
294
+ class="ui primary disabled right floated button"
295
+ disabled=""
296
+ tabindex="-1"
297
+ type="submit"
298
+ >
299
+ Save
300
+ </button>
301
+ <button
302
+ class="ui secondary right floated button"
303
+ >
304
+ Cancel
305
+ </button>
306
+ </form>
307
+ </div>
308
+ </div>
309
+ <div
310
+ class="five wide column"
311
+ />
312
+ </div>
313
+ </div>
314
+ `;
@@ -65,6 +65,7 @@ exports[`<RuleImplementationsActions /> matches the latest snapshot 1`] = `
65
65
  to="/implementations/new"
66
66
  />
67
67
  <RuleImplementationsOptions
68
+ canCreateBasicImplementations={false}
68
69
  canUploadResults={true}
69
70
  loading={false}
70
71
  />
@@ -0,0 +1,203 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState, useEffect } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { connect } from "react-redux";
5
+ import { useHistory } from "react-router-dom";
6
+ import { useIntl } from "react-intl";
7
+ import { useQuery } from "@apollo/client";
8
+ import { Button, Form, Icon, Popup } from "semantic-ui-react";
9
+ import { DomainSelector } from "@truedat/core/components";
10
+ import { DOMAIN_QUERY } from "@truedat/core/api/queries";
11
+ import LimitsForm from "./LimitsForm";
12
+ import { areLimitsValid } from "./limitsValidation";
13
+
14
+ const SelectableDynamicForm = React.lazy(() =>
15
+ import("@truedat/df/components/SelectableDynamicForm")
16
+ );
17
+
18
+ const DomainActionsLoader = ({ id, actions, onLoad }) => {
19
+ useQuery(DOMAIN_QUERY, {
20
+ fetchPolicy: "cache-and-network",
21
+ variables: { id, actions },
22
+ onCompleted: onLoad,
23
+ });
24
+ return null;
25
+ };
26
+
27
+ DomainActionsLoader.propTypes = {
28
+ id: PropTypes.number,
29
+ actions: PropTypes.array,
30
+ onLoad: PropTypes.func,
31
+ };
32
+
33
+ const Help = ({ message }) => {
34
+ const { formatMessage } = useIntl();
35
+ return (
36
+ <Popup
37
+ trigger={
38
+ <Icon className="rule-form-popup" name="question circle outline" />
39
+ }
40
+ content={formatMessage({ id: message })}
41
+ on="click"
42
+ hideOnScroll
43
+ />
44
+ );
45
+ };
46
+
47
+ Help.propTypes = {
48
+ message: PropTypes.string,
49
+ };
50
+
51
+ export const RuleImplementationBasicForm = ({
52
+ implementationKey,
53
+ isSubmitting,
54
+ onChange,
55
+ onSubmit,
56
+ rule,
57
+ ruleImplementation,
58
+ setImplementationKey,
59
+ }) => {
60
+ const { formatMessage } = useIntl();
61
+ const history = useHistory();
62
+
63
+ const domainActions = ["publishImplementation"];
64
+
65
+ const [isContentValid, setIsContentValid] = useState();
66
+
67
+ const [domains, setDomains] = useState();
68
+ const [canPublish, setCanPublish] = useState(false);
69
+
70
+ useEffect(() => {
71
+ const currentDomainActions = _.flow(
72
+ _.find((domain) => domain.id === String(ruleImplementation.domain_id)),
73
+ _.pathOr([], "actions")
74
+ )(domains);
75
+
76
+ _.flow(
77
+ _.any((action) => action == "publishImplementation"),
78
+ setCanPublish
79
+ )(currentDomainActions);
80
+
81
+ return () => {
82
+ setCanPublish(false);
83
+ };
84
+ }, [domains, ruleImplementation.domain_id]);
85
+
86
+ const handleContentChange = ({ content, valid }) => {
87
+ onChange("dfContent", content);
88
+ setIsContentValid(_.isEmpty(valid));
89
+ };
90
+
91
+ const doSubmit = (params) => {
92
+ if (isValidForm()) {
93
+ onSubmit(params);
94
+ }
95
+ };
96
+
97
+ const isValidForm = () =>
98
+ ruleImplementation?.dfName &&
99
+ !_.isEmpty(ruleImplementation?.dfName) &&
100
+ isContentValid &&
101
+ (!_.isEmpty(rule) || _.prop("domain_id")(ruleImplementation)) &&
102
+ areLimitsValid(ruleImplementation);
103
+
104
+ const doCancel = () => history.goBack();
105
+
106
+ const domainId = ruleImplementation?.domain_id || rule?.domain_id;
107
+
108
+ return (
109
+ <Form className="rule">
110
+ <Form.Field>
111
+ <label>
112
+ {formatMessage({ id: "ruleImplementation.props.name" })}
113
+ <Help message="datasetForm.implementation_key.tooltip" />
114
+ </label>
115
+ <Form.Input
116
+ autoComplete="off"
117
+ name="implementation_key"
118
+ onChange={(_e, { value }) => setImplementationKey(value)}
119
+ placeholder={formatMessage({
120
+ id: "ruleImplementation.props.name.placeholder",
121
+ })}
122
+ required
123
+ value={implementationKey}
124
+ />
125
+ </Form.Field>
126
+ {_.isEmpty(rule) && !ruleImplementation?.rule_id ? (
127
+ <Form.Field>
128
+ <DomainSelector
129
+ action={
130
+ ruleImplementation.rule_id
131
+ ? "manageBasicImplementations"
132
+ : "manageBasicRulelessImplementations"
133
+ }
134
+ value={domainId}
135
+ domainActions={domainActions}
136
+ onLoad={(data) => setDomains(data.domains)}
137
+ onChange={(_e, { value }) => onChange("domain_id", value)}
138
+ labels
139
+ />
140
+ </Form.Field>
141
+ ) : (
142
+ <DomainActionsLoader
143
+ id={domainId}
144
+ actions={domainActions}
145
+ onLoad={(data) => setDomains([data.domain])}
146
+ />
147
+ )}
148
+ <LimitsForm onChange={onChange} ruleImplementation={ruleImplementation} />
149
+ <SelectableDynamicForm
150
+ scope="ri"
151
+ domainIds={_.isNil(domainId) ? null : [domainId]}
152
+ required
153
+ content={ruleImplementation?.dfContent}
154
+ name={ruleImplementation?.dfName}
155
+ onChange={handleContentChange}
156
+ onNameChange={(dfName) => onChange("dfName", dfName)}
157
+ />
158
+ {canPublish ? (
159
+ <Button
160
+ floated="right"
161
+ disabled={!isValidForm()}
162
+ type="submit"
163
+ primary
164
+ loading={isSubmitting}
165
+ onClick={() => doSubmit({ status: "published" })}
166
+ content={formatMessage({ id: "actions.publish" })}
167
+ />
168
+ ) : null}
169
+ <Button
170
+ floated="right"
171
+ disabled={!isValidForm()}
172
+ type="submit"
173
+ primary
174
+ loading={isSubmitting}
175
+ onClick={() => doSubmit({ status: "draft" })}
176
+ content={formatMessage({ id: "actions.save" })}
177
+ />
178
+ <Button
179
+ content={formatMessage({ id: "actions.cancel" })}
180
+ floated="right"
181
+ onClick={() => doCancel()}
182
+ secondary
183
+ />
184
+ </Form>
185
+ );
186
+ };
187
+
188
+ RuleImplementationBasicForm.propTypes = {
189
+ implementationKey: PropTypes.string,
190
+ isSubmitting: PropTypes.bool,
191
+ onChange: PropTypes.func,
192
+ onSubmit: PropTypes.func.isRequired,
193
+ rule: PropTypes.object,
194
+ ruleImplementation: PropTypes.object,
195
+ setImplementationKey: PropTypes.func,
196
+ };
197
+
198
+ const mapStateToProps = ({ rule, ruleImplementationCreating }) => ({
199
+ isSubmitting: ruleImplementationCreating,
200
+ rule,
201
+ });
202
+
203
+ export default connect(mapStateToProps)(RuleImplementationBasicForm);
@@ -0,0 +1,136 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { render } from "@truedat/test/render";
6
+ import { DOMAIN_QUERY, DOMAINS_QUERY } from "@truedat/core/api/queries";
7
+ import { multipleTemplatesMock } from "@truedat/test/mocks";
8
+ import { RuleImplementationBasicForm } from "../RuleImplementationBasicForm";
9
+
10
+ jest.setTimeout(30000);
11
+
12
+ const domains = [
13
+ { id: 1, name: "domain1", actions: ["publishImplementation"] },
14
+ ];
15
+ const domainsMock = {
16
+ request: { query: DOMAINS_QUERY },
17
+ result: { data: { domains: domains } },
18
+ };
19
+
20
+ const domainMock = {
21
+ request: { query: DOMAIN_QUERY },
22
+ result: { data: { domain: domains } },
23
+ };
24
+
25
+ const renderOpts = {
26
+ mocks: [
27
+ multipleTemplatesMock({ scope: "ri", domainIds: [1] }),
28
+ domainMock,
29
+ domainsMock,
30
+ ],
31
+ state: {
32
+ rule: { domain_id: 1 },
33
+ ruleImplementationCreating: false,
34
+ },
35
+ fallback: "lazy",
36
+ };
37
+
38
+ const props = {
39
+ onChange: jest.fn(),
40
+ onSubmit: jest.fn(),
41
+ implementationKey: "",
42
+ isSubmitting: false,
43
+ ruleImplementation: {
44
+ id: 1,
45
+ goal: "10",
46
+ minimum: "1",
47
+ result_type: "percentage",
48
+ },
49
+ };
50
+
51
+ describe("<RuleImplementationBasicForm />", () => {
52
+ it("matches the latest snapshot", async () => {
53
+ const updatedProps = _.flow(_.set("ruleImplementation.domain_id", 1))(
54
+ props
55
+ );
56
+ const { container, queryByText } = render(
57
+ <RuleImplementationBasicForm {...updatedProps} />,
58
+ renderOpts
59
+ );
60
+ await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument(), {
61
+ timeout: 10000,
62
+ });
63
+ await waitFor(() =>
64
+ expect(queryByText(/loading/i)).not.toBeInTheDocument()
65
+ );
66
+ expect(container).toMatchSnapshot();
67
+ });
68
+
69
+ it("Implementation_key field is editable with status published", () => {
70
+ const updatedProps = _.flow(
71
+ _.set("ruleImplementation.domain_id", 1),
72
+ _.set("ruleImplementation.status", "published"),
73
+ _.set("ruleImplementation.implementation_key", "implementation_test")
74
+ )(props);
75
+ const { queryByPlaceholderText } = render(
76
+ <RuleImplementationBasicForm {...updatedProps} />,
77
+ renderOpts
78
+ );
79
+ expect(
80
+ queryByPlaceholderText("Rule Implementation Key")
81
+ ).not.toBeDisabled();
82
+ });
83
+
84
+ it("submit button enabled if there is valid content in form without rule", async () => {
85
+ const updatedProps = _.flow(
86
+ _.set("ruleImplementation.domain_id", 1),
87
+ _.set("ruleImplementation.dfName", "template1"),
88
+ _.set("ruleImplementation.dfContent", { field1: "foo" })
89
+ )(props);
90
+ const customProps = {
91
+ ...updatedProps,
92
+ };
93
+
94
+ const { getByRole } = render(
95
+ <RuleImplementationBasicForm {...customProps} />,
96
+ renderOpts
97
+ );
98
+ await waitFor(() => {
99
+ expect(getByRole("button", { name: "Save" })).toBeEnabled();
100
+ });
101
+ });
102
+
103
+ it("submit button enabled if there is valid content in form with rule", async () => {
104
+ const updatedProps = _.flow(
105
+ _.set("ruleImplementation.domain_id", 1),
106
+ _.set("ruleImplementation.dfName", "template1"),
107
+ _.set("ruleImplementation.dfContent", { field1: "foo" })
108
+ )(props);
109
+ const customProps = {
110
+ ...updatedProps,
111
+ rule: { id: 5, name: "regla" },
112
+ };
113
+
114
+ const { findByRole } = render(
115
+ <RuleImplementationBasicForm {...customProps} />,
116
+ renderOpts
117
+ );
118
+ expect(await findByRole("button", { name: /save/i })).toBeEnabled();
119
+ });
120
+
121
+ it("submit button disabled if there is invalid content in form", async () => {
122
+ const updatedProps = _.flow(
123
+ _.set("ruleImplementation.domain_id", 1),
124
+ _.set("ruleImplementation.dfName", "template1"),
125
+ _.set("ruleImplementation.dfContent", {})
126
+ )(props);
127
+ const customProps = {
128
+ ...updatedProps,
129
+ };
130
+ const { queryByRole } = render(
131
+ <RuleImplementationBasicForm {...customProps} />,
132
+ renderOpts
133
+ );
134
+ expect(queryByRole("button", { name: /save/i })).toBeDisabled();
135
+ });
136
+ });