@truedat/qx 8.5.1 → 8.5.2

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.
Files changed (52) hide show
  1. package/package.json +3 -3
  2. package/src/components/common/expressions/Clauses.js +5 -9
  3. package/src/components/common/expressions/Condition.js +36 -37
  4. package/src/components/common/expressions/__tests__/Clauses.spec.js +87 -0
  5. package/src/components/common/expressions/__tests__/Condition.spec.js +220 -1
  6. package/src/components/common/expressions/__tests__/__snapshots__/Clauses.spec.js.snap +22 -21
  7. package/src/components/common/expressions/__tests__/__snapshots__/Condition.spec.js.snap +212 -214
  8. package/src/components/dataViews/queryableProperties/GroupBy.js +4 -2
  9. package/src/components/dataViews/queryableProperties/__tests__/GroupBy.spec.js +134 -28
  10. package/src/components/qualityControls/ControlProperties.js +32 -16
  11. package/src/components/qualityControls/ControlPropertiesForm.js +3 -3
  12. package/src/components/qualityControls/ControlPropertiesSummary.js +127 -104
  13. package/src/components/qualityControls/ControlPropertiesView.js +48 -74
  14. package/src/components/qualityControls/InformationForm.js +181 -180
  15. package/src/components/qualityControls/ScoreCriteria.js +0 -4
  16. package/src/components/qualityControls/ScoreCriteriaView.js +6 -9
  17. package/src/components/qualityControls/__tests__/ControlProperties.spec.js +47 -16
  18. package/src/components/qualityControls/__tests__/ControlPropertiesForm.spec.js +100 -0
  19. package/src/components/qualityControls/__tests__/ControlPropertiesSummary.spec.js +141 -0
  20. package/src/components/qualityControls/__tests__/ControlPropertiesView.spec.js +102 -11
  21. package/src/components/qualityControls/__tests__/EditQualityControl.spec.js +27 -3
  22. package/src/components/qualityControls/__tests__/InformationForm.spec.js +342 -0
  23. package/src/components/qualityControls/__tests__/NewDraftQualityControl.spec.js +26 -6
  24. package/src/components/qualityControls/__tests__/NewQualityControl.spec.js +66 -20
  25. package/src/components/qualityControls/__tests__/QualityBadge.spec.js +30 -3
  26. package/src/components/qualityControls/__tests__/QualityControlEditor.spec.js +282 -45
  27. package/src/components/qualityControls/__tests__/ScoreCriteria.spec.js +25 -3
  28. package/src/components/qualityControls/__tests__/ScoreCriteriaView.spec.js +19 -3
  29. package/src/components/qualityControls/__tests__/__fixtures__/qualityControlHelper.js +1 -1
  30. package/src/components/qualityControls/__tests__/__snapshots__/ControlProperties.spec.js.snap +13 -1
  31. package/src/components/qualityControls/__tests__/__snapshots__/ControlPropertiesView.spec.js.snap +70 -40
  32. package/src/components/qualityControls/__tests__/__snapshots__/EditQualityControl.spec.js.snap +118 -132
  33. package/src/components/qualityControls/__tests__/__snapshots__/NewDraftQualityControl.spec.js.snap +118 -132
  34. package/src/components/qualityControls/__tests__/__snapshots__/NewQualityControl.spec.js.snap +0 -13
  35. package/src/components/qualityControls/__tests__/__snapshots__/QualityBadge.spec.js.snap +15 -5
  36. package/src/components/qualityControls/__tests__/__snapshots__/QualityControlEditor.spec.js.snap +488 -125
  37. package/src/components/qualityControls/__tests__/__snapshots__/ScoreCriteria.spec.js.snap +13 -5
  38. package/src/components/qualityControls/__tests__/qualityByControlMode.spec.js +65 -14
  39. package/src/components/qualityControls/controlProperties/{Ratio.js → ResourceWithValidation.js} +19 -10
  40. package/src/components/qualityControls/controlProperties/__tests__/ResourceWithValidation.spec.js +192 -0
  41. package/src/components/qualityControls/qualityByControlMode.js +6 -30
  42. package/src/components/qualityControls/scoreCriterias/ErrorCount.js +0 -1
  43. package/src/components/qualityControls/scoreCriterias/__tests__/ErrorCount.spec.js +4 -4
  44. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/GroupBy.spec.js.snap +0 -604
  45. package/src/components/qualityControls/controlProperties/Count.js +0 -56
  46. package/src/components/qualityControls/controlProperties/__tests__/Count.spec.js +0 -66
  47. package/src/components/qualityControls/controlProperties/__tests__/Ratio.spec.js +0 -95
  48. package/src/components/qualityControls/controlProperties/__tests__/__snapshots__/Count.spec.js.snap +0 -67
  49. package/src/components/qualityControls/controlProperties/__tests__/__snapshots__/Ratio.spec.js.snap +0 -161
  50. package/src/components/qualityControls/scoreCriterias/Count.js +0 -88
  51. package/src/components/qualityControls/scoreCriterias/__tests__/Count.spec.js +0 -62
  52. package/src/components/qualityControls/scoreCriterias/__tests__/__snapshots__/Count.spec.js.snap +0 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/qx",
