@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.
Files changed (61) hide show
  1. package/package.json +3 -3
  2. package/src/components/QxRoutes.js +8 -7
  3. package/src/components/__tests__/QxRoutes.spec.js +2 -1
  4. package/src/components/__tests__/__fixtures__/helper.js +13 -0
  5. package/src/components/__tests__/__snapshots__/QxRoutes.spec.js.snap +1 -1
  6. package/src/components/common/ClauseViewer.js +52 -8
  7. package/src/components/common/DescriptionInput.js +9 -34
  8. package/src/components/common/TypeSelector.js +20 -10
  9. package/src/components/common/__tests__/ClauseViewer.spec.js +150 -0
  10. package/src/components/common/__tests__/DescriptionInput.spec.js +2 -5
  11. package/src/components/common/__tests__/__snapshots__/DescriptionInput.spec.js.snap +7 -10
  12. package/src/components/common/expressions/Clauses.js +22 -17
  13. package/src/components/common/expressions/Condition.js +201 -102
  14. package/src/components/common/expressions/Expression.js +17 -23
  15. package/src/components/common/expressions/FunctionSelector.js +3 -3
  16. package/src/components/common/expressions/ShapeSelector.js +1 -1
  17. package/src/components/common/expressions/__tests__/Clauses.spec.js +1 -1
  18. package/src/components/common/expressions/__tests__/Condition.spec.js +83 -26
  19. package/src/components/common/expressions/__tests__/ParamSelector.spec.js +1 -1
  20. package/src/components/common/expressions/__tests__/__snapshots__/Clauses.spec.js.snap +18 -22
  21. package/src/components/common/expressions/__tests__/__snapshots__/Condition.spec.js.snap +215 -216
  22. package/src/components/common/expressions/__tests__/__snapshots__/ConstantSelector.spec.js.snap +6 -10
  23. package/src/components/common/expressions/__tests__/__snapshots__/Expression.spec.js.snap +630 -654
  24. package/src/components/common/expressions/__tests__/__snapshots__/FunctionArgs.spec.js.snap +193 -201
  25. package/src/components/common/expressions/__tests__/__snapshots__/FunctionSelector.spec.js.snap +450 -466
  26. package/src/components/common/expressions/__tests__/__snapshots__/ShapeSelector.spec.js.snap +34 -34
  27. package/src/components/common/expressions/constantInputs/AnySelector.js +1 -0
  28. package/src/components/common/expressions/constantInputs/__tests__/__snapshots__/AnySelector.spec.js.snap +15 -15
  29. package/src/components/dataViews/__tests__/__snapshots__/AdvancedDataViewEditor.spec.js.snap +378 -406
  30. package/src/components/dataViews/__tests__/__snapshots__/DataViewSummary.spec.js.snap +6 -6
  31. package/src/components/dataViews/__tests__/__snapshots__/Queryable.spec.js.snap +489 -577
  32. package/src/components/dataViews/__tests__/__snapshots__/Queryables.spec.js.snap +400 -476
  33. package/src/components/dataViews/advancedForm/Queryable.js +56 -64
  34. package/src/components/dataViews/advancedForm/Queryables.js +11 -2
  35. package/src/components/dataViews/queryableProperties/Join.js +16 -6
  36. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Join.spec.js.snap +0 -28
  37. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Select.spec.js.snap +270 -282
  38. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/SelectField.spec.js.snap +172 -180
  39. package/src/components/dataViews/simpleForm/DatasetForm.js +4 -0
  40. package/src/components/dataViews/simpleForm/FormQueryable.js +49 -53
  41. package/src/components/dataViews/summary/Where.js +3 -2
  42. package/src/components/functions/FunctionEdit.js +92 -0
  43. package/src/components/functions/FunctionEditor.js +32 -10
  44. package/src/components/functions/FunctionParams.js +54 -61
  45. package/src/components/functions/FunctionsList.js +213 -0
  46. package/src/components/functions/__tests__/FunctionEdit.spec.js +168 -0
  47. package/src/components/functions/__tests__/FunctionParams.spec.js +1 -8
  48. package/src/components/functions/__tests__/FunctionsList.spec.js +62 -0
  49. package/src/components/functions/__tests__/__snapshots__/FunctionEditor.spec.js.snap +553 -469
  50. package/src/components/functions/__tests__/__snapshots__/FunctionParams.spec.js.snap +139 -161
  51. package/src/components/functions/__tests__/__snapshots__/FunctionsList.spec.js.snap +588 -0
  52. package/src/components/qualityControls/__tests__/NewQualityControl.spec.js +4 -0
  53. package/src/components/qualityControls/__tests__/QualityControlEditor.spec.js +8 -1
  54. package/src/components/qualityControls/__tests__/__snapshots__/EditQualityControl.spec.js.snap +109 -118
  55. package/src/components/qualityControls/__tests__/__snapshots__/NewDraftQualityControl.spec.js.snap +109 -118
  56. package/src/components/qualityControls/__tests__/__snapshots__/QualityControlEditor.spec.js.snap +471 -468
  57. package/src/hooks/useFunctions.js +6 -0
  58. package/src/styles/Expression.less +39 -6
  59. package/src/components/functions/Functions.js +0 -143
  60. package/src/components/functions/__tests__/Functions.spec.js +0 -56
  61. package/src/components/functions/__tests__/__snapshots__/Functions.spec.js.snap +0 -85
