@truedat/qx 8.10.2 → 8.10.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.
- package/package.json +3 -3
- package/src/components/QxRoutes.js +8 -7
- package/src/components/__tests__/QxRoutes.spec.js +2 -1
- package/src/components/__tests__/__fixtures__/helper.js +13 -0
- package/src/components/__tests__/__snapshots__/QxRoutes.spec.js.snap +1 -1
- package/src/components/common/ClauseViewer.js +52 -8
- package/src/components/common/DescriptionInput.js +9 -34
- package/src/components/common/TypeSelector.js +20 -10
- package/src/components/common/__tests__/ClauseViewer.spec.js +150 -0
- package/src/components/common/__tests__/DescriptionInput.spec.js +2 -5
- package/src/components/common/__tests__/__snapshots__/DescriptionInput.spec.js.snap +7 -10
- package/src/components/common/expressions/Clauses.js +22 -17
- package/src/components/common/expressions/Condition.js +201 -102
- package/src/components/common/expressions/Expression.js +17 -23
- package/src/components/common/expressions/FunctionSelector.js +3 -3
- package/src/components/common/expressions/ShapeSelector.js +1 -1
- package/src/components/common/expressions/__tests__/Clauses.spec.js +1 -1
- package/src/components/common/expressions/__tests__/Condition.spec.js +83 -26
- package/src/components/common/expressions/__tests__/ParamSelector.spec.js +1 -1
- package/src/components/common/expressions/__tests__/__snapshots__/Clauses.spec.js.snap +18 -22
- package/src/components/common/expressions/__tests__/__snapshots__/Condition.spec.js.snap +215 -216
- package/src/components/common/expressions/__tests__/__snapshots__/ConstantSelector.spec.js.snap +6 -10
- package/src/components/common/expressions/__tests__/__snapshots__/Expression.spec.js.snap +630 -654
- package/src/components/common/expressions/__tests__/__snapshots__/FunctionArgs.spec.js.snap +193 -201
- package/src/components/common/expressions/__tests__/__snapshots__/FunctionSelector.spec.js.snap +450 -466
- package/src/components/common/expressions/__tests__/__snapshots__/ShapeSelector.spec.js.snap +34 -34
- package/src/components/common/expressions/constantInputs/AnySelector.js +1 -0
- package/src/components/common/expressions/constantInputs/__tests__/__snapshots__/AnySelector.spec.js.snap +15 -15
- package/src/components/dataViews/__tests__/__snapshots__/AdvancedDataViewEditor.spec.js.snap +378 -406
- package/src/components/dataViews/__tests__/__snapshots__/DataViewSummary.spec.js.snap +6 -6
- package/src/components/dataViews/__tests__/__snapshots__/Queryable.spec.js.snap +489 -577
- package/src/components/dataViews/__tests__/__snapshots__/Queryables.spec.js.snap +400 -476
- package/src/components/dataViews/advancedForm/Queryable.js +56 -64
- package/src/components/dataViews/advancedForm/Queryables.js +11 -2
- package/src/components/dataViews/queryableProperties/Join.js +16 -6
- package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Join.spec.js.snap +0 -28
- package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Select.spec.js.snap +270 -282
- package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/SelectField.spec.js.snap +172 -180
- package/src/components/dataViews/simpleForm/DatasetForm.js +4 -0
- package/src/components/dataViews/simpleForm/FormQueryable.js +49 -53
- package/src/components/dataViews/summary/Where.js +3 -2
- package/src/components/functions/FunctionEdit.js +92 -0
- package/src/components/functions/FunctionEditor.js +32 -10
- package/src/components/functions/FunctionParams.js +54 -61
- package/src/components/functions/FunctionsList.js +213 -0
- package/src/components/functions/__tests__/FunctionEdit.spec.js +168 -0
- package/src/components/functions/__tests__/FunctionParams.spec.js +1 -8
- package/src/components/functions/__tests__/FunctionsList.spec.js +62 -0
- package/src/components/functions/__tests__/__snapshots__/FunctionEditor.spec.js.snap +553 -469
- package/src/components/functions/__tests__/__snapshots__/FunctionParams.spec.js.snap +139 -161
- package/src/components/functions/__tests__/__snapshots__/FunctionsList.spec.js.snap +588 -0
- package/src/components/qualityControls/__tests__/NewQualityControl.spec.js +4 -0
- package/src/components/qualityControls/__tests__/QualityControlEditor.spec.js +8 -1
- package/src/components/qualityControls/__tests__/__snapshots__/EditQualityControl.spec.js.snap +109 -118
- package/src/components/qualityControls/__tests__/__snapshots__/NewDraftQualityControl.spec.js.snap +109 -118
- package/src/components/qualityControls/__tests__/__snapshots__/QualityControlEditor.spec.js.snap +471 -468
- package/src/hooks/useFunctions.js +6 -0
- package/src/styles/Expression.less +39 -6
- package/src/components/functions/Functions.js +0 -143
- package/src/components/functions/__tests__/Functions.spec.js +0 -56
- package/src/components/functions/__tests__/__snapshots__/Functions.spec.js.snap +0 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/qx",
|
|
3
|
-
"version": "8.10.
|
|
3
|
+
"version": "8.10.4",
|
|
4
4
|
"description": "Truedat Web Quality Experience package",
|
|
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.10.
|
|
62
|
+
"@truedat/test": "8.10.4",
|
|
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": "
|
|
95
|
+
"gitHead": "fe4fda03b2b73d26f644219d806dcc45767a3c7a"
|
|
96
96
|
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
} from "@truedat/core/routes";
|
|
12
12
|
import DataViewEditor from "./dataViews/DataViewEditor";
|
|
13
13
|
import DataViews from "./dataViews/DataViews";
|
|
14
|
-
import
|
|
14
|
+
import FunctionEdit from "./functions/FunctionEdit";
|
|
15
|
+
import FunctionsList from "./functions/FunctionsList";
|
|
15
16
|
import QualityControlRoutes from "./qualityControls/QualityControlRoutes";
|
|
16
17
|
import ScoreRoutes from "./scores/ScoreRoutes";
|
|
17
18
|
import ScoreGroupsRoutes from "./scores/ScoreGroupsRoutes";
|
|
@@ -29,12 +30,12 @@ export default function QxRoutes() {
|
|
|
29
30
|
</Route>
|
|
30
31
|
<Route
|
|
31
32
|
path={FUNCTIONS}
|
|
32
|
-
element={
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
33
|
+
element={<ProtectedRoute authorization="quality_control" />}
|
|
34
|
+
>
|
|
35
|
+
<Route index element={<FunctionsList />} />
|
|
36
|
+
<Route path="new" element={<FunctionEdit />} />
|
|
37
|
+
<Route path=":id" element={<FunctionEdit />} />
|
|
38
|
+
</Route>
|
|
38
39
|
<Route
|
|
39
40
|
path={`${QUALITY_CONTROLS}/*`}
|
|
40
41
|
element={
|
|
@@ -17,7 +17,8 @@ jest.mock("@truedat/core/hooks/useAuthorized", () => ({
|
|
|
17
17
|
|
|
18
18
|
// Mock all child components
|
|
19
19
|
jest.mock("../dataViews/DataViews", () => () => <div>DataViews</div>);
|
|
20
|
-
jest.mock("../functions/
|
|
20
|
+
jest.mock("../functions/FunctionsList", () => () => <div>FunctionsList</div>);
|
|
21
|
+
jest.mock("../functions/FunctionEdit", () => () => <div>FunctionEdit</div>);
|
|
21
22
|
jest.mock("../qualityControls/QualityControlRoutes", () => () => (
|
|
22
23
|
<div>QualityControlRoutes</div>
|
|
23
24
|
));
|
|
@@ -19,6 +19,13 @@ export const messages = {
|
|
|
19
19
|
"expression.clause.action.addGroup": "Add group",
|
|
20
20
|
"expression.condition.customExpression": "Custom expression",
|
|
21
21
|
"expression.condition.selectFunction": "Select function",
|
|
22
|
+
"expression.condition.class.comparison": "Comparison",
|
|
23
|
+
"expression.condition.class.length_comparison": "Length comparison",
|
|
24
|
+
"expression.condition.class.date_comparisons": "Date comparisons",
|
|
25
|
+
"expression.condition.class.string": "String",
|
|
26
|
+
"expression.condition.class.format": "Format",
|
|
27
|
+
"expression.condition.class.logic": "Logic",
|
|
28
|
+
"expression.condition.class.validation": "Validation",
|
|
22
29
|
"expressions.data_type.any": "any",
|
|
23
30
|
"expressions.data_type.boolean": "boolean",
|
|
24
31
|
"expressions.data_type.date": "date",
|
|
@@ -54,6 +61,12 @@ export const messages = {
|
|
|
54
61
|
"functions.header": "Quality Functions",
|
|
55
62
|
"functions.no_selection": "No function selected",
|
|
56
63
|
"functions.subheader": "List and edit quality functions",
|
|
64
|
+
"functions.list.count": "{count} functions",
|
|
65
|
+
"functions.table.class": "Class",
|
|
66
|
+
"functions.table.description": "Description",
|
|
67
|
+
"functions.table.name": "Name",
|
|
68
|
+
"functions.table.params": "Parameters",
|
|
69
|
+
"functions.table.type": "Type",
|
|
57
70
|
"group.props.name": "Name",
|
|
58
71
|
"quality_control.actions.do_execution": "Execute quality control",
|
|
59
72
|
"quality_control.actions.execution.confirmation.content":
|
|
@@ -4,7 +4,10 @@ import PropTypes from "prop-types";
|
|
|
4
4
|
import { useIntl } from "react-intl";
|
|
5
5
|
import { Divider, Label, Segment } from "semantic-ui-react";
|
|
6
6
|
import { useFunctions } from "@truedat/qx/hooks/useFunctions";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
argNames,
|
|
9
|
+
isConditionFunction,
|
|
10
|
+
} from "@truedat/qx/components/common/expressions/Condition";
|
|
8
11
|
import QxContext from "@truedat/qx/components/QxContext";
|
|
9
12
|
import { getColorById } from "../dataViews/queryableFunctions";
|
|
10
13
|
import "@truedat/qx/styles/Expression.less";
|
|
@@ -27,9 +30,8 @@ const isExpressionValid = (expression, functions = []) => {
|
|
|
27
30
|
}
|
|
28
31
|
const func = _.find({ name: value.name, type: value.type })(functions);
|
|
29
32
|
if (func && isConditionFunction(func)) {
|
|
30
|
-
return (
|
|
31
|
-
isExpressionValid(value.args?.
|
|
32
|
-
isExpressionValid(value.args?.arg2, functions)
|
|
33
|
+
return argNames(func).every((argName) =>
|
|
34
|
+
isExpressionValid(value.args?.[argName], functions),
|
|
33
35
|
);
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -145,6 +147,7 @@ function ExpressionFunctionViewer({ value, alias }) {
|
|
|
145
147
|
return isConditionFunction(func) ? (
|
|
146
148
|
<ConditionExpressionFunctionViewer
|
|
147
149
|
value={value}
|
|
150
|
+
func={func}
|
|
148
151
|
operator={func?.operator}
|
|
149
152
|
/>
|
|
150
153
|
) : (
|
|
@@ -203,7 +206,7 @@ function GenericExpressionFunctionViewer({ alias, value }) {
|
|
|
203
206
|
GenericExpressionFunctionViewer.propTypes = {
|
|
204
207
|
value: PropTypes.object,
|
|
205
208
|
};
|
|
206
|
-
function ConditionExpressionFunctionViewer({ value, operator }) {
|
|
209
|
+
function ConditionExpressionFunctionViewer({ value, operator, func }) {
|
|
207
210
|
const { formatMessage } = useIntl();
|
|
208
211
|
if (!value || !value.args) {
|
|
209
212
|
return (
|
|
@@ -213,17 +216,58 @@ function ConditionExpressionFunctionViewer({ value, operator }) {
|
|
|
213
216
|
);
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
const names = argNames(func);
|
|
220
|
+
|
|
221
|
+
// Same precedence the editor uses in Condition.js: only 6 of the ~100 functions
|
|
222
|
+
// carry an `operator`, but every one carries a `label`.
|
|
223
|
+
const label = operator || func?.label || func?.name;
|
|
224
|
+
|
|
225
|
+
// Unary conditions read as `f( x )`. The infix layout below would trail the
|
|
226
|
+
// name *after* its own operand, which misreads as belonging to the operand —
|
|
227
|
+
// badly so when the operand is itself a function block, e.g. `empty(to_string(x))`
|
|
228
|
+
// rendering as `to_string ( x ) Empty`.
|
|
229
|
+
if (names.length === 1) {
|
|
230
|
+
const arg = value.args[names[0]];
|
|
231
|
+
|
|
232
|
+
// A function operand renders as its own multi-line block. Wrap it the way
|
|
233
|
+
// GenericExpressionFunctionViewer does: inline parens inside a flex row would
|
|
234
|
+
// put the closing paren on the first line, above the operand.
|
|
235
|
+
if (arg?.shape === "function") {
|
|
236
|
+
return (
|
|
237
|
+
<div className="text-align-left">
|
|
238
|
+
<b>{label}</b>(
|
|
239
|
+
<div className="clause-viewer-function">
|
|
240
|
+
<ExpressionViewer expression={arg} />
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className="condition-function-viewer">
|
|
249
|
+
<b>{label}</b>
|
|
250
|
+
<span>(</span>
|
|
251
|
+
<ExpressionViewer expression={arg} />
|
|
252
|
+
<span>)</span>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
216
257
|
return (
|
|
217
258
|
<div className="condition-function-viewer">
|
|
218
|
-
<ExpressionViewer expression={value.args
|
|
219
|
-
<b className="font-big">{
|
|
220
|
-
|
|
259
|
+
<ExpressionViewer expression={value.args[names[0]]} />
|
|
260
|
+
<b className={operator ? "font-big" : undefined}>{label}</b>
|
|
261
|
+
{names.slice(1).map((argName) => (
|
|
262
|
+
<ExpressionViewer key={argName} expression={value.args[argName]} />
|
|
263
|
+
))}
|
|
221
264
|
</div>
|
|
222
265
|
);
|
|
223
266
|
}
|
|
224
267
|
ConditionExpressionFunctionViewer.propTypes = {
|
|
225
268
|
value: PropTypes.object,
|
|
226
269
|
operator: PropTypes.string,
|
|
270
|
+
func: PropTypes.object,
|
|
227
271
|
};
|
|
228
272
|
|
|
229
273
|
function ExpressionConstantViewer({ value }) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import _ from "lodash/fp";
|
|
2
|
-
import { useState } from "react";
|
|
3
1
|
import { useIntl } from "react-intl";
|
|
4
|
-
import { Input
|
|
2
|
+
import { Input } from "semantic-ui-react";
|
|
5
3
|
|
|
6
4
|
export default function DescriptionInput({
|
|
7
5
|
value,
|
|
@@ -10,37 +8,14 @@ export default function DescriptionInput({
|
|
|
10
8
|
name = "description",
|
|
11
9
|
}) {
|
|
12
10
|
const { formatMessage } = useIntl();
|
|
13
|
-
const [editionMode, setEditMode] = useState();
|
|
14
11
|
return (
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
>
|
|
24
|
-
{_.isEmpty(value)
|
|
25
|
-
? formatMessage({ id: "functions.form.add_description" })
|
|
26
|
-
: value}
|
|
27
|
-
</List.Item>
|
|
28
|
-
) : null}
|
|
29
|
-
{editionMode ? (
|
|
30
|
-
<List.Item>
|
|
31
|
-
<Input
|
|
32
|
-
onBlur={() => {
|
|
33
|
-
onBlur && onBlur();
|
|
34
|
-
setEditMode(false);
|
|
35
|
-
}}
|
|
36
|
-
placeholder={formatMessage({ id: "functions.form.description" })}
|
|
37
|
-
name={name}
|
|
38
|
-
onChange={onChange}
|
|
39
|
-
value={value}
|
|
40
|
-
autoFocus
|
|
41
|
-
/>
|
|
42
|
-
</List.Item>
|
|
43
|
-
) : null}
|
|
44
|
-
</List>
|
|
12
|
+
<Input
|
|
13
|
+
fluid
|
|
14
|
+
placeholder={formatMessage({ id: "functions.form.description" })}
|
|
15
|
+
name={name}
|
|
16
|
+
onChange={onChange}
|
|
17
|
+
onBlur={onBlur}
|
|
18
|
+
value={value ?? ""}
|
|
19
|
+
/>
|
|
45
20
|
);
|
|
46
21
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
2
|
import { useIntl } from "react-intl";
|
|
3
|
-
import { Dropdown } from "semantic-ui-react";
|
|
3
|
+
import { Dropdown, Icon, Form, Label } from "semantic-ui-react";
|
|
4
4
|
import { typeToIcon, types } from "@truedat/qx/types";
|
|
5
|
-
import { Form, Label } from "semantic-ui-react";
|
|
6
5
|
|
|
7
6
|
export default function TypeSelector({
|
|
8
7
|
value,
|
|
@@ -11,22 +10,33 @@ export default function TypeSelector({
|
|
|
11
10
|
withoutTypeAny,
|
|
12
11
|
error,
|
|
13
12
|
name = "type",
|
|
13
|
+
iconOnly = false,
|
|
14
14
|
}) {
|
|
15
15
|
const { formatMessage } = useIntl();
|
|
16
16
|
const options = _.flow(
|
|
17
|
-
_.reject((
|
|
18
|
-
_.map((
|
|
19
|
-
key:
|
|
20
|
-
text: formatMessage({ id: `expressions.data_type.${
|
|
21
|
-
value,
|
|
22
|
-
icon: typeToIcon(
|
|
17
|
+
_.reject((v) => withoutTypeAny && v == "any"),
|
|
18
|
+
_.map((v) => ({
|
|
19
|
+
key: v,
|
|
20
|
+
text: formatMessage({ id: `expressions.data_type.${v}` }),
|
|
21
|
+
value: v,
|
|
22
|
+
icon: typeToIcon(v),
|
|
23
23
|
})),
|
|
24
24
|
)(types);
|
|
25
|
+
|
|
25
26
|
return (
|
|
26
27
|
<Form.Field error={!!error?.message}>
|
|
27
28
|
<Dropdown
|
|
28
|
-
button
|
|
29
|
-
fluid
|
|
29
|
+
button={!iconOnly}
|
|
30
|
+
fluid={!iconOnly}
|
|
31
|
+
icon={iconOnly ? null : undefined}
|
|
32
|
+
trigger={
|
|
33
|
+
iconOnly ? (
|
|
34
|
+
<div className="type-selector-trigger">
|
|
35
|
+
<Icon name={typeToIcon(value)} />
|
|
36
|
+
</div>
|
|
37
|
+
) : undefined
|
|
38
|
+
}
|
|
39
|
+
pointing={iconOnly ? "top left" : undefined}
|
|
30
40
|
onBlur={onBlur}
|
|
31
41
|
name={name}
|
|
32
42
|
options={options}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
2
|
+
import ClauseViewer from "../ClauseViewer";
|
|
3
|
+
|
|
4
|
+
jest.mock("@truedat/qx/hooks/useFunctions", () => {
|
|
5
|
+
const originalModule = jest.requireActual("@truedat/qx/hooks/useFunctions");
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
__esModule: true,
|
|
9
|
+
...originalModule,
|
|
10
|
+
useFunctions: jest.fn(() => ({
|
|
11
|
+
data: {
|
|
12
|
+
data: [
|
|
13
|
+
{
|
|
14
|
+
name: "eq",
|
|
15
|
+
type: "boolean",
|
|
16
|
+
class: "comparison",
|
|
17
|
+
label: "=",
|
|
18
|
+
operator: "=",
|
|
19
|
+
params: [
|
|
20
|
+
{ name: "arg1", type: "any" },
|
|
21
|
+
{ name: "arg2", type: "any" },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
// Only 6 of the ~100 shipped functions carry an `operator`; the rest
|
|
26
|
+
// rely on `label`, as `lte_first_day_of_current_year` does here.
|
|
27
|
+
name: "lte_first_day_of_current_year",
|
|
28
|
+
type: "boolean",
|
|
29
|
+
class: "lte_date",
|
|
30
|
+
label: "≤ First Day of Year",
|
|
31
|
+
params: [{ name: "value", type: "date" }],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "empty",
|
|
35
|
+
type: "boolean",
|
|
36
|
+
class: "validation",
|
|
37
|
+
label: "Empty",
|
|
38
|
+
params: [{ name: "value", type: "any" }],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
// Not boolean, so it takes the generic `name ( args )` renderer.
|
|
42
|
+
name: "to_string",
|
|
43
|
+
type: "string",
|
|
44
|
+
class: "string",
|
|
45
|
+
label: "To String",
|
|
46
|
+
params: [{ name: "value", type: "any" }],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
loading: false,
|
|
51
|
+
error: null,
|
|
52
|
+
mutate: jest.fn(),
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const field = (name) => ({
|
|
58
|
+
shape: "field",
|
|
59
|
+
value: { id: 1, name, parent_id: 0, type: "string" },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const conditionClause = (name, args) => [
|
|
63
|
+
{
|
|
64
|
+
expressions: [
|
|
65
|
+
{ shape: "function", value: { name, type: "boolean", args } },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
describe("<ClauseViewer />", () => {
|
|
71
|
+
it("renders the operator for functions that define one", async () => {
|
|
72
|
+
const rendered = render(
|
|
73
|
+
<ClauseViewer
|
|
74
|
+
clause={conditionClause("eq", {
|
|
75
|
+
arg1: field("product_id"),
|
|
76
|
+
arg2: field("product_name"),
|
|
77
|
+
})}
|
|
78
|
+
parentResource={{ id: 0, embedded: { name: "Products" } }}
|
|
79
|
+
/>,
|
|
80
|
+
);
|
|
81
|
+
await waitForLoad(rendered);
|
|
82
|
+
|
|
83
|
+
expect(rendered.getByText("=")).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("falls back to the function label, not its raw name", async () => {
|
|
87
|
+
const rendered = render(
|
|
88
|
+
<ClauseViewer
|
|
89
|
+
clause={conditionClause("lte_first_day_of_current_year", {
|
|
90
|
+
value: field("warehouse_id"),
|
|
91
|
+
})}
|
|
92
|
+
parentResource={{ id: 0, embedded: { name: "Products" } }}
|
|
93
|
+
/>,
|
|
94
|
+
);
|
|
95
|
+
await waitForLoad(rendered);
|
|
96
|
+
|
|
97
|
+
expect(rendered.getByText("≤ First Day of Year")).toBeInTheDocument();
|
|
98
|
+
expect(
|
|
99
|
+
rendered.queryByText("lte_first_day_of_current_year"),
|
|
100
|
+
).not.toBeInTheDocument();
|
|
101
|
+
|
|
102
|
+
// A leaf operand stays on one line rather than expanding into a block.
|
|
103
|
+
expect(rendered.container.textContent).toBe(
|
|
104
|
+
"≤ First Day of Year(Products.warehouse_id)",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("renders unary conditions prefix-first so nesting stays unambiguous", async () => {
|
|
109
|
+
const rendered = render(
|
|
110
|
+
<ClauseViewer
|
|
111
|
+
clause={conditionClause("empty", {
|
|
112
|
+
value: {
|
|
113
|
+
shape: "function",
|
|
114
|
+
value: {
|
|
115
|
+
name: "to_string",
|
|
116
|
+
type: "string",
|
|
117
|
+
args: {
|
|
118
|
+
value: {
|
|
119
|
+
shape: "constant",
|
|
120
|
+
value: { type: "string", value: "Text" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
})}
|
|
126
|
+
parentResource={{ id: 0, embedded: { name: "actor" } }}
|
|
127
|
+
/>,
|
|
128
|
+
);
|
|
129
|
+
await waitForLoad(rendered);
|
|
130
|
+
|
|
131
|
+
expect(rendered.container.textContent).toBe("Empty(to_string(Text))");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("keeps binary conditions infix", async () => {
|
|
135
|
+
const rendered = render(
|
|
136
|
+
<ClauseViewer
|
|
137
|
+
clause={conditionClause("eq", {
|
|
138
|
+
arg1: field("product_id"),
|
|
139
|
+
arg2: field("product_name"),
|
|
140
|
+
})}
|
|
141
|
+
parentResource={{ id: 0, embedded: { name: "Products" } }}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
await waitForLoad(rendered);
|
|
145
|
+
|
|
146
|
+
expect(rendered.container.textContent).toBe(
|
|
147
|
+
"Products.product_id=Products.product_name",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -27,13 +27,10 @@ describe("<DescriptionInput />", () => {
|
|
|
27
27
|
const rendered = render(<DescriptionInput {...props} />, renderOpts);
|
|
28
28
|
await waitForLoad(rendered);
|
|
29
29
|
|
|
30
|
-
await user.click(rendered.getByRole("listitem"));
|
|
31
30
|
await user.type(rendered.getByRole("textbox"), "description");
|
|
32
|
-
await user.
|
|
31
|
+
await user.tab();
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
expect(lastValue).toBe("description");
|
|
33
|
+
expect(props.onChange).toHaveBeenCalled();
|
|
37
34
|
expect(props.onBlur).toHaveBeenCalledTimes(1);
|
|
38
35
|
});
|
|
39
36
|
});
|
|
@@ -3,17 +3,14 @@
|
|
|
3
3
|
exports[`<DescriptionInput /> matches the latest snapshot 1`] = `
|
|
4
4
|
<div>
|
|
5
5
|
<div
|
|
6
|
-
class="ui
|
|
7
|
-
role="list"
|
|
6
|
+
class="ui fluid input"
|
|
8
7
|
>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
value
|
|
16
|
-
</a>
|
|
8
|
+
<input
|
|
9
|
+
name="description"
|
|
10
|
+
placeholder="description"
|
|
11
|
+
type="text"
|
|
12
|
+
value="value"
|
|
13
|
+
/>
|
|
17
14
|
</div>
|
|
18
15
|
</div>
|
|
19
16
|
`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import {
|
|
2
|
+
import { use } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import { useIntl } from "react-intl";
|
|
5
5
|
import { useFieldArray } from "react-hook-form";
|
|
@@ -56,21 +56,24 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
|
-
|
|
59
|
+
<div>
|
|
60
60
|
<Segment>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
{lastGroup && removeGroup ? (
|
|
62
|
+
<>
|
|
63
|
+
<div className="clause-and-group">
|
|
64
|
+
<Button
|
|
65
|
+
size="tiny"
|
|
66
|
+
onClick={() => removeGroup(groupIndex)}
|
|
67
|
+
icon="trash alternate outline"
|
|
68
|
+
basic
|
|
69
|
+
color="red"
|
|
70
|
+
aria-label="delete"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
<Divider hidden />
|
|
74
|
+
</>
|
|
75
|
+
) : null}
|
|
76
|
+
|
|
74
77
|
{fields.map((_clauseExpression, expressionIndex) => (
|
|
75
78
|
<div key={`and-${groupIndex}-${expressionIndex}`}>
|
|
76
79
|
<QxContext
|
|
@@ -82,7 +85,9 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
|
|
|
82
85
|
>
|
|
83
86
|
<Condition
|
|
84
87
|
onDelete={
|
|
85
|
-
|
|
88
|
+
_.size(fields) >= 2 && expressionIndex === _.size(fields) - 1
|
|
89
|
+
? () => remove(expressionIndex)
|
|
90
|
+
: null
|
|
86
91
|
}
|
|
87
92
|
/>
|
|
88
93
|
</QxContext>
|
|
@@ -104,7 +109,7 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
|
|
|
104
109
|
</Divider>
|
|
105
110
|
</Segment>
|
|
106
111
|
<Divider horizontal>{lastGroup ? null : "OR"}</Divider>
|
|
107
|
-
|
|
112
|
+
</div>
|
|
108
113
|
);
|
|
109
114
|
};
|
|
110
115
|
|