3
- "version": "8.5.1",
3
+ "version": "8.5.2",
4
4
  "description": "Truedat Web Quality Experience package",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -56,7 +56,7 @@
56
56
  "@testing-library/jest-dom": "^6.6.3",
57
57
  "@testing-library/react": "^16.3.0",
58
58
  "@testing-library/user-event": "^14.6.1",
59
- "@truedat/test": "8.5.1",
59
+ "@truedat/test": "8.5.2",
60
60
  "identity-obj-proxy": "^3.0.0",
61
61
  "jest": "^29.7.0",
62
62
  "redux-saga-test-plan": "^4.0.6"
@@ -89,5 +89,5 @@
89
89
  "semantic-ui-react": "^3.0.0-beta.2",
90
90
  "swr": "^2.3.3"
91
91
  },
92
- "gitHead": "d29d6cba24bb1c464650348851cfaf1911bbedfd"
92
+ "gitHead": "d0bbace3698b98e458856143676b7f631ee8e18f"
93
93
  }
@@ -51,19 +51,15 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
51
51
  const { formatMessage } = useIntl();
52
52
  const context = use(QxContext);
53
53
  const { field } = context;
54
- const [isShownDelete, setIsShownDelete] = useState();
55
54
  const { fields, append, remove } = useFieldArray({
56
55
  name: `${field}[${groupIndex}].expressions`,
57
56
  });
58
57
 