@@ -1,8 +1,8 @@
1
1
  import _ from "lodash/fp";
2
2
  import { useIntl } from "react-intl";
3
- import { use } from "react";
3
+ import { use, useState } from "react";
4
4
  import PropTypes from "prop-types";
5
- import { Dropdown, Button, Form, Grid, Label } from "semantic-ui-react";
5
+ import { Dropdown, Button, Form, Input, Label } from "semantic-ui-react";
6
6
  import { Controller, useFormContext } from "react-hook-form";
7
7
  import QxContext from "@truedat/qx/components/QxContext";
8
8
  import Expression from "./Expression";
@@ -10,11 +10,8 @@ import Expression from "./Expression";
10
10
  const hasTypeKey = _.flow(_.keys, _.contains("type"));
11
11
  export const isConditionFunction = _.conforms({
12
12
  type: _.isEqual("boolean"),
13
- params: _.conforms([
14
- _.conforms({ name: _.isEqual("arg1"), type: _.isEqual("any") }),
15
- _.conforms({ name: _.isEqual("arg2"), type: _.isEqual("any") }),
16
- ]),
17
13
  });
14
+ export const argNames = (func) => _.map(_.prop("name"))(func?.params ?? []);
18
15
  const isConditionExpression = (expression, functions) => {
19
16
  const value = _.propOr({}, "value")(expression);
20
17
  const func = _.find({ name: value?.name, type: value?.type })(functions);
@@ -23,29 +20,77 @@ const isConditionExpression = (expression, functions) => {
23
20
  : isConditionFunction(func) || _.isNil(value) || !hasTypeKey(value);
24
21
  };
25
22
 
23
+ const CLASS_ORDER = [
24
+ "comparison",
25
+ "length_comparison",
26
+ "eq_date",
27
+ "neq_date",
28
+ "gt_date",
29
+ "gte_date",
30
+ "lt_date",
31
+ "lte_date",
32
+ "string",
33
+ "format",
34
+ "logic",
35
+ "validation",
36
+ ];
37
+
38
+ const CLASS_ICONS = {
39
+ comparison: "balance scale",
40
+ length_comparison: "arrows alternate horizontal",
41
+ eq_date: "calendar alternate outline",
42
+ neq_date: "calendar alternate outline",
43
+ gt_date: "calendar alternate outline",
44
+ gte_date: "calendar alternate outline",
45
+ lt_date: "calendar alternate outline",
46
+ lte_date: "calendar alternate outline",
47
+ string: "font",
48
+ format: "magic",
49
+ logic: "code branch",
50
+ validation: "check circle outline",
51
+ };
52
+
26
53
  export default function Condition({ onDelete }) {
27
54
  const { formatMessage } = useIntl();
55
+ const [searchQuery, setSearchQuery] = useState("");
28
56
  const context = use(QxContext);
29
57
  const { control, watch, setValue } = useFormContext();
30
- const { field, functions } = context;
58
+ const { field, functions, conditionClasses } = context;
31
59
 
32
60
  const expression = watch(field);
33
61
  const isCondition = isConditionExpression(expression, functions);
34
62
 
35
- const conditionFunctions = _.filter(isConditionFunction)(functions);
36
- const condFuncOptions = [
37
- ..._.map(({ name, operator, description }) => ({
38
- key: name,
39
- text: operator || name,
40
- content: <span title={description}>{operator || name}</span>,
41
- value: name,
42
- }))(conditionFunctions),
43
- {
44
- key: "customExpression",
45
- text: formatMessage({ id: "expression.condition.customExpression" }),
46
- value: "customExpression",
47
- },
48
- ];
63
+ const conditionFunctions = _.filter(
64
+ (fn) =>
65
+ isConditionFunction(fn) && fn.params?.length > 0 && fn.class !== "logic",
66
+ )(functions);
67
+
68
+ const grouped = _.groupBy((fn) => fn.class)(conditionFunctions);
69
+ const allowedClasses = conditionClasses ?? CLASS_ORDER;
70
+ const orderedGroups = allowedClasses.filter(
71
+ (group) => grouped[group]?.length,
72
+ );
73
+
74
+ const selectedFunc = _.find({ name: expression?.value?.name })(
75
+ conditionFunctions,
76
+ );
77
+ const params = selectedFunc?.params ?? [];
78
+ const arity = params.length || 2;
79
+ const argName = (i) => params[i]?.name ?? `arg${i + 1}`;
80
+
81
+ const argContext = (argField) => ({
82
+ ...context,
83
+ type: "any",
84
+ field: `${field}.value.args.${argField}`,
85
+ omitLogicOperators: true,
86
+ defaultShape: "field",
87
+ });
88
+
89
+ const matchesSearch = ({ name, label, operator }) => {
90
+ if (!searchQuery) return true;
91
+ const q = searchQuery.toLowerCase();
92
+ return (operator || label || name).toLowerCase().includes(q);
93
+ };
49
94
 
50
95
  const operatorTrigger = (name) => {
51
96
  const currentCondFunction = _.find({ name })(conditionFunctions);
@@ -54,7 +99,9 @@ export default function Condition({ onDelete }) {
54
99
  {currentCondFunction ? (
55
100
  <b>
56
101
  <code>
57
- {currentCondFunction.operator || currentCondFunction.name}
102
+ {currentCondFunction.operator ||
103
+ currentCondFunction.label ||
104
+ currentCondFunction.name}
58
105
  </code>
59
106
  </b>
60
107
  ) : (
@@ -68,7 +115,7 @@ export default function Condition({ onDelete }) {
68
115
  );
69
116
  };
70
117
  return (
71
- <>
118
+ <div>
72
119
  {!isCondition ? (
73
120
  <QxContext
74
121
  value={{
@@ -80,22 +127,15 @@ export default function Condition({ onDelete }) {
80
127
  <Expression onDelete={onDelete} deletable />
81
128
  </QxContext>
82
129
  ) : (
83
- <Grid className="condition-row">
84
- <Grid.Column width={6}>
85
- <QxContext
86
- value={{
87
- ...context,
88
- type: "any",
89
- field: `${field}.value.args.arg1`,
90
- omitLogicOperators: true,
91
- defaultShape: "field",
92
- }}
93
- >
94
- <Expression />
95
- </QxContext>
96
- </Grid.Column>
97
- <Grid.Column width={3}>
98
- <div className="flex-justify-center">
130
+ <div className="condition-row">
131
+ <div className="condition-fields">
132
+ <div className="condition-arg-field">
133
+ <QxContext value={argContext(argName(0))}>
134
+ <Expression />
135
+ </QxContext>
136
+ </div>
137
+
138
+ <div className="condition-operator-field">
99
139
  <Controller
100
140
  control={control}
101
141
  name={`${field}.value.name`}
@@ -111,73 +151,132 @@ export default function Condition({ onDelete }) {
111
151
  },
112
152
  }}
113
153
  render={({
114
- field: { onBlur, onChange },
154
+ field: { onBlur, onChange, value },
115
155
  fieldState: { error },
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
- }}
156
+ }) => (
157
+ <Form.Field error={!!error?.message}>
158
+ <Dropdown
159
+ fluid
160
+ onClose={() => setSearchQuery("")}
161
+ onBlur={onBlur}
162
+ trigger={operatorTrigger(value)}
163
+ pointing="top left"
164
+ icon={null}
165
+ scrolling
166
+ >
167
+ <Dropdown.Menu>
168
+ <Input
169
+ icon="search"
170
+ iconPosition="left"
171
+ className="search"
172
+ value={searchQuery}
173
+ onChange={(_, { value: q }) => setSearchQuery(q)}
174
+ onClick={(e) => {
175
+ e.preventDefault();
176
+ e.stopPropagation();
177
+ }}
178
+ onKeyDown={(e) => {
179
+ if (e.key === " ") e.stopPropagation();
180
+ }}
181
+ fluid
182
+ />
183
+ <Dropdown.Divider />
184
+ {orderedGroups
185
+ .map((group) => ({
186
+ group,
187
+ fns: grouped[group].filter(matchesSearch),
188
+ }))
189
+ .filter(({ fns }) => fns.length > 0)
190
+ .flatMap(({ group, fns }, renderedIndex) => [
191
+ ...(renderedIndex > 0
192
+ ? [<Dropdown.Divider key={`divider-${group}`} />]
193
+ : []),
194
+ <Dropdown.Header
195
+ key={`header-${group}`}
196
+ icon={CLASS_ICONS[group]}
197
+ content={formatMessage({
198
+ id: `expression.condition.class.${group}`,
199
+ })}
200
+ />,
201
+ ...fns.map(
202
+ ({ name, label, operator, description }) => (
203
+ <Dropdown.Item
204
+ key={name}
205
+ text={operator || label || name}
206
+ value={name}
207
+ active={value === name}
208
+ selected={value === name}
209
+ title={description}
210
+ onClick={() => {
211
+ const fn = _.find({ name })(
212
+ conditionFunctions,
213
+ );
214
+ const newParams = fn?.params ?? [];
215
+ const oldArgs =
216
+ expression?.value?.args ?? {};
217
+ const nextArgs = newParams.reduce(
218
+ (acc, param, i) => ({
219
+ ...acc,
220
+ [param.name]: oldArgs[argName(i)],
221
+ }),
222
+ {},
223
+ );
224
+ onChange(name);
225
+ setValue(`${field}.value.type`, "boolean");
226
+ setValue(
227
+ `${field}.value.isCondition`,
228
+ true,
229
+ );
230
+ setValue(`${field}.value.args`, nextArgs);
231
+ }}
232
+ />
233
+ ),
234
+ ),
235
+ ])}
236
+ </Dropdown.Menu>
237
+ </Dropdown>
238
+ {error?.message && (
239
+ <Label prompt pointing>
240
+ {error?.message}
241
+ </Label>
242
+ )}
243
+ </Form.Field>
244
+ )}
146
245
  />
147
246
  </div>
148
- </Grid.Column>
149
-
150
- <Grid.Column width={6}>
151
- <QxContext
152
- value={{
153
- ...context,
154
- type: "any",
155
- field: `${field}.value.args.arg2`,
156
- omitLogicOperators: true,
157
- defaultShape: "field",
158
- }}
159
- >
160
- <Expression />
161
- </QxContext>
162
- </Grid.Column>
163
-
164
- <Grid.Column width={1} verticalAlign="top">
165
- {onDelete ? (
166
- <div className="delete-expression-column">
167
- <Button
168
- size="mini"
169
- onClick={onDelete}
170
- icon="trash alternate outline"
171
- basic
172
- color="red"
173
- aria-label="delete"
174
- />
175
- </div>
176
- ) : null}
177
- </Grid.Column>
178
- </Grid>
247
+
248
+ <div className="condition-arg-field">
249
+ {arity >= 2 && (
250
+ <QxContext value={argContext(argName(1))}>
251
+ <Expression />
252
+ </QxContext>
253
+ )}
254
+ </div>
255
+
256
+ <div className="condition-arg-field">
257
+ {arity >= 3 && (
258
+ <QxContext value={argContext(argName(2))}>
259
+ <Expression />
260
+ </QxContext>
261
+ )}
262
+ </div>
263
+ </div>
264
+
265
+ <div className="delete-expression-column">
266
+ {onDelete && (
267
+ <Button
268
+ size="mini"
269
+ onClick={onDelete}
270
+ icon="trash alternate outline"
271
+ basic
272
+ color="red"
273
+ aria-label="delete"
274
+ />
275
+ )}
276
+ </div>
277
+ </div>
179
278
  )}
180
- </>
279
+ </div>
181
280
  );
182
281
  }
183
282
 
@@ -2,7 +2,7 @@ import _ from "lodash/fp";
2
2
  import { use } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useFormContext } from "react-hook-form";
5
- import { Button, Grid } from "semantic-ui-react";
5
+ import { Button } from "semantic-ui-react";
6
6
  import QxContext from "@truedat/qx/components/QxContext";
7
7
  import ShapeSelector from "./ShapeSelector";
8
8
  import FunctionSelector from "./FunctionSelector";
@@ -23,28 +23,22 @@ export default function Expression({ onDelete, deletable }) {
23
23
  };
24
24
 
25
25
  return (
26
- <Grid>
27
- <Grid.Column width={deletable ? 15 : 16}>
28
- <div className="expression-container">
29
- {!aggregate ? <ShapeSelector /> : null}
30
- {shape ? valueComponentForShape[shape] : null}
31
- </div>
32
- </Grid.Column>
33
- {deletable ? (
34
- <Grid.Column width={1} verticalAlign="middle">
35
- {onDelete ? (
36
- <Button
37
- size="mini"
38
- onClick={onDelete}
39
- icon="trash alternate outline"
40
- basic
41
- color="red"
42
- aria-label="delete"
43
- />
44
- ) : null}
45
- </Grid.Column>
46
- ) : null}
47
- </Grid>
26
+ <div className="expression-container">
27
+ <div className="expression-content">
28
+ {!aggregate ? <ShapeSelector /> : null}
29
+ {shape ? valueComponentForShape[shape] : null}
30
+ </div>
31
+ {deletable && onDelete && (
32
+ <Button
33
+ size="mini"
34
+ onClick={onDelete}
35
+ icon="trash alternate outline"
36
+ basic
37
+ color="red"
38
+ aria-label="delete"
39
+ />
40
+ )}
41
+ </div>
48
42
  );
49
43
  }
50
44
 
@@ -46,15 +46,15 @@ export default function FunctionSelector() {
46
46
 
47
47
  const funcOpts = _.flow(
48
48
  _.reject({ name: functionName, type: functionType }),
49
- _.map(({ name, type, description, params }) => ({
49
+ _.map(({ name, type, label, operator, description, params }) => ({
50
50
  key: fnToKey({ name, type }),
51
51
  value: fnToKey({ name, type }),
52
- text: fnToKey({ name, type }),
52
+ text: operator || label || name,
53
53
  content: (
54
54
  <>
55
55
  <Header>
56
56
  <div className="flex-center">
57
- <div>{name}</div>
57
+ <div>{operator || label || name}</div>
58
58
  <div className="function-params-label">
59
59
  {params?.map(({ type: paramType, name: paramName }, index) => (
60
60
  <Label key={index} color={typeToColor(paramType)}>
@@ -38,7 +38,7 @@ export default function ShapeSelector() {
38
38
  (shape) =>
39
39
  (shape === "param" && emptyTypeParams) ||
40
40
  (shape === "field" && emptyTypeFields),
41
- )(["constant", "function", "param", "field"]);
41
+ )(["field", "function", "constant", "param"]);
42
42
 
43
43
  const shape = watch(`${field}.shape`);
44
44
 
@@ -96,7 +96,7 @@ describe("<Clauses />", () => {
96
96
  ),
97
97
  );
98
98
  await waitFor(() =>
99
- expect(rendered.getAllByLabelText(/delete/i)).toHaveLength(2),
99
+ expect(rendered.getAllByLabelText(/delete/i)).toHaveLength(1),
100
100
  );
101
101
 
102
102
  await act(async () => {
@@ -1,3 +1,4 @@
1
+ import _ from "lodash/fp";
1
2
  import userEvent from "@testing-library/user-event";
2
3
  import { waitFor } from "@testing-library/react";
3
4
  import PropTypes from "prop-types";
@@ -6,6 +7,27 @@ import { useFormContext } from "react-hook-form";
6
7
  import TestFormWrapper from "@truedat/qx/components/common/TestFormWrapper";
7
8
  import Condition, { isConditionFunction } from "../Condition";
8
9
 
10
+ const eq = {
11
+ name: "eq",
12
+ type: "boolean",
13
+ class: "comparison",
14
+ params: [
15
+ { name: "arg1", type: "any" },
16
+ { name: "arg2", type: "any" },
17
+ ],
18
+ };
19
+
20
+ const lengthGt = {
21
+ name: "length_gt",
22
+ type: "boolean",
23
+ class: "length_comparison",
24
+ label: "LengthGreater",
25
+ params: [
26
+ { name: "value", type: "string" },
27
+ { name: "length", type: "number" },
28
+ ],
29
+ };
30
+
9
31
  function ValidationTrigger({ name }) {
10
32
  const { trigger } = useFormContext();
11
33
  return (
@@ -63,6 +85,7 @@ describe("<Condition />", () => {
63
85
  {
64
86
  name: "eq",
65
87
  type: "boolean",
88
+ class: "comparison",
66
89
  params: [
67
90
  { name: "arg1", type: "any" },
68
91
  { name: "arg2", type: "any" },
@@ -100,6 +123,7 @@ describe("<Condition />", () => {
100
123
  {
101
124
  name: "eq",
102
125
  type: "boolean",
126
+ class: "comparison",
103
127
  params: [
104
128
  { name: "arg1", type: "any" },
105
129
  { name: "arg2", type: "any" },
@@ -133,7 +157,7 @@ describe("<Condition />", () => {
133
157
  );
134
158
  });
135
159
 
136
- it("switches from condition to custom expression option", async () => {
160
+ it("marks selected operator as condition function", async () => {
137
161
  const user = userEvent.setup({ delay: null });
138
162
  const formValuesRef = { current: {} };
139
163
  const rendered = render(
@@ -144,6 +168,7 @@ describe("<Condition />", () => {
144
168
  {
145
169
  name: "eq",
146
170
  type: "boolean",
171
+ class: "comparison",
147
172
  params: [
148
173
  { name: "arg1", type: "any" },
149
174
  { name: "arg2", type: "any" },
@@ -155,7 +180,7 @@ describe("<Condition />", () => {
155
180
  test: {
156
181
  shape: "function",
157
182
  value: {
158
- name: "eq",
183
+ name: "",
159
184
  type: "boolean",
160
185
  isCondition: true,
161
186
  args: { arg1: { shape: "field" }, arg2: { shape: "field" } },
@@ -171,42 +196,77 @@ describe("<Condition />", () => {
171
196
  );
172
197
  await waitForLoad(rendered);
173
198
 
174
- await user.click(rendered.getAllByRole("listbox")[1]);
175
199
  await user.click(
176
- rendered.getByText(/expression.condition.customexpression/i),
200
+ rendered.getByText(/expression.condition.selectfunction/i),
177
201
  );
202
+ await user.click(rendered.getByText(/eq/i));
178
203
 
179
204
  await waitFor(() =>
180
- expect(formValuesRef.current.test.value).toEqual({ isCondition: false }),
205
+ expect(formValuesRef.current.test.value).toEqual(
206
+ expect.objectContaining({
207
+ name: "eq",
208
+ type: "boolean",
209
+ isCondition: true,
210
+ }),
211
+ ),
181
212
  );
182
213
  });
183
214
 
184
- it("marks selected operator as condition function", async () => {
185
- const user = userEvent.setup({ delay: null });
215
+ it("names args after the function params, not their position", async () => {
186
216
  const formValuesRef = { current: {} };
187
217
  const rendered = render(
188
218
  <TestFormWrapper
189
219
  context={{
190
220
  field: "test",
191
- functions: [
192
- {
193
- name: "eq",
221
+ functions: [lengthGt],
222
+ }}
223
+ defaultValues={{
224
+ test: {
225
+ shape: "function",
226
+ value: {
227
+ name: "length_gt",
194
228
  type: "boolean",
195
- params: [
196
- { name: "arg1", type: "any" },
197
- { name: "arg2", type: "any" },
198
- ],
229
+ isCondition: true,
199
230
  },
200
- ],
231
+ },
232
+ }}
233
+ watcher={(values) => {
234
+ formValuesRef.current = values;
235
+ }}
236
+ >
237
+ <Condition />
238
+ </TestFormWrapper>,
239
+ );
240
+ await waitForLoad(rendered);
241
+
242
+ await waitFor(() =>
243
+ expect(_.keys(formValuesRef.current.test.value.args)).toEqual([
244
+ "value",
245
+ "length",
246
+ ]),
247
+ );
248
+ });
249
+
250
+ it("remaps operands onto the new param names when the operator changes", async () => {
251
+ const user = userEvent.setup({ delay: null });
252
+ const formValuesRef = { current: {} };
253
+ const rendered = render(
254
+ <TestFormWrapper
255
+ context={{
256
+ field: "test",
257
+ functions: [eq, lengthGt],
201
258
  }}
202
259
  defaultValues={{
203
260
  test: {
204
261
  shape: "function",
205
262
  value: {
206
- name: "",
263
+ name: "eq",
207
264
  type: "boolean",
208
265
  isCondition: true,
209
- args: { arg1: { shape: "field" }, arg2: { shape: "field" } },
266
+ args: {
267
+ arg1: { shape: "constant", value: { value: "left" } },
268
+ arg2: { shape: "constant", value: { value: "right" } },
269
+ },
210
270
  },
211
271
  },
212
272
  }}
@@ -220,18 +280,15 @@ describe("<Condition />", () => {
220
280
  await waitForLoad(rendered);
221
281
 
222
282
  await user.click(
223
- rendered.getByText(/expression.condition.selectfunction/i),
283
+ rendered.container.querySelector(".condition-operator-trigger"),
224
284
  );
225
- await user.click(rendered.getByText(/eq/i));
285
+ await user.click(rendered.getByText("LengthGreater"));
226
286
 
227
287
  await waitFor(() =>
228
- expect(formValuesRef.current.test.value).toEqual(
229
- expect.objectContaining({
230
- name: "eq",
231
- type: "boolean",
232
- isCondition: true,
233
- }),
234
- ),
288
+ expect(formValuesRef.current.test.value.args).toEqual({
289
+ value: { shape: "constant", value: { value: "left" } },
290
+ length: { shape: "constant", value: { value: "right" } },
291
+ }),
235
292
  );
236
293
  });
237
294
  });
@@ -84,7 +84,7 @@ describe("<ParamSelector />", () => {
84
84
  test: { value: { id: 1 } },
85
85
  });
86
86
 
87
- await user.hover(rendered.getByRole("textbox"));
87
+ await user.hover(rendered.getByPlaceholderText("functions.form.name"));
88
88
  await user.click(rendered.getAllByRole("button")[0]);
89
89
 
90
90
  expect(watcher).toHaveBeenLastCalledWith({