@truedat/qx 7.13.9 → 7.14.0

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 (46) hide show
  1. package/package.json +3 -3
  2. package/src/components/common/ClauseViewer.js +183 -21
  3. package/src/components/common/expressions/Condition.js +13 -6
  4. package/src/components/dataViews/DataViewEditor.js +0 -2
  5. package/src/components/dataViews/DataViewSummary.js +73 -0
  6. package/src/components/dataViews/__tests__/AdvancedDataViewEditor.spec.js +4 -1
  7. package/src/components/dataViews/__tests__/DataViewEditor.spec.js +167 -132
  8. package/src/components/dataViews/__tests__/DataViewSummary.spec.js +820 -0
  9. package/src/components/dataViews/__tests__/DataViews.spec.js +57 -17
  10. package/src/components/dataViews/__tests__/SimpleDataViewEditor.spec.js +140 -141
  11. package/src/components/dataViews/__tests__/__snapshots__/AdvancedDataViewEditor.spec.js.snap +963 -759
  12. package/src/components/dataViews/__tests__/__snapshots__/DataViewSelect.spec.js.snap +17 -13
  13. package/src/components/dataViews/__tests__/__snapshots__/DataViewSummary.spec.js.snap +1786 -0
  14. package/src/components/dataViews/__tests__/__snapshots__/Queryable.spec.js.snap +18 -14
  15. package/src/components/dataViews/__tests__/__snapshots__/Queryables.spec.js.snap +18 -14
  16. package/src/components/dataViews/advancedForm/AdvancedDataViewEditor.js +59 -48
  17. package/src/components/dataViews/queryableProperties/Join.js +2 -1
  18. package/src/components/dataViews/queryableProperties/Select.js +22 -30
  19. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Join.spec.js.snap +1 -1
  20. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Select.spec.js.snap +37 -25
  21. package/src/components/dataViews/queryableSummaryHelpers.js +101 -0
  22. package/src/components/dataViews/simpleForm/SimpleDataViewEditor.js +9 -4
  23. package/src/components/dataViews/summary/From.js +45 -0
  24. package/src/components/dataViews/summary/GroupBy.js +82 -0
  25. package/src/components/dataViews/summary/Join.js +60 -0
  26. package/src/components/dataViews/summary/Select.js +31 -0
  27. package/src/components/dataViews/summary/Where.js +37 -0
  28. package/src/components/qualityControls/ControlPropertiesView.js +115 -63
  29. package/src/components/qualityControls/EditQualityControl.js +5 -3
  30. package/src/components/qualityControls/NewDraftQualityControl.js +8 -3
  31. package/src/components/qualityControls/NewQualityControl.js +5 -3
  32. package/src/components/qualityControls/QualityControlCrumbs.js +46 -5
  33. package/src/components/qualityControls/QualityControlRoutes.js +3 -1
  34. package/src/components/qualityControls/QualityControls.js +9 -18
  35. package/src/components/qualityControls/QualityControlsLabelResults.js +2 -2
  36. package/src/components/qualityControls/__tests__/__snapshots__/ControlPropertiesView.spec.js.snap +12 -9
  37. package/src/components/qualityControls/__tests__/__snapshots__/EditQualityControl.spec.js.snap +536 -493
  38. package/src/components/qualityControls/__tests__/__snapshots__/NewDraftQualityControl.spec.js.snap +510 -483
  39. package/src/components/qualityControls/__tests__/__snapshots__/NewQualityControl.spec.js.snap +261 -245
  40. package/src/components/qualityControls/__tests__/__snapshots__/QualityControl.spec.js.snap +11 -8
  41. package/src/components/qualityControls/__tests__/__snapshots__/QualityControlCrumbs.spec.js.snap +1 -1
  42. package/src/components/qualityControls/__tests__/__snapshots__/QualityControlHeader.spec.js.snap +1 -1
  43. package/src/components/qualityControls/__tests__/__snapshots__/QualityControls.spec.js.snap +87 -87
  44. package/src/components/qualityControls/__tests__/__snapshots__/QualityControlsLabelResults.spec.js.snap +6 -2
  45. package/src/hooks/useDataViews.js +1 -1
  46. package/src/styles/Expression.less +25 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/qx",