59
58
  return (
60
- <div
61
- onMouseEnter={() => setIsShownDelete(true)}
62
- onMouseLeave={() => setIsShownDelete(false)}
63
- >
59
+ <>
64
60
  <Segment>
65
61
  <div className="clause-and-group">
66
- {isShownDelete && removeGroup ? (
62
+ {removeGroup ? (
67
63
  <Button
68
64
  size="tiny"
69
65
  onClick={() => removeGroup(groupIndex)}
@@ -74,7 +70,7 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
74
70
  />
75
71
  ) : null}
76
72
  </div>
77
-
73
+ <Divider hidden />
78
74
  {fields.map((_clauseExpression, expressionIndex) => (
79
75
  <div key={`and-${groupIndex}-${expressionIndex}`}>
80
76
  <QxContext
@@ -86,7 +82,7 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
86
82
  >
87
83
  <Condition
88
84
  onDelete={
89
- _.size(fields) < 2 ? null : () => remove(expressionIndex)
85
+ expressionIndex > 0 ? () => remove(expressionIndex) : null
90
86
  }
91
87
  />
92
88
  </QxContext>
@@ -108,7 +104,7 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
108
104
  </Divider>
109
105
  </Segment>
110
106
  <Divider horizontal>{lastGroup ? null : "OR"}</Divider>
111
- </div>
107
+ </>
112
108
  );
113
109
  };
114
110
 
@@ -1,6 +1,6 @@
1
1
  import _ from "lodash/fp";
2
2
  import { useIntl } from "react-intl";
3
- import { useState, use } from "react";
3
+ import { use } from "react";
4
4
  import PropTypes from "prop-types";
5
5
  import { Dropdown, Button, Form, Grid, Label } from "semantic-ui-react";
6
6
  import { Controller, useFormContext } from "react-hook-form";
@@ -25,7 +25,6 @@ const isConditionExpression = (expression, functions) => {
25
25
 
26
26
  export default function Condition({ onDelete }) {
27
27
  const { formatMessage } = useIntl();
28
- const [isShownDelete, setIsShownDelete] = useState();
29
28
  const context = use(QxContext);
30
29
  const { control, watch, setValue } = useFormContext();
31
30
  const { field, functions } = context;
@@ -69,10 +68,7 @@ export default function Condition({ onDelete }) {
69
68
  );
70
69
  };
71
70
  return (
72
- <div
73
- onMouseEnter={() => setIsShownDelete(true)}
74
- onMouseLeave={() => setIsShownDelete(false)}
75
- >
71
+ <>
76
72
  {!isCondition ? (
77
73
  <QxContext
78
74
  value={{
@@ -81,7 +77,7 @@ export default function Condition({ onDelete }) {
81
77
  omitLogicOperators: true,
82
78
  }}
83
79
  >
84
- <Expression onDelete={isShownDelete ? onDelete : null} deletable />
80
+ <Expression onDelete={onDelete} deletable />
85
81
  </QxContext>
86
82
  ) : (
87
83
  <Grid className="condition-row">
@@ -115,35 +111,38 @@ export default function Condition({ onDelete }) {
115
111
  },
116
112
  }}
117
113
  render={({
118
- field: { onBlur, onChange, value },
114
+ field: { onBlur, onChange },
119
115
  fieldState: { error },
120
- }) => (
121
- <Form.Field error={!!error?.message}>
122
- <Dropdown
123
- options={condFuncOptions}
124
- onBlur={onBlur}
125
- value={value || ""}
126
- fluid
127
- onChange={(_e, { value }) => {
128
- if (value == "customExpression") {
129
- setValue(`${field}.value`, { isCondition: false });
130
- } else {
131
- onChange(value);
132
- setValue(`${field}.value.type`, "boolean");
133
- setValue(`${field}.value.isCondition`, true);
134
- }
135
- }}
136
- trigger={operatorTrigger(value)}
137
- pointing="top left"
138
- icon={null}
139
- />
140
- {error?.message && (
141
- <Label prompt pointing>
142
- {error?.message}
143
- </Label>
144
- )}
145
- </Form.Field>
146
- )}
116
+ }) => {
117
+ const displayValue = expression?.value?.name ?? "";
118
+ return (
119
+ <Form.Field error={!!error?.message}>
120
+ <Dropdown
121
+ options={condFuncOptions}
122
+ onBlur={onBlur}
123
+ value={displayValue || ""}
124
+ fluid
125
+ onChange={(_e, { value }) => {
126
+ if (value == "customExpression") {
127
+ setValue(`${field}.value`, { isCondition: false });
128
+ } else {
129
+ onChange(value);
130
+ setValue(`${field}.value.type`, "boolean");
131
+ setValue(`${field}.value.isCondition`, true);
132
+ }
133
+ }}
134
+ trigger={operatorTrigger(displayValue)}
135
+ pointing="top left"
136
+ icon={null}
137
+ />
138
+ {error?.message && (
139
+ <Label prompt pointing>
140
+ {error?.message}
141
+ </Label>
142
+ )}
143
+ </Form.Field>
144
+ );
145
+ }}
147
146
  />
148
147
  </div>
149
148
  </Grid.Column>
@@ -163,7 +162,7 @@ export default function Condition({ onDelete }) {
163
162
  </Grid.Column>
164
163
 
165
164
  <Grid.Column width={1} verticalAlign="top">
166
- {onDelete && isShownDelete ? (
165
+ {onDelete ? (
167
166
  <div className="delete-expression-column">
168
167
  <Button
169
168
  size="mini"
@@ -178,7 +177,7 @@ export default function Condition({ onDelete }) {
178
177
  </Grid.Column>
179
178
  </Grid>
180
179
  )}
181
- </div>
180
+ </>
182
181
  );
183
182
  }
184
183
 
@@ -1,3 +1,5 @@
1
+ import { act, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
1
3
  import { render, waitForLoad } from "@truedat/test/render";
2
4
  import TestFormWrapper from "@truedat/qx/components/common/TestFormWrapper";
3
5
  import Clauses from "../Clauses";
@@ -13,6 +15,14 @@ const renderOpts = {
13
15
  };
14
16
 
15
17
  describe("<Clauses />", () => {
18
+ beforeEach(() => {
19
+ jest.spyOn(console, "error").mockImplementation(() => {});
20
+ });
21
+
22
+ afterEach(() => {
23
+ jest.restoreAllMocks();
24
+ });
25
+
16
26
  it("matches the latest snapshot", async () => {
17
27
  const rendered = render(
18
28
  <TestFormWrapper>
@@ -51,4 +61,81 @@ describe("<Clauses />", () => {
51
61
  await waitForLoad(rendered);
52
62
  expect(rendered.container).toMatchSnapshot();
53
63
  });
64
+
65
+ it("adds and removes clause groups", async () => {
66
+ const user = userEvent.setup({ delay: null });
67
+ const formValuesRef = { current: {} };
68
+ const rendered = render(
69
+ <TestFormWrapper
70
+ context={{
71
+ field: "test",
72
+ }}
73
+ defaultValues={{
74
+ test: [{ expressions: [{ shape: "function", value: null }] }],
75
+ }}
76
+ watcher={(values) => {
77
+ formValuesRef.current = values;
78
+ }}
79
+ >
80
+ <Clauses />
81
+ </TestFormWrapper>,
82
+ renderOpts
83
+ );
84
+ await waitForLoad(rendered);
85
+
86
+ await act(async () => {
87
+ await user.click(rendered.getByText(/add group/i));
88
+ });
89
+ await waitFor(() => expect(formValuesRef.current.test).toHaveLength(2));
90
+ await waitFor(() =>
91
+ expect(formValuesRef.current.test[1].expressions).toHaveLength(1)
92
+ );
93
+ await waitFor(() =>
94
+ expect(formValuesRef.current.test[1].expressions[0]).toEqual(
95
+ expect.objectContaining({ shape: "function" })
96
+ )
97
+ );
98
+ await waitFor(() =>
99
+ expect(rendered.getAllByLabelText(/delete/i)).toHaveLength(2)
100
+ );
101
+
102
+ await act(async () => {
103
+ await user.click(rendered.getAllByLabelText(/delete/i)[0]);
104
+ });
105
+ await waitFor(() => expect(formValuesRef.current.test).toHaveLength(1));
106
+ });
107
+
108
+ it("adds expression into a clause group", async () => {
109
+ const user = userEvent.setup({ delay: null });
110
+ const formValuesRef = { current: {} };
111
+ const rendered = render(
112
+ <TestFormWrapper
113
+ context={{
114
+ field: "test",
115
+ }}
116
+ defaultValues={{
117
+ test: [{ expressions: [{ shape: "function", value: null }] }],
118
+ }}
119
+ watcher={(values) => {
120
+ formValuesRef.current = values;
121
+ }}
122
+ >
123
+ <Clauses />
124
+ </TestFormWrapper>,
125
+ renderOpts
126
+ );
127
+ await waitForLoad(rendered);
128
+
129
+ await act(async () => {
130
+ await user.click(rendered.getByText(/addexpression/i));
131
+ });
132
+ await waitFor(() =>
133
+ expect(formValuesRef.current.test[0].expressions).toHaveLength(2)
134
+ );
135
+ await waitFor(() =>
136
+ expect(formValuesRef.current.test[0].expressions[1]).toEqual(
137
+ expect.objectContaining({ shape: "function" })
138
+ )
139
+ );
140
+ });
54
141
  });
@@ -1,6 +1,22 @@
1
+ import userEvent from "@testing-library/user-event";
2
+ import { waitFor } from "@testing-library/react";
3
+ import PropTypes from "prop-types";
1
4
  import { render, waitForLoad } from "@truedat/test/render";
5
+ import { useFormContext } from "react-hook-form";
2
6
  import TestFormWrapper from "@truedat/qx/components/common/TestFormWrapper";
3
- import Condition from "../Condition";
7
+ import Condition, { isConditionFunction } from "../Condition";
8
+
9
+ function ValidationTrigger({ name }) {
10
+ const { trigger } = useFormContext();
11
+ return (
12
+ <button type="button" onClick={() => trigger(name)}>
13
+ validate
14
+ </button>
15
+ );
16
+ }
17
+ ValidationTrigger.propTypes = {
18
+ name: PropTypes.string.isRequired,
19
+ };
4
20
 
5
21
  describe("<Condition />", () => {
6
22
  it("matches the latest snapshot", async () => {
@@ -13,4 +29,207 @@ describe("<Condition />", () => {
13
29
 
14
30
  expect(rendered.container).toMatchSnapshot();
15
31
  });
32
+
33
+ it("identifies valid condition functions", () => {
34
+ expect(
35
+ isConditionFunction({
36
+ type: "boolean",
37
+ params: [
38
+ { name: "arg1", type: "any" },
39
+ { name: "arg2", type: "any" },
40
+ ],
41
+ })
42
+ ).toBe(true);
43
+
44
+ expect(
45
+ isConditionFunction({
46
+ type: "string",
47
+ params: [
48
+ { name: "arg1", type: "any" },
49
+ { name: "arg2", type: "any" },
50
+ ],
51
+ })
52
+ ).toBe(false);
53
+ });
54
+
55
+ it("shows delete action and handles delete click for condition rows", async () => {
56
+ const user = userEvent.setup({ delay: null });
57
+ const onDelete = jest.fn();
58
+ const rendered = render(
59
+ <TestFormWrapper
60
+ context={{
61
+ field: "test",
62
+ functions: [
63
+ {
64
+ name: "eq",
65
+ type: "boolean",
66
+ params: [
67
+ { name: "arg1", type: "any" },
68
+ { name: "arg2", type: "any" },
69
+ ],
70
+ },
71
+ ],
72
+ }}
73
+ defaultValues={{
74
+ test: {
75
+ shape: "function",
76
+ value: {
77
+ name: "eq",
78
+ type: "boolean",
79
+ args: { arg1: { shape: "field" }, arg2: { shape: "field" } },
80
+ },
81
+ },
82
+ }}
83
+ >
84
+ <Condition onDelete={onDelete} />
85
+ </TestFormWrapper>
86
+ );
87
+ await waitForLoad(rendered);
88
+
89
+ await user.click(rendered.getByLabelText(/delete/i));
90
+ expect(onDelete).toHaveBeenCalledTimes(1);
91
+ });
92
+
93
+ it("shows required validation for condition function name", async () => {
94
+ const user = userEvent.setup({ delay: null });
95
+ const rendered = render(
96
+ <TestFormWrapper
97
+ context={{
98
+ field: "test",
99
+ functions: [
100
+ {
101
+ name: "eq",
102
+ type: "boolean",
103
+ params: [
104
+ { name: "arg1", type: "any" },
105
+ { name: "arg2", type: "any" },
106
+ ],
107
+ },
108
+ ],
109
+ }}
110
+ defaultValues={{
111
+ test: {
112
+ shape: "function",
113
+ value: {
114
+ name: "",
115
+ type: "boolean",
116
+ isCondition: true,
117
+ args: { arg1: { shape: "field" }, arg2: { shape: "field" } },
118
+ },
119
+ },
120
+ }}
121
+ >
122
+ <Condition />
123
+ <ValidationTrigger name="test.value.name" />
124
+ </TestFormWrapper>
125
+ );
126
+ await waitForLoad(rendered);
127
+
128
+ await user.click(rendered.getByText(/validate/i));
129
+ await waitFor(() =>
130
+ expect(rendered.getByText(/functions.form.required/i)).toBeInTheDocument()
131
+ );
132
+ });
133
+
134
+ it("switches from condition to custom expression option", async () => {
135
+ const user = userEvent.setup({ delay: null });
136
+ const formValuesRef = { current: {} };
137
+ const rendered = render(
138
+ <TestFormWrapper
139
+ context={{
140
+ field: "test",
141
+ functions: [
142
+ {
143
+ name: "eq",
144
+ type: "boolean",
145
+ params: [
146
+ { name: "arg1", type: "any" },
147
+ { name: "arg2", type: "any" },
148
+ ],
149
+ },
150
+ ],
151
+ }}
152
+ defaultValues={{
153
+ test: {
154
+ shape: "function",
155
+ value: {
156
+ name: "eq",
157
+ type: "boolean",
158
+ isCondition: true,
159
+ args: { arg1: { shape: "field" }, arg2: { shape: "field" } },
160
+ },
161
+ },
162
+ }}
163
+ watcher={(values) => {
164
+ formValuesRef.current = values;
165
+ }}
166
+ >
167
+ <Condition />
168
+ </TestFormWrapper>
169
+ );
170
+ await waitForLoad(rendered);
171
+
172
+ await user.click(rendered.getAllByRole("listbox")[1]);
173
+ await user.click(
174
+ rendered.getByText(/expression.condition.customexpression/i)
175
+ );
176
+
177
+ await waitFor(() =>
178
+ expect(formValuesRef.current.test.value).toEqual({ isCondition: false })
179
+ );
180
+ });
181
+
182
+ it("marks selected operator as condition function", async () => {
183
+ const user = userEvent.setup({ delay: null });
184
+ const formValuesRef = { current: {} };
185
+ const rendered = render(
186
+ <TestFormWrapper
187
+ context={{
188
+ field: "test",
189
+ functions: [
190
+ {
191
+ name: "eq",
192
+ type: "boolean",
193
+ params: [
194
+ { name: "arg1", type: "any" },
195
+ { name: "arg2", type: "any" },
196
+ ],
197
+ },
198
+ ],
199
+ }}
200
+ defaultValues={{
201
+ test: {
202
+ shape: "function",
203
+ value: {
204
+ name: "",
205
+ type: "boolean",
206
+ isCondition: true,
207
+ args: { arg1: { shape: "field" }, arg2: { shape: "field" } },
208
+ },
209
+ },
210
+ }}
211
+ watcher={(values) => {
212
+ formValuesRef.current = values;
213
+ }}
214
+ >
215
+ <Condition />
216
+ </TestFormWrapper>
217
+ );
218
+ await waitForLoad(rendered);
219
+
220
+ await user.click(
221
+ rendered.getByText(/expression.condition.selectfunction/i)
222
+ );
223
+ await user.click(rendered.getByText(/eq/i));
224
+
225
+ await waitFor(() =>
226
+ expect(formValuesRef.current.test.value).toEqual(
227
+ expect.objectContaining({
228
+ name: "eq",
229
+ type: "boolean",
230
+ isCondition: true,
231
+ })
232
+ )
233
+ );
234
+ });
16
235
  });
@@ -37,35 +37,36 @@ exports[`<Clauses /> matches the latest snapshot with content 1`] = `
37
37
  <label>
38
38
  Clause
39
39
  </label>
40
- <div>
40
+ <div
41
+ class="ui segment"
42
+ >
43
+ <div
44
+ class="clause-and-group"
45
+ />
41
46
  <div
42
- class="ui segment"
47
+ class="ui hidden divider"
48
+ />
49
+ <div
50
+ class="ui horizontal divider"
43
51
  >
44
52
  <div
45
- class="clause-and-group"
46
- />
47
- <div
48
- class="ui horizontal divider"
53
+ class="ui medium basic buttons"
49
54
  >
50
- <div
51
- class="ui medium basic buttons"
55
+ <button
56
+ class="ui button"
52
57
  >
53
- <button
54
- class="ui button"
55
- >
56
- <i
57
- aria-hidden="true"
58
- class="plus icon"
59
- />
60
- addExpression
61
- </button>
62
- </div>
58
+ <i
59
+ aria-hidden="true"
60
+ class="plus icon"
61
+ />
62
+ addExpression
63
+ </button>
63
64
  </div>
64
65
  </div>
65
- <div
66
- class="ui horizontal divider"
67
- />
68
66
  </div>
67
+ <div
68
+ class="ui horizontal divider"
69
+ />
69
70
  <div
70
71
  class="ui horizontal divider"
71
72
  >