@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/qx",
3
- "version": "8.10.2",
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.2",
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": "40436c0d8d650f31421d9695035f2d54e5b1c28c"
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 Functions from "./functions/Functions";
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
- <ProtectedRoute authorization="quality_control">
34
- <Functions />
35
- </ProtectedRoute>
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/Functions", () => () => <div>Functions</div>);
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":
@@ -13,7 +13,7 @@ exports[`<QxRoutes /> renders correctly with default route 1`] = `<div />`;
13
13
  exports[`<QxRoutes /> renders correctly with functions route 1`] = `
14
14
  <div>
15
15
  <div>
16
- Functions
16
+ FunctionsList
17
17
  </div>
18
18
  </div>
19
19
  `;
@@ -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 { isConditionFunction } from "@truedat/qx/components/common/expressions/Condition";
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?.arg1, functions) &&
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.arg1} />
219
- <b className="font-big">{operator}</b>
220
- <ExpressionViewer expression={value.args.arg2} />
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, List } from "semantic-ui-react";
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
- <List link horizontal>
16
- {!editionMode ? (
17
- <List.Item
18
- as="a"
19
- tabIndex={0}
20
- onClick={() => setEditMode(true)}
21
- onFocus={() => setEditMode(true)}
22
- style={{ fontStyle: "italic", fontSize: 13, padding: "9.5px 14px" }}
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((value) => withoutTypeAny && value == "any"),
18
- _.map((value) => ({
19
- key: value,
20
- text: formatMessage({ id: `expressions.data_type.${value}` }),
21
- value,
22
- icon: typeToIcon(value),
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.click(rendered.getByRole("list"));
31
+ await user.tab();
33
32
 
34
- const [_event, { value: lastValue }] = props.onChange.mock.lastCall;
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 horizontal link list"
7
- role="list"
6
+ class="ui fluid input"
8
7
  >
9
- <a
10
- class="item"
11
- role="listitem"
12
- style="font-style: italic; font-size: 13px; padding: 9.5px 14px;"
13
- tabindex="0"
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 { useState, use } from "react";
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
- <div className="clause-and-group">
62
- {removeGroup ? (
63
- <Button
64
- size="tiny"
65
- onClick={() => removeGroup(groupIndex)}
66
- icon="trash alternate outline"
67
- basic
68
- color="red"
69
- aria-label="delete"
70
- />
71
- ) : null}
72
- </div>
73
- <Divider hidden />
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
- expressionIndex > 0 ? () => remove(expressionIndex) : null
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