3
- "version": "7.13.9",
3
+ "version": "7.14.0",
4
4
  "description": "Truedat Web Quality Experience package",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -53,7 +53,7 @@
53
53
  "@testing-library/jest-dom": "^6.6.3",
54
54
  "@testing-library/react": "^16.3.0",
55
55
  "@testing-library/user-event": "^14.6.1",
56
- "@truedat/test": "7.13.9",
56
+ "@truedat/test": "7.14.0",
57
57
  "identity-obj-proxy": "^3.0.0",
58
58
  "jest": "^29.7.0",
59
59
  "redux-saga-test-plan": "^4.0.6"
@@ -86,5 +86,5 @@
86
86
  "semantic-ui-react": "^3.0.0-beta.2",
87
87
  "swr": "^2.3.3"
88
88
  },
89
- "gitHead": "72faa643a2dab00d15490cd15abb85d89364e375"
89
+ "gitHead": "861787da648eec4a053e7982c59e1b140316d160"
90
90
  }
@@ -1,41 +1,96 @@
1
1
  import _ from "lodash/fp";
2
2
  import { Fragment, use } from "react";
3
3
  import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
4
5
  import { Divider, Label, Segment } from "semantic-ui-react";
5
6
  import { useFunctions } from "@truedat/qx/hooks/useFunctions";
6
- import QxContext from "@truedat/qx/components/QxContext";
7
7
  import { isConditionFunction } from "@truedat/qx/components/common/expressions/Condition";
8
+ import { getColorById } from "../dataViews/queryableFunctions";
9
+ import QxContext from "@truedat/qx/components/QxContext";
10
+ import "@truedat/qx/styles/Expression.less";
11
+
12
+ const isExpressionValid = (expression, functions = []) => {
13
+ if (_.isNil(expression?.shape)) {
14
+ return false;
15
+ }
16
+
17
+ const { shape, value } = expression;
18
+
19
+ if (_.isNil(value)) {
20
+ return true;
21
+ }
8
22
 
9
- export default function ClauseViewer({ clause }) {
23
+ switch (shape) {
24
+ case "function": {
25
+ if (!value.name || !value.type) {
26
+ return false;
27
+ }
28
+ const func = _.find({ name: value.name, type: value.type })(functions);
29
+ if (func && isConditionFunction(func)) {
30
+ return (
31
+ isExpressionValid(value.args?.arg1, functions) &&
32
+ isExpressionValid(value.args?.arg2, functions)
33
+ );
34
+ }
35
+
36
+ return true;
37
+ }
38
+ case "field":
39
+ return !_.isEmpty(value?.name);
40
+ case "constant":
41
+ return !_.isNil(value?.value) && !_.isEmpty(value?.value);
42
+ case "param":
43
+ return !_.isNil(value?.id);
44
+ default:
45
+ return false;
46
+ }
47
+ };
48
+
49
+ export default function ClauseViewer({ clause, parentResource }) {
10
50
  const { data, loading } = useFunctions();
11
51
  const functions = data?.data;
52
+ const context = use(QxContext);
12
53
 
13
54
  const _map = _.map.convert({ cap: false });
55
+
56
+ const validClauseGroups = _.flow(
57
+ _.map((group) => ({
58
+ ...group,
59
+ expressions: _.filter((expr) => isExpressionValid(expr, functions))(
60
+ group.expressions || []
61
+ ),
62
+ })),
63
+ _.filter((group) => _.size(group.expressions) > 0)
64
+ )(clause || []);
65
+
14
66
  return (
15
- <Segment loading={loading} compact basic>
67
+ <Segment loading={loading} compact basic className="text-break-word">
16
68
  {loading
17
69
  ? null
18
70
  : _map(({ expressions }, idx) => (
19
- <QxContext value={{ functions }} key={idx}>
20
- <Segment textAlign="center" className="no-margin">
71
+ <QxContext
72
+ value={{ ...context, functions, parentResource }}
73
+ key={idx}
74
+ >
75
+ <Segment className="no-margin text-break-word">
21
76
  {_map((expression, idx) => (
22
77
  <Fragment key={idx}>
23
78
  <ExpressionViewer expression={expression} />
24
79
  {idx == _.size(expressions) - 1 ? null : (
25
80
  <Divider horizontal className="divider-compact">
26
- OR
81
+ AND
27
82
  </Divider>
28
83
  )}
29
84
  </Fragment>
30
85
  ))(expressions)}
31
86
  </Segment>
32
- {idx == _.size(clause) - 1 ? null : (
87
+ {idx == _.size(validClauseGroups) - 1 ? null : (
33
88
  <Divider horizontal className="divider-compact">
34
- AND
89
+ OR
35
90
  </Divider>
36
91
  )}
37
92
  </QxContext>
38
- ))(clause)}
93
+ ))(validClauseGroups)}
39
94
  </Segment>
40
95
  );
41
96
  }
