@truedat/dq 4.47.4 → 4.47.5

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.
@@ -1,9 +1,21 @@
1
+ import _ from "lodash/fp";
1
2
  import React from "react";
2
- import { waitFor } from "@testing-library/react";
3
+ import { waitFor, within } from "@testing-library/react";
3
4
  import { render } from "@truedat/test/render";
4
5
  import { multipleTemplatesMock } from "@truedat/test/mocks";
5
- import { NewRuleImplementation } from "../NewRuleImplementation";
6
+ import userEvent from "@testing-library/user-event";
7
+ import defaultMessages from "@truedat/test/messages";
8
+ import {
9
+ NewRuleImplementation,
10
+ SOURCES_WITH_CONFIG,
11
+ } from "../NewRuleImplementation";
12
+ import NewRuleImplementationLoader from "../NewRuleImplementation";
6
13
  import { newRuleImplementationProps } from "../__test_samples__/NewRuleImplementationProps";
14
+ import {
15
+ ruleImplementationLoaderGlobalState,
16
+ ruleImplementationLoaderActions,
17
+ sourcesRuleImplementationLoader,
18
+ } from "../__test_samples__/newRuleImplementationLoader";
7
19
 
8
20
  describe("<NewRuleImplementation />", () => {
9
21
  const createRuleImplementation = jest.fn();
@@ -57,3 +69,270 @@ describe("<NewRuleImplementation />", () => {
57
69
  expect(container).toMatchSnapshot();
58
70
  });
59
71
  });
72
+
73
+ describe("<NewRuleImplementationLoader> NewRuleImplementation doSubmit", () => {
74
+ const dispatch = jest.fn();
75
+
76
+ const sourcesWithConfigMock = {
77
+ request: {
78
+ query: SOURCES_WITH_CONFIG,
79
+ variables: { jobTypes: "quality" },
80
+ },
81
+ result: { data: { sources: sourcesRuleImplementationLoader } },
82
+ };
83
+
84
+ const renderOptsImplementationEdit = {
85
+ mocks: [
86
+ multipleTemplatesMock({
87
+ scope: "ri",
88
+ domainIds: [2],
89
+ }),
90
+ sourcesWithConfigMock,
91
+ ],
92
+ messages: {
93
+ en: {
94
+ ...defaultMessages.en,
95
+ // This is in td-web
96
+ "filtersGrid.field.modifier.cast_as_date": "Cast as date",
97
+ },
98
+ },
99
+ state: ruleImplementationLoaderGlobalState,
100
+ fallback: "lazy",
101
+ dispatch,
102
+ };
103
+
104
+ const props = {
105
+ ...ruleImplementationLoaderActions,
106
+ edition: true,
107
+ };
108
+
109
+ const expectedRuleImplementationBase = {
110
+ dataset: [
111
+ {
112
+ alias: { index: 4, text: null },
113
+ clauses: [],
114
+ structure: { id: 11127104 },
115
+ },
116
+ ],
117
+ df_content: {},
118
+ df_name: "template1",
119
+ domain_id: 2,
120
+ executable: true,
121
+ goal: 90,
122
+ id: 1329 + 1,
123
+ implementation_key: "oracle_prueba",
124
+ implementation_type: "default",
125
+ minimum: 80,
126
+ populations: [],
127
+ result_type: "percentage",
128
+ rule_id: undefined,
129
+ segments: [],
130
+ status: undefined,
131
+ };
132
+
133
+ it("doSubmit: is empty operator", async () => {
134
+ const expectedRuleImplementation = {
135
+ ...expectedRuleImplementationBase,
136
+ validations: [
137
+ {
138
+ modifier: null,
139
+ operator: { name: "empty" },
140
+ population: [],
141
+ structure: { id: 11127109, parent_index: 4 },
142
+ value: [],
143
+ value_modifier: [],
144
+ },
145
+ ],
146
+ };
147
+
148
+ const { queryByText, getByRole, findByRole, findByTestId } = render(
149
+ <NewRuleImplementationLoader {...props} />,
150
+ renderOptsImplementationEdit
151
+ );
152
+
153
+ // Information Form
154
+
155
+ await waitFor(() => {
156
+ expect(queryByText(/Template/)).toBeTruthy();
157
+ });
158
+
159
+ userEvent.click(await findByRole("option", { name: "template1" }));
160
+
161
+ await waitFor(() => {
162
+ expect(getByRole("button", { name: "Next" })).toBeEnabled();
163
+ });
164
+ userEvent.click(await getByRole("button", { name: "Next" }));
165
+
166
+ // Dataset Form
167
+
168
+ await waitFor(() => {
169
+ expect(getByRole("button", { name: "Next" })).toBeEnabled();
170
+ });
171
+ userEvent.click(await getByRole("button", { name: "Next" }));
172
+
173
+ // Population Form
174
+
175
+ await waitFor(() => {
176
+ expect(getByRole("button", { name: "Next" })).toBeEnabled();
177
+ });
178
+ userEvent.click(await getByRole("button", { name: "Next" }));
179
+
180
+ // Validations Form
181
+
182
+ userEvent.click(await findByRole("button", { name: "add-condition-row" }));
183
+
184
+ const row = await findByTestId("row-0");
185
+
186
+ const field = row.querySelector('div[label="Field"]');
187
+ const operator = row.querySelector('div[label="Operator"]');
188
+
189
+ userEvent.click(field);
190
+ const fieldOptions = await within(field).findByRole("listbox");
191
+ const emailFieldOption = within(fieldOptions).getByText("EMAIL");
192
+ userEvent.click(emailFieldOption);
193
+ const selectedField = fieldOptions.querySelector(".selected>span");
194
+ expect(within(selectedField).queryByText(/EMAIL/)).toBeInTheDocument();
195
+
196
+ userEvent.click(operator);
197
+ const operatorOptions = await within(operator).findByRole("listbox");
198
+ const emptyOperatorOption = within(operatorOptions).getByText("is empty");
199
+ userEvent.click(emptyOperatorOption);
200
+ const selectedOperator = operatorOptions.querySelector(".selected>span");
201
+ expect(
202
+ within(selectedOperator).queryByText(/is empty/)
203
+ ).toBeInTheDocument();
204
+
205
+ await waitFor(() => {
206
+ expect(getByRole("button", { name: "Save" })).toBeEnabled();
207
+ });
208
+
209
+ userEvent.click(await getByRole("button", { name: "Save" }));
210
+
211
+ expect(dispatch).toHaveBeenCalledWith({
212
+ ...ruleImplementationLoaderActions.updateRuleImplementation(),
213
+ payload: { rule_implementation: expectedRuleImplementation },
214
+ });
215
+ });
216
+
217
+ it("doSubmit: unique across fields operator", async () => {
218
+ const expectedRuleImplementation = {
219
+ ...expectedRuleImplementationBase,
220
+ validations: [
221
+ {
222
+ modifier: null,
223
+ operator: { name: "unique", value_type: "field_list" },
224
+ population: [],
225
+ structure: { id: 11127109, parent_index: 4 },
226
+ value: [
227
+ {
228
+ fields: [
229
+ {
230
+ id: 11127109,
231
+ name: "EMAIL",
232
+ parent_index: 4,
233
+ path: undefined,
234
+ },
235
+ {
236
+ id: 11127116,
237
+ name: "PHONE_NUMBER",
238
+ parent_index: 4,
239
+ path: undefined,
240
+ },
241
+ ],
242
+ },
243
+ ],
244
+ value_modifier: [],
245
+ },
246
+ ],
247
+ };
248
+
249
+ const { queryByText, getByRole, findByRole, findByTestId } = render(
250
+ <NewRuleImplementationLoader {...props} />,
251
+ renderOptsImplementationEdit
252
+ );
253
+
254
+ // Information Form
255
+
256
+ await waitFor(() => {
257
+ expect(queryByText(/Template/)).toBeTruthy();
258
+ });
259
+
260
+ userEvent.click(await findByRole("option", { name: "template1" }));
261
+
262
+ await waitFor(() => {
263
+ expect(getByRole("button", { name: "Next" })).toBeEnabled();
264
+ });
265
+ userEvent.click(await getByRole("button", { name: "Next" }));
266
+
267
+ // Dataset Form
268
+
269
+ await waitFor(() => {
270
+ expect(getByRole("button", { name: "Next" })).toBeEnabled();
271
+ });
272
+ userEvent.click(await getByRole("button", { name: "Next" }));
273
+
274
+ // Population Form
275
+
276
+ await waitFor(() => {
277
+ expect(getByRole("button", { name: "Next" })).toBeEnabled();
278
+ });
279
+ userEvent.click(await getByRole("button", { name: "Next" }));
280
+
281
+ // Validations Form
282
+
283
+ userEvent.click(await findByRole("button", { name: "add-condition-row" }));
284
+
285
+ const row = await findByTestId("row-0");
286
+
287
+ const field = row.querySelector('div[label="Field"]');
288
+ const operator = row.querySelector('div[label="Operator"]');
289
+
290
+ userEvent.click(field);
291
+ const fieldOptions = await within(field).findByRole("listbox");
292
+ const emailFieldOption = within(fieldOptions).getByText("EMAIL");
293
+ userEvent.click(emailFieldOption);
294
+ const selectedField = fieldOptions.querySelector(".selected>span");
295
+ expect(within(selectedField).queryByText(/EMAIL/)).toBeInTheDocument();
296
+
297
+ userEvent.click(operator);
298
+ const operatorOptions = await within(operator).findByRole("listbox");
299
+ const uniqueAcrossFieldsOperatorOption = within(operatorOptions).getByText(
300
+ "unique across fields"
301
+ );
302
+ userEvent.click(uniqueAcrossFieldsOperatorOption);
303
+ const selectedOperator = operatorOptions.querySelector(".selected>span");
304
+ expect(
305
+ within(selectedOperator).queryByText(/unique across fields/)
306
+ ).toBeInTheDocument();
307
+
308
+ const updatedRow = await findByTestId("row-0");
309
+ const value = updatedRow.querySelector('div[label="Value"]');
310
+ userEvent.click(value);
311
+ const valueOptions = await within(value).findByRole("listbox");
312
+ const valueEmailFieldOption = within(valueOptions).getByText("EMAIL");
313
+ const valuePhoneNumberFieldOption =
314
+ within(valueOptions).getByText("PHONE_NUMBER");
315
+
316
+ userEvent.click(valueEmailFieldOption);
317
+ userEvent.click(valuePhoneNumberFieldOption);
318
+ const selectedFieldValues = updatedRow.querySelectorAll(".label");
319
+
320
+ const selectedFieldTexts = [...selectedFieldValues].map(
321
+ (selectedFieldValue) => selectedFieldValue.text
322
+ );
323
+
324
+ expect(_.difference(selectedFieldTexts, ["EMAIL", "PHONE_NUMBER"])).toEqual(
325
+ []
326
+ );
327
+
328
+ await waitFor(() => {
329
+ expect(getByRole("button", { name: "Save" })).toBeEnabled();
330
+ });
331
+ userEvent.click(await getByRole("button", { name: "Save" }));
332
+
333
+ expect(dispatch).toHaveBeenCalledWith({
334
+ ...ruleImplementationLoaderActions.updateRuleImplementation(),
335
+ payload: { rule_implementation: expectedRuleImplementation },
336
+ });
337
+ });
338
+ });
@@ -48,6 +48,19 @@ export const FiltersField = ({
48
48
  const { value_type, value_type_filter, fixed_values } = operator;
49
49
 
50
50
  const modifierDef = _.find({ name: modifier?.name })(typeCastModifiers);
51
+ const pickFromValue = _.pick(["data_structure_id", "name", "parent_index"]);
52
+
53
+ const getVal = (value) =>
54
+ _.isNil(_.prop("parent_index")(value))
55
+ ? _.prop("id")(value)
56
+ : `${_.prop("id")(value)}/${_.prop("parent_index")(value)}`;
57
+
58
+ const getValue = (valueOrValues, operator) =>
59
+ operator?.value_type === "field_list"
60
+ ? (valueOrValues?.fields || []).map((v) => {
61
+ return getVal(v);
62
+ })
63
+ : getVal(valueOrValues);
51
64
 
52
65
  switch (value_type) {
53
66
  case "string":
@@ -89,6 +102,7 @@ export const FiltersField = ({
89
102
  case "timestamp":
90
103
  return <DateTimeField label={label} value={value} onChange={onChange} />;
91
104
  case "field":
105
+ case "field_list":
92
106
  const structureFields = getStructureFields(parentStructures);
93
107
  return value_type_filter == "any" ? (
94
108
  <StructureSelectorInputField
@@ -111,24 +125,30 @@ export const FiltersField = ({
111
125
  ) : (
112
126
  <>
113
127
  <StructureFieldsDropdown
128
+ multiple={value_type === "field_list"}
114
129
  label={label}
115
130
  inline={false}
116
131
  parentStructures={parentStructures}
117
132
  structureFields={structureFields}
118
133
  typeCastModifiers={typeCastModifiers}
119
- filters={{ field_type: [fieldType] }}
120
- onSelectField={(value, modifier) =>
134
+ filters={
135
+ value_type === "field_list" ? null : { field_type: [fieldType] }
136
+ }
137
+ onSelectField={(value, modifier) => {
121
138
  onChange(
122
139
  null,
123
- _.pick(["data_structure_id", "name", "parent_index"])(value),
140
+ pickFromValue(value),
124
141
  modifier ? { name: modifier.name } : null
125
- )
126
- }
127
- value={
128
- _.isNil(_.prop("parent_index")(value))
129
- ? _.prop("id")(value)
130
- : `${_.prop("id")(value)}/${_.prop("parent_index")(value)}`
131
- }
142
+ );
143
+ }}
144
+ onSelectFields={(values) => {
145
+ onChange(
146
+ null,
147
+ values.map((value) => pickFromValue(value)),
148
+ null
149
+ );
150
+ }}
151
+ value={getValue(value, operator)}
132
152
  />
133
153
  {modifier && (
134
154
  <FieldModifier
@@ -158,7 +178,7 @@ FiltersField.propTypes = {
158
178
  parentStructures: PropTypes.array,
159
179
  operator: PropTypes.object,
160
180
  fieldType: PropTypes.string,
161
- value: PropTypes.string,
181
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
162
182
  name: PropTypes.string,
163
183
  onChange: PropTypes.func,
164
184
  modifier: PropTypes.object,
@@ -162,7 +162,7 @@ FiltersFormGroup.propTypes = {
162
162
  parentStructures: PropTypes.array,
163
163
  siblings: PropTypes.array,
164
164
  scope: PropTypes.string,
165
- operators: PropTypes.array,
165
+ operators: PropTypes.object,
166
166
  };
167
167
 
168
168
  export default FiltersFormGroup;
@@ -56,13 +56,21 @@ export const FiltersGrid = ({
56
56
  });
57
57
  };
58
58
 
59
+ const composeFieldValue = (value) => ({
60
+ id: value.data_structure_id || value.id,
61
+ name: value.name,
62
+ path: value.path,
63
+ parent_index: value.parent_index,
64
+ });
65
+
59
66
  const composeValue = (value_type, value) => {
60
67
  if (value_type == "field") {
68
+ return composeFieldValue(value);
69
+ } else if (value_type === "field_list") {
61
70
  return {
62
- id: value.data_structure_id || value.id,
63
- name: value.name,
64
- path: value.path,
65
- parent_index: value.parent_index,
71
+ fields: _.map((v) => {
72
+ return composeFieldValue(v);
73
+ })(value),
66
74
  };
67
75
  } else {
68
76
  return { raw: value };
@@ -151,7 +159,11 @@ export const FiltersGrid = ({
151
159
  scope={scope}
152
160
  />
153
161
  </Grid>
154
- <Button onClick={onRowAddition} icon="plus circle" />
162
+ <Button
163
+ aria-label="add-condition-row"
164
+ onClick={onRowAddition}
165
+ icon="plus circle"
166
+ />
155
167
  </>
156
168
  );
157
169
  };
@@ -161,7 +173,7 @@ FiltersGrid.propTypes = {
161
173
  setRowValue: PropTypes.func,
162
174
  scope: PropTypes.string,
163
175
  structures: PropTypes.array,
164
- typeOperators: PropTypes.array,
176
+ typeOperators: PropTypes.object,
165
177
  };
166
178
 
167
179
  export default FiltersGrid;
@@ -50,7 +50,7 @@ const Filter = ({
50
50
  _.filter((v) => !_.isNil(v?.id))(row?.value || []);
51
51
  return (
52
52
  <>
53
- <Grid.Row>
53
+ <Grid.Row data-testid={`row-${index}`}>
54
54
  <Grid.Column width={14}>
55
55
  <FiltersFormGroup
56
56
  clause={clause}
@@ -126,7 +126,7 @@ const Filter = ({
126
126
  Filter.propTypes = {
127
127
  activeConditionIndex: PropTypes.number,
128
128
  allOperators: PropTypes.array,
129
- operators: PropTypes.array,
129
+ operators: PropTypes.object,
130
130
  clause: PropTypes.object,
131
131
  composeValue: PropTypes.func,
132
132
  onConditionChange: PropTypes.func,
@@ -182,7 +182,7 @@ export const FiltersGroup = ({
182
182
  FiltersGroup.propTypes = {
183
183
  activeConditionIndex: PropTypes.number,
184
184
  allOperators: PropTypes.array,
185
- operators: PropTypes.array,
185
+ operators: PropTypes.object,
186
186
  composeValue: PropTypes.func,
187
187
  onConditionChange: PropTypes.func,
188
188
  onOperatorChange: PropTypes.func,
@@ -25,6 +25,7 @@ export const InformationForm = ({
25
25
 
26
26
  const handleContentChange = ({ content, valid }) => {
27
27
  onChange("dfContent", content);
28
+
28
29
  setIsValid(_.isEmpty(valid));
29
30
  };
30
31
  const domainId = ruleImplementation?.domain_id || rule?.domain_id;
@@ -64,6 +65,7 @@ export const InformationForm = ({
64
65
  autoComplete="off"
65
66
  />
66
67
  </Form.Field>
68
+
67
69
  {_.isEmpty(rule) && !ruleImplementation?.rule_id ? (
68
70
  <Form.Field>
69
71
  <DomainDropdownSelector
@@ -350,7 +350,7 @@ RuleImplementationForm.propTypes = {
350
350
  addSegments: PropTypes.func,
351
351
  authManageSegments: PropTypes.bool,
352
352
  isAdmin: PropTypes.bool,
353
- isSubmitting: PropTypes.bool.isRequired,
353
+ isSubmitting: PropTypes.bool,
354
354
  onChange: PropTypes.func,
355
355
  onSubmit: PropTypes.func.isRequired,
356
356
  operators: PropTypes.object,
@@ -50,7 +50,13 @@ exports[`<FiltersFormGroup /> matches the latest snapshot 1`] = `
50
50
  <div
51
51
  class="menu transition"
52
52
  role="listbox"
53
- />
53
+ >
54
+ <div
55
+ class="message"
56
+ >
57
+ No results found.
58
+ </div>
59
+ </div>
54
60
  </div>
55
61
  </div>
56
62
  <div
@@ -4,6 +4,7 @@ exports[`<FiltersGroup /> matches the latest snapshot when siblings provided 1`]
4
4
  <div>
5
5
  <div
6
6
  class="row"
7
+ data-testid="row-0"
7
8
  style="display: none;"
8
9
  >
9
10
  <div
@@ -56,7 +57,13 @@ exports[`<FiltersGroup /> matches the latest snapshot when siblings provided 1`]
56
57
  <div
57
58
  class="menu transition"
58
59
  role="listbox"
59
- />
60
+ >
61
+ <div
62
+ class="message"
63
+ >
64
+ No results found.
65
+ </div>
66
+ </div>
60
67
  </div>
61
68
  </div>
62
69
  <div
@@ -56,7 +56,13 @@ exports[`<ValueConditions /> matches the latest snapshot when siblings provided
56
56
  <div
57
57
  class="menu transition"
58
58
  role="listbox"
59
- />
59
+ >
60
+ <div
61
+ class="message"
62
+ >
63
+ No results found.
64
+ </div>
65
+ </div>
60
66
  </div>
61
67
  </div>
62
68
  <div
@@ -423,7 +423,8 @@ export default {
423
423
  "matches regular expression",
424
424
  "ruleImplementation.operator.starts_with": "starts with",
425
425
  "ruleImplementation.operator.starts_with.string": "starts with",
426
- "ruleImplementation.operator.unique": "has unique value",
426
+ "ruleImplementation.operator.unique": "unique",
427
+ "ruleImplementation.operator.unique.field_list": "unique across fields",
427
428
  "ruleImplementation.operator.variation_on_count": "count variation",
428
429
  "ruleImplementation.operator.variation_on_count.string": "count variation",
429
430
  "ruleImplementation.props.esquema": "Structure",
@@ -435,7 +435,8 @@ export default {
435
435
  "ruleImplementation.operator.regex_format": "cumple la expresión regular",
436
436
  "ruleImplementation.operator.starts_with.string": "empieza por",
437
437
  "ruleImplementation.operator.starts_with": "empieza por",
438
- "ruleImplementation.operator.unique": "tiene valor único",
438
+ "ruleImplementation.operator.unique": "único",
439
+ "ruleImplementation.operator.unique.field_list": "único en conjunto",
439
440
  "ruleImplementation.operator.variation_on_count.string": "variación conteo",
440
441
  "ruleImplementation.operator.variation_on_count": "variación conteo",
441
442
  "ruleImplementation.props.esquema.placeholder": "añade una estructura",
@@ -5,6 +5,7 @@ const defaultOperators = {
5
5
  any: {
6
6
  operators: [
7
7
  { name: "unique", scope: "validation" },
8
+ { name: "unique", value_type: "field_list" },
8
9
  { name: "not_empty" },
9
10
  { name: "empty" },
10
11
  {