@@ -44,20 +99,46 @@ ClauseViewer.propTypes = {
44
99
  clause: PropTypes.array,
45
100
  };
46
101
 
47
- function ExpressionViewer({ expression }) {
102
+ export function ExpressionViewer({ expression, alias }) {
103
+ const { formatMessage } = useIntl();
104
+
105
+ if (_.isNil(expression?.shape)) {
106
+ return null;
107
+ }
108
+
109
+ if (_.isNil(expression?.value)) {
110
+ return (
111
+ <Label basic color="grey">
112
+ {formatMessage({ id: "expression.viewer.undefined" })}
113
+ </Label>
114
+ );
115
+ }
116
+
48
117
  const componentForShape = {
49
- function: (value) => <ExpressionFunctionViewer value={value} />,
118
+ function: (value) => (
119
+ <ExpressionFunctionViewer value={value} alias={alias} />
120
+ ),
50
121
  constant: (value) => <ExpressionConstantViewer value={value} />,
51
- field: (value) => <ExpressionFieldViewer value={value} />,
122
+ field: (value) => <ExpressionFieldViewer value={value} alias={alias} />,
52
123
  };
53
- return componentForShape[expression.shape](expression.value);
124
+
125
+ const handler = componentForShape[expression.shape];
126
+ if (!handler) {
127
+ return (
128
+ <Label basic color="orange">
129
+ {expression.shape}
130
+ </Label>
131
+ );
132
+ }
133
+
134
+ return handler(expression.value);
54
135
  }
55
136
 
56
137
  ExpressionViewer.propTypes = {
57
138
  expression: PropTypes.object,
58
139
  };
59
140
 
60
- function ExpressionFunctionViewer({ value }) {
141
+ function ExpressionFunctionViewer({ value, alias }) {
61
142
  const { functions } = use(QxContext);
62
143
  const { name, type } = value;
63
144
  const func = _.find({ name, type })(functions);
@@ -67,19 +148,41 @@ function ExpressionFunctionViewer({ value }) {
67
148
  operator={func?.operator}
68
149
  />
69
150
  ) : (
70
- <GenericExpressionFunctionViewer value={value} />
151
+ <GenericExpressionFunctionViewer value={value} alias={alias} />
71
152
  );
72
153
  }
73
154
  ExpressionFunctionViewer.propTypes = {
74
155
  value: PropTypes.object,
75
156
  };
76
157
 
77
- function GenericExpressionFunctionViewer({ value }) {
158
+ function GenericExpressionFunctionViewer({ alias, value }) {
159
+ const { formatMessage } = useIntl();
160
+ if (!value || !value.name) {
161
+ return (
162
+ <Label basic color="grey">
163
+ {formatMessage({ id: "expression.viewer.function.undefined" })}
164
+ </Label>
165
+ );
166
+ }
167
+
78
168
  return (
79
169
  <div className="text-align-left">
80
- <Label color="teal" horizontal size="tiny">
170
+ <Label
171
+ horizontal
172
+ size="tiny"
173
+ className="text-break-word"
174
+ title={value.name}
175
+ >
81
176
  {value.name}
82
177
  </Label>
178
+ {alias && (
179
+ <>
180
+ <b style={{ marginRight: 4, marginLeft: 4 }}>as</b>
181
+ <Label size="tiny" className="text-break-word">
182
+ {alias}
183
+ </Label>
184
+ </>
185
+ )}
83
186
  (
84
187
  <div className="clause-viewer-function">
85
188
  {_.flow(
@@ -101,6 +204,15 @@ GenericExpressionFunctionViewer.propTypes = {
101
204
  value: PropTypes.object,
102
205
  };
103
206
  function ConditionExpressionFunctionViewer({ value, operator }) {
207
+ const { formatMessage } = useIntl();
208
+ if (!value || !value.args) {
209
+ return (
210
+ <Label basic color="grey">
211
+ {formatMessage({ id: "expression.viewer.condition.undefined" })}
212
+ </Label>
213
+ );
214
+ }
215
+
104
216
  return (
105
217
  <div className="condition-function-viewer">
106
218
  <ExpressionViewer expression={value.args.arg1} />
@@ -115,15 +227,65 @@ ConditionExpressionFunctionViewer.propTypes = {
115
227
  };
116
228
 
117
229
  function ExpressionConstantViewer({ value }) {
118
- return <Label color="purple">{value.value}</Label>;
230
+ const { formatMessage } = useIntl();
231
+ if (!value || _.isNil(value.value)) {
232
+ return (
233
+ <Label basic color="grey">
234
+ {formatMessage({ id: "expression.viewer.constant.undefined" })}
235
+ </Label>
236
+ );
237
+ }
238
+ return (
239
+ <Label basic className="text-break-word">
240
+ {value.value}
241
+ </Label>
242
+ );
119
243
  }
120
244
  ExpressionConstantViewer.propTypes = {
121
245
  value: PropTypes.object,
122
246
  };
123
247
 
124
- function ExpressionFieldViewer({ value }) {
125
- return <Label color="blue">{value.name}</Label>;
248
+ const getParent = (parent_id, parentResource, parentResourceMap) => {
249
+ if (parentResource) {
250
+ return { ...parentResource, label: parentResource?.embedded?.name };
251
+ }
252
+ if (parentResourceMap) {
253
+ return parentResourceMap[parent_id];
254
+ }
255
+ return null;
256
+ };
257
+
258
+ export function ExpressionFieldViewer({ value, alias }) {
259
+ const { formatMessage } = useIntl();
260
+ const { parentResourceMap, parentResource } = use(QxContext);
261
+
262
+ if (!value || !value.name) {
263
+ return (
264
+ <Label basic color="grey">
265
+ {formatMessage({ id: "expression.viewer.field.undefined" })}
266
+ </Label>
267
+ );
268
+ }
269
+
270
+ const parent = getParent(value.parent_id, parentResource, parentResourceMap);
271
+ const color = getColorById(parent?.id);
272
+ const labelContent = parent ? `${parent.label}.${value.name}` : value.name;
273
+ return (
274
+ <>
275
+ <Label color={color} className="text-break-word" title={labelContent}>
276
+ {labelContent}
277
+ </Label>
278
+ {alias && (
279
+ <>
280
+ <b style={{ marginRight: 4, marginLeft: 4 }}>as</b>
281
+ <Label color={color} className="text-break-word">
282
+ {alias}
283
+ </Label>
284
+ </>
285
+ )}
286
+ </>
287
+ );
126
288
  }
127
289
  ExpressionFieldViewer.propTypes = {
128
- value: PropTypes.object,
290
+ field: PropTypes.object,
129
291
  };
@@ -106,8 +106,12 @@ export default function Condition({ onDelete }) {
106
106
  rules={{
107
107
  validate: (v) => {
108
108
  const name = v ?? "";
109
- const isValid = Boolean(name) || expression?.value?.isCondition === false;
110
- return isValid || formatMessage({ id: "functions.form.required" });
109
+ const isValid =
110
+ Boolean(name) || expression?.value?.isCondition === false;
111
+ return (
112
+ isValid ||
113
+ formatMessage({ id: "functions.form.required" })
114
+ );
111
115
  },
112
116
  }}
113
117
  render={({
@@ -122,7 +126,7 @@ export default function Condition({ onDelete }) {
122
126
  fluid
123
127
  onChange={(_e, { value }) => {
124
128
  if (value == "customExpression") {
125
- setValue(`${field}.value`, { ...(expression?.value || {}), isCondition: false });
129
+ setValue(`${field}.value`, { isCondition: false });
126
130
  } else {
127
131
  onChange(value);
128
132
  setValue(`${field}.value.type`, "boolean");
@@ -133,10 +137,13 @@ export default function Condition({ onDelete }) {
133
137
  pointing="top left"
134
138
  icon={null}
135
139
  />
136
- {error?.message && <Label prompt pointing>{error?.message}</Label>}
140
+ {error?.message && (
141
+ <Label prompt pointing>
142
+ {error?.message}
143
+ </Label>
144
+ )}
137
145
  </Form.Field>
138
- )
139
- }
146
+ )}
140
147
  />
141
148
  </div>
142
149
  </Grid.Column>
@@ -109,7 +109,6 @@ export function DataViewDetailEditor({ dataView }) {
109
109
  disabled={viewMode === "advanced" || mode == "guided"}
110
110
  icon="compass"
111
111
  content={formatMessage({ id: "actions.mode.guided" })}
112
-
113
112
  />
114
113
  }
115
114
  header={formatMessage({ id: "actions.mode.confirmation.header" })}
@@ -127,7 +126,6 @@ export function DataViewDetailEditor({ dataView }) {
127
126
  disabled={mode === "advanced"}
128
127
  icon="code"
129
128
  content={formatMessage({ id: "actions.mode.advanced" })}
130
-
131
129
  />
132
130
  }
133
131
  header={formatMessage({ id: "actions.mode.confirmation.header" })}
@@ -0,0 +1,73 @@
1
+ import _ from "lodash/fp";
2
+ import { Fragment } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Segment, List, Divider } from "semantic-ui-react";
5
+ import From from "./summary/From";
6
+ import Join from "./summary/Join";
7
+ import Where from "./summary/Where";
8
+ import GroupBy from "./summary/GroupBy";
9
+ import Select from "./summary/Select";
10
+ import {
11
+ buildParentResourceMap,
12
+ hasValidStructure,
13
+ } from "./queryableSummaryHelpers";
14
+ import QxContext from "@truedat/qx/components/QxContext";
15
+
16
+ export default function DataViewSummary({ dataView }) {
17
+ if (!dataView) {
18
+ return null;
19
+ }
20
+
21
+ const queryables = dataView.queryables;
22
+ if (!hasValidStructure(queryables)) {
23
+ return null;
24
+ }
25
+
26
+ const parentResourceMap = buildParentResourceMap(queryables);
27
+
28
+ const clauseTypeForWhere = (index, queryables) => {
29
+ const previousQueryable = queryables[index - 1];
30
+ if (previousQueryable.type === "group_by") {
31
+ return "having";
32
+ }
33
+ return "where";
34
+ };
35
+
36
+ return (
37
+ <QxContext value={{ parentResourceMap }}>
38
+ <Segment>
39
+ <List>
40
+ {queryables.map((queryable, index) => (
41
+ <Fragment key={index}>
42
+ {queryable.type === "from" && <From queryable={queryable} />}
43
+ {queryable.type === "join" && <Join queryable={queryable} />}
44
+ {queryable.type === "where" && (
45
+ <Where
46
+ queryable={queryable}
47
+ clauseType={clauseTypeForWhere(index, queryables)}
48
+ />
49
+ )}
50
+ {queryable.type === "group_by" && (
51
+ <GroupBy queryable={queryable} />
52
+ )}
53
+ {queryable.type === "select" && <Select queryable={queryable} />}
54
+ {index < queryables.length - 1 && (
55
+ <Divider key={`divider-${index}`} />
56
+ )}
57
+ </Fragment>
58
+ ))}
59
+ {!_.isEmpty(dataView.select?.properties?.fields) && (
60
+ <>
61
+ <Divider />
62
+ <Select queryable={dataView.select} />
63
+ </>
64
+ )}
65
+ </List>
66
+ </Segment>
67
+ </QxContext>
68
+ );
69
+ }
70
+
71
+ DataViewSummary.propTypes = {
72
+ dataView: PropTypes.object,
73
+ };
@@ -157,7 +157,10 @@ describe("<AdvancedDataViewEditor />", () => {
157
157
  await user.type(rendered.getAllByRole("textbox")[0], "data_view_name");
158
158
 
159
159
  // Insert description
160
- await user.type(rendered.getAllByRole("textbox")[1], "data_view_description");
160
+ await user.type(
161
+ rendered.getAllByRole("textbox")[1],
162
+ "data_view_description"
163
+ );
161
164
 
162
165
  // Insert From information
163
166
  await user.type(rendered.getByPlaceholderText(/alias/i), "from_alias");