@truedat/qx 7.13.7 → 7.13.9

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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/components/QxRoutes.js +8 -6
  3. package/src/components/common/ResourceSelector.js +25 -6
  4. package/src/components/common/TypeSelector.js +14 -9
  5. package/src/components/common/__tests__/__snapshots__/ResourceSelector.spec.js.snap +269 -241
  6. package/src/components/common/__tests__/__snapshots__/TypeSelector.spec.js.snap +198 -190
  7. package/src/components/common/expressions/Clauses.js +19 -11
  8. package/src/components/common/expressions/Condition.js +32 -31
  9. package/src/components/common/expressions/FieldSelector.js +30 -16
  10. package/src/components/common/expressions/FunctionSelector.js +38 -23
  11. package/src/components/common/expressions/ShapeSelector.js +6 -5
  12. package/src/components/common/expressions/__tests__/ShapeSelector.spec.js +1 -1
  13. package/src/components/common/expressions/__tests__/__snapshots__/Clauses.spec.js.snap +36 -12
  14. package/src/components/common/expressions/__tests__/__snapshots__/Condition.spec.js.snap +87 -75
  15. package/src/components/common/expressions/__tests__/__snapshots__/ConstantSelector.spec.js.snap +99 -97
  16. package/src/components/common/expressions/__tests__/__snapshots__/Expression.spec.js.snap +236 -216
  17. package/src/components/common/expressions/__tests__/__snapshots__/FunctionArgs.spec.js.snap +97 -89
  18. package/src/components/common/expressions/__tests__/__snapshots__/FunctionSelector.spec.js.snap +373 -345
  19. package/src/components/common/expressions/constantInputs/AnySelector.js +2 -1
  20. package/src/components/common/expressions/constantInputs/BooleanSelector.js +20 -15
  21. package/src/components/common/expressions/constantInputs/DefaultSelector.js +0 -1
  22. package/src/components/common/expressions/constantInputs/__tests__/__snapshots__/AnySelector.spec.js.snap +189 -182
  23. package/src/components/common/expressions/constantInputs/__tests__/__snapshots__/BooleanSelector.spec.js.snap +74 -66
  24. package/src/components/common/expressions/constantInputs/__tests__/__snapshots__/DefaultSelector.spec.js.snap +2 -4
  25. package/src/components/common/resourceSelectors/DataStructureSelector.js +5 -4
  26. package/src/components/common/resourceSelectors/DataViewSelector.js +4 -3
  27. package/src/components/common/resourceSelectors/ReferenceDatasetSelector.js +4 -3
  28. package/src/components/common/resourceSelectors/__tests__/__snapshots__/DataStructureSelector.spec.js.snap +65 -61
  29. package/src/components/common/resourceSelectors/__tests__/__snapshots__/DataViewSelector.spec.js.snap +38 -34
  30. package/src/components/common/resourceSelectors/__tests__/__snapshots__/ReferenceDatasetSelector.spec.js.snap +38 -34
  31. package/src/components/dataViews/BreadCrumb.js +20 -0
  32. package/src/components/dataViews/DataViewEditor.js +169 -178
  33. package/src/components/dataViews/DataViews.js +113 -135
  34. package/src/components/dataViews/__tests__/AdvancedDataViewEditor.spec.js +260 -0
  35. package/src/components/dataViews/__tests__/DataViewEditor.spec.js +173 -239
  36. package/src/components/dataViews/__tests__/DataViewSelect.spec.js +1 -1
  37. package/src/components/dataViews/__tests__/DataViews.spec.js +124 -51
  38. package/src/components/dataViews/__tests__/Queryable.spec.js +1 -1
  39. package/src/components/dataViews/__tests__/Queryables.spec.js +1 -1
  40. package/src/components/dataViews/__tests__/SimpleDataViewEditor.spec.js +164 -0
  41. package/src/components/dataViews/__tests__/__snapshots__/{DataViewEditor.spec.js.snap → AdvancedDataViewEditor.spec.js.snap} +230 -200
  42. package/src/components/dataViews/__tests__/__snapshots__/DataViews.spec.js.snap +141 -29
  43. package/src/components/dataViews/__tests__/__snapshots__/Queryable.spec.js.snap +184 -141
  44. package/src/components/dataViews/__tests__/__snapshots__/Queryables.spec.js.snap +126 -91
  45. package/src/components/dataViews/actions/CancelButton.js +33 -0
  46. package/src/components/dataViews/actions/DeleteButton.js +33 -0
  47. package/src/components/dataViews/advancedForm/AdvancedDataViewEditor.js +159 -0
  48. package/src/components/dataViews/{DataViewSelect.js → advancedForm/DataViewSelect.js} +2 -2
  49. package/src/components/dataViews/{Queryable.js → advancedForm/Queryable.js} +2 -2
  50. package/src/components/dataViews/queryableFunctions.js +7 -0
  51. package/src/components/dataViews/queryableProperties/GroupBy.js +23 -27
  52. package/src/components/dataViews/queryableProperties/Join.js +0 -3
  53. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/From.spec.js.snap +30 -26
  54. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/GroupBy.spec.js.snap +130 -102
  55. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Join.spec.js.snap +42 -31
  56. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Select.spec.js.snap +81 -69
  57. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/SelectField.spec.js.snap +62 -54
  58. package/src/components/dataViews/queryableProperties/__tests__/__snapshots__/Where.spec.js.snap +12 -4
  59. package/src/components/dataViews/simpleForm/AggregationForm.js +179 -0
  60. package/src/components/dataViews/simpleForm/DatasetForm.js +199 -0
  61. package/src/components/dataViews/simpleForm/FormQueryable.js +114 -0
  62. package/src/components/dataViews/simpleForm/InformationForm.js +107 -0
  63. package/src/components/dataViews/simpleForm/SelectionForm.js +50 -0
  64. package/src/components/dataViews/simpleForm/SimpleDataViewEditor.js +265 -0
  65. package/src/components/functions/__tests__/__snapshots__/FunctionEditor.spec.js.snap +663 -631
  66. package/src/components/functions/__tests__/__snapshots__/FunctionParams.spec.js.snap +113 -109
  67. package/src/components/qualityControls/__tests__/__snapshots__/ControlProperties.spec.js.snap +92 -76
  68. package/src/components/qualityControls/__tests__/__snapshots__/EditQualityControl.spec.js.snap +108 -80
  69. package/src/components/qualityControls/__tests__/__snapshots__/NewDraftQualityControl.spec.js.snap +108 -80
  70. package/src/components/qualityControls/__tests__/__snapshots__/QualityControlEditor.spec.js.snap +108 -80
  71. package/src/components/qualityControls/controlProperties/__tests__/__snapshots__/Count.spec.js.snap +40 -36
  72. package/src/components/qualityControls/controlProperties/__tests__/__snapshots__/Ratio.spec.js.snap +92 -76
  73. package/src/hooks/useDataViews.js +7 -0
  74. /package/src/components/dataViews/{Queryables.js → advancedForm/Queryables.js} +0 -0
@@ -3,120 +3,124 @@
3
3
  exports[`<TypeSelector /> matches the latest snapshot 1`] = `
4
4
  <div>
5
5
  <div
6
- aria-expanded="false"
7
- class="ui basic button dropdown"
8
- name="type"
9
- role="listbox"
10
- tabindex="0"
6
+ class="field"
11
7
  >
12
- <i
13
- aria-hidden="true"
14
- class="dropdown icon"
15
- />
16
8
  <div
17
- class="menu transition"
9
+ aria-expanded="false"
10
+ class="ui button fluid dropdown"
11
+ name="type"
12
+ role="listbox"
13
+ tabindex="0"
18
14
  >
15
+ <i
16
+ aria-hidden="true"
17
+ class="dropdown icon"
18
+ />
19
19
  <div
20
- aria-checked="false"
21
- aria-selected="true"
22
- class="selected item"
23
- role="option"
24
- style="pointer-events: all;"
20
+ class="menu transition"
25
21
  >
26
- <i
27
- aria-hidden="true"
28
- class="adjust icon"
29
- />
30
- <span
31
- class="text"
22
+ <div
23
+ aria-checked="false"
24
+ aria-selected="true"
25
+ class="selected item"
26
+ role="option"
27
+ style="pointer-events: all;"
32
28
  >
33
- boolean
34
- </span>
35
- </div>
36
- <div
37
- aria-checked="false"
38
- aria-selected="false"
39
- class="item"
40
- role="option"
41
- style="pointer-events: all;"
42
- >
43
- <i
44
- aria-hidden="true"
45
- class="font icon"
46
- />
47
- <span
48
- class="text"
29
+ <i
30
+ aria-hidden="true"
31
+ class="adjust icon"
32
+ />
33
+ <span
34
+ class="text"
35
+ >
36
+ boolean
37
+ </span>
38
+ </div>
39
+ <div
40
+ aria-checked="false"
41
+ aria-selected="false"
42
+ class="item"
43
+ role="option"
44
+ style="pointer-events: all;"
49
45
  >
50
- string
51
- </span>
52
- </div>
53
- <div
54
- aria-checked="false"
55
- aria-selected="false"
56
- class="item"
57
- role="option"
58
- style="pointer-events: all;"
59
- >
60
- <i
61
- aria-hidden="true"
62
- class="hashtag icon"
63
- />
64
- <span
65
- class="text"
46
+ <i
47
+ aria-hidden="true"
48
+ class="font icon"
49
+ />
50
+ <span
51
+ class="text"
52
+ >
53
+ string
54
+ </span>
55
+ </div>
56
+ <div
57
+ aria-checked="false"
58
+ aria-selected="false"
59
+ class="item"
60
+ role="option"
61
+ style="pointer-events: all;"
66
62
  >
67
- number
68
- </span>
69
- </div>
70
- <div
71
- aria-checked="false"
72
- aria-selected="false"
73
- class="item"
74
- role="option"
75
- style="pointer-events: all;"
76
- >
77
- <i
78
- aria-hidden="true"
79
- class="calendar alternate outline icon"
80
- />
81
- <span
82
- class="text"
63
+ <i
64
+ aria-hidden="true"
65
+ class="hashtag icon"
66
+ />
67
+ <span
68
+ class="text"
69
+ >
70
+ number
71
+ </span>
72
+ </div>
73
+ <div
74
+ aria-checked="false"
75
+ aria-selected="false"
76
+ class="item"
77
+ role="option"
78
+ style="pointer-events: all;"
83
79
  >
84
- date
85
- </span>
86
- </div>
87
- <div
88
- aria-checked="false"
89
- aria-selected="false"
90
- class="item"
91
- role="option"
92
- style="pointer-events: all;"
93
- >
94
- <i
95
- aria-hidden="true"
96
- class="clock outline icon"
97
- />
98
- <span
99
- class="text"
80
+ <i
81
+ aria-hidden="true"
82
+ class="calendar alternate outline icon"
83
+ />
84
+ <span
85
+ class="text"
86
+ >
87
+ date
88
+ </span>
89
+ </div>
90
+ <div
91
+ aria-checked="false"
92
+ aria-selected="false"
93
+ class="item"
94
+ role="option"
95
+ style="pointer-events: all;"
100
96
  >
101
- timestamp
102
- </span>
103
- </div>
104
- <div
105
- aria-checked="false"
106
- aria-selected="false"
107
- class="item"
108
- role="option"
109
- style="pointer-events: all;"
110
- >
111
- <i
112
- aria-hidden="true"
113
- class="question circle outline icon"
114
- />
115
- <span
116
- class="text"
97
+ <i
98
+ aria-hidden="true"
99
+ class="clock outline icon"
100
+ />
101
+ <span
102
+ class="text"
103
+ >
104
+ timestamp
105
+ </span>
106
+ </div>
107
+ <div
108
+ aria-checked="false"
109
+ aria-selected="false"
110
+ class="item"
111
+ role="option"
112
+ style="pointer-events: all;"
117
113
  >
118
- any
119
- </span>
114
+ <i
115
+ aria-hidden="true"
116
+ class="question circle outline icon"
117
+ />
118
+ <span
119
+ class="text"
120
+ >
121
+ any
122
+ </span>
123
+ </div>
120
124
  </div>
121
125
  </div>
122
126
  </div>
@@ -126,103 +130,107 @@ exports[`<TypeSelector /> matches the latest snapshot 1`] = `
126
130
  exports[`<TypeSelector /> matches the latest snapshot withoutTypeAny 1`] = `
127
131
  <div>
128
132
  <div
129
- aria-expanded="false"
130
- class="ui basic button dropdown"
131
- name="type"
132
- role="listbox"
133
- tabindex="0"
133
+ class="field"
134
134
  >
135
- <i
136
- aria-hidden="true"
137
- class="dropdown icon"
138
- />
139
135
  <div
140
- class="menu transition"
136
+ aria-expanded="false"
137
+ class="ui button fluid dropdown"
138
+ name="type"
139
+ role="listbox"
140
+ tabindex="0"
141
141
  >
142
+ <i
143
+ aria-hidden="true"
144
+ class="dropdown icon"
145
+ />
142
146
  <div
143
- aria-checked="false"
144
- aria-selected="true"
145
- class="selected item"
146
- role="option"
147
- style="pointer-events: all;"
147
+ class="menu transition"
148
148
  >
149
- <i
150
- aria-hidden="true"
151
- class="adjust icon"
152
- />
153
- <span
154
- class="text"
149
+ <div
150
+ aria-checked="false"
151
+ aria-selected="true"
152
+ class="selected item"
153
+ role="option"
154
+ style="pointer-events: all;"
155
155
  >
156
- boolean
157
- </span>
158
- </div>
159
- <div
160
- aria-checked="false"
161
- aria-selected="false"
162
- class="item"
163
- role="option"
164
- style="pointer-events: all;"
165
- >
166
- <i
167
- aria-hidden="true"
168
- class="font icon"
169
- />
170
- <span
171
- class="text"
156
+ <i
157
+ aria-hidden="true"
158
+ class="adjust icon"
159
+ />
160
+ <span
161
+ class="text"
162
+ >
163
+ boolean
164
+ </span>
165
+ </div>
166
+ <div
167
+ aria-checked="false"
168
+ aria-selected="false"
169
+ class="item"
170
+ role="option"
171
+ style="pointer-events: all;"
172
172
  >
173
- string
174
- </span>
175
- </div>
176
- <div
177
- aria-checked="false"
178
- aria-selected="false"
179
- class="item"
180
- role="option"
181
- style="pointer-events: all;"
182
- >
183
- <i
184
- aria-hidden="true"
185
- class="hashtag icon"
186
- />
187
- <span
188
- class="text"
173
+ <i
174
+ aria-hidden="true"
175
+ class="font icon"
176
+ />
177
+ <span
178
+ class="text"
179
+ >
180
+ string
181
+ </span>
182
+ </div>
183
+ <div
184
+ aria-checked="false"
185
+ aria-selected="false"
186
+ class="item"
187
+ role="option"
188
+ style="pointer-events: all;"
189
189
  >
190
- number
191
- </span>
192
- </div>
193
- <div
194
- aria-checked="false"
195
- aria-selected="false"
196
- class="item"
197
- role="option"
198
- style="pointer-events: all;"
199
- >
200
- <i
201
- aria-hidden="true"
202
- class="calendar alternate outline icon"
203
- />
204
- <span
205
- class="text"
190
+ <i
191
+ aria-hidden="true"
192
+ class="hashtag icon"
193
+ />
194
+ <span
195
+ class="text"
196
+ >
197
+ number
198
+ </span>
199
+ </div>
200
+ <div
201
+ aria-checked="false"
202
+ aria-selected="false"
203
+ class="item"
204
+ role="option"
205
+ style="pointer-events: all;"
206
206
  >
207
- date
208
- </span>
209
- </div>
210
- <div
211
- aria-checked="false"
212
- aria-selected="false"
213
- class="item"
214
- role="option"
215
- style="pointer-events: all;"
216
- >
217
- <i
218
- aria-hidden="true"
219
- class="clock outline icon"
220
- />
221
- <span
222
- class="text"
207
+ <i
208
+ aria-hidden="true"
209
+ class="calendar alternate outline icon"
210
+ />
211
+ <span
212
+ class="text"
213
+ >
214
+ date
215
+ </span>
216
+ </div>
217
+ <div
218
+ aria-checked="false"
219
+ aria-selected="false"
220
+ class="item"
221
+ role="option"
222
+ style="pointer-events: all;"
223
223
  >
224
- timestamp
225
- </span>
224
+ <i
225
+ aria-hidden="true"
226
+ class="clock outline icon"
227
+ />
228
+ <span
229
+ class="text"
230
+ >
231
+ timestamp
232
+ </span>
233
+ </div>
226
234
  </div>
227
235
  </div>
228
236
  </div>
@@ -29,12 +29,15 @@ export default function Clauses({ labelId }) {
29
29
  />
30
30
  ))}
31
31
  <Divider horizontal>
32
- <Button
33
- size="mini"
34
- onClick={() => append({ expressions: [newExpression()] })}
35
- >
36
- {formatMessage({ id: "expression.clause.action.addGroup" })}
37
- </Button>
32
+ <Button.Group basic size="medium">
33
+ <Button
34
+ icon="plus"
35
+ content={formatMessage({
36
+ id: "expression.clause.action.addGroup",
37
+ })}
38
+ onClick={() => append({ expressions: [newExpression()] })}
39
+ />
40
+ </Button.Group>
38
41
  </Divider>
39
42
  </Form.Field>
40
43
  );
@@ -93,17 +96,22 @@ const ClauseExpression = ({ groupIndex, removeGroup, lastGroup }) => {
93
96
  </div>
94
97
  ))}
95
98
  <Divider horizontal>
96
- <Button size="mini" onClick={() => append(newExpression())}>
97
- {formatMessage({
98
- id: "expression.clause.action.addExpression",
99
- })}
100
- </Button>
99
+ <Button.Group basic size="medium">
100
+ <Button
101
+ icon="plus"
102
+ content={formatMessage({
103
+ id: "expression.clause.action.addExpression",
104
+ })}
105
+ onClick={() => append(newExpression())}
106
+ />
107
+ </Button.Group>
101
108
  </Divider>
102
109
  </Segment>
103
110
  <Divider horizontal>{lastGroup ? null : "OR"}</Divider>
104
111
  </div>
105
112
  );
106
113
  };
114
+
107
115
  ClauseExpression.propTypes = {
108
116
  groupIndex: PropTypes.number,
109
117
  removeGroup: PropTypes.func,
@@ -2,7 +2,7 @@ import _ from "lodash/fp";
2
2
  import { useIntl } from "react-intl";
3
3
  import { useState, use } from "react";
4
4
  import PropTypes from "prop-types";
5
- import { Dropdown, Button, Grid } from "semantic-ui-react";
5
+ import { Dropdown, Button, Form, Grid, 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";
@@ -27,7 +27,7 @@ export default function Condition({ onDelete }) {
27
27
  const { formatMessage } = useIntl();
28
28
  const [isShownDelete, setIsShownDelete] = useState();
29
29
  const context = use(QxContext);
30
- const { control, watch } = useFormContext();
30
+ const { control, watch, setValue } = useFormContext();
31
31
  const { field, functions } = context;
32
32
 
33
33
  const expression = watch(field);
@@ -68,7 +68,6 @@ export default function Condition({ onDelete }) {
68
68
  </div>
69
69
  );
70
70
  };
71
-
72
71
  return (
73
72
  <div
74
73
  onMouseEnter={() => setIsShownDelete(true)}
@@ -103,39 +102,41 @@ export default function Condition({ onDelete }) {
103
102
  <div className="flex-justify-center">
104
103
  <Controller
105
104
  control={control}
106
- name={`${field}.value`}
105
+ name={`${field}.value.name`}
107
106
  rules={{
108
- required: formatMessage({ id: "functions.form.required" }),
107
+ validate: (v) => {
108
+ const name = v ?? "";
109
+ const isValid = Boolean(name) || expression?.value?.isCondition === false;
110
+ return isValid || formatMessage({ id: "functions.form.required" });
111
+ },
109
112
  }}
110
113
  render={({
111
- field: { onBlur, onChange },
114
+ field: { onBlur, onChange, value },
112
115
  fieldState: { error },
113
116
  }) => (
114
- <Dropdown
115
- options={condFuncOptions}
116
- onBlur={onBlur}
117
- error={!!error}
118
- value={expression?.value?.name || ""}
119
- onChange={(_e, { value }) => {
120
- if (value == "customExpression") {
121
- onChange({
122
- ...(expression?.value || {}),
123
- isCondition: false,
124
- });
125
- } else {
126
- onChange({
127
- ...(expression?.value || {}),
128
- name: value,
129
- type: "boolean",
130
- isCondition: true,
131
- });
132
- }
133
- }}
134
- trigger={operatorTrigger(expression?.value?.name)}
135
- pointing="top left"
136
- icon={null}
137
- />
138
- )}
117
+ <Form.Field error={!!error?.message}>
118
+ <Dropdown
119
+ options={condFuncOptions}
120
+ onBlur={onBlur}
121
+ value={value || ""}
122
+ fluid
123
+ onChange={(_e, { value }) => {
124
+ if (value == "customExpression") {
125
+ setValue(`${field}.value`, { ...(expression?.value || {}), isCondition: false });
126
+ } else {
127
+ onChange(value);
128
+ setValue(`${field}.value.type`, "boolean");
129
+ setValue(`${field}.value.isCondition`, true);
130
+ }
131
+ }}
132
+ trigger={operatorTrigger(value)}
133
+ pointing="top left"
134
+ icon={null}
135
+ />
136
+ {error?.message && <Label prompt pointing>{error?.message}</Label>}
137
+ </Form.Field>
138
+ )
139
+ }
139
140
  />
140
141
  </div>
141
142
  </Grid.Column>
@@ -1,10 +1,11 @@
1
1
  import _ from "lodash/fp";
2
2
  import { use, useEffect } from "react";
3
3
  import { Controller, useFormContext } from "react-hook-form";
4
- import { Dropdown, Label } from "semantic-ui-react";
4
+ import { Dropdown, Form, Label } from "semantic-ui-react";
5
5
 
6
6
  import QxContext from "@truedat/qx/components/QxContext";
7
7
  import { getColorById } from "../../dataViews/queryableFunctions";
8
+ import { useIntl } from "react-intl";
8
9
 
9
10
  const fieldToKey = ({ parent_id, id }) => `${parent_id}:${id}`;
10
11
  const keyToField = (key) => {
@@ -25,6 +26,7 @@ export const fieldsToOptions = _.map((field) => ({
25
26
  }));
26
27
 
27
28
  export default function FieldSelector() {
29
+ const { formatMessage } = useIntl();
28
30
  const { field, type, fields } = use(QxContext);
29
31
  const { control, watch, setValue } = useFormContext();
30
32
  const fieldOptions = _.flow(
@@ -44,22 +46,34 @@ export default function FieldSelector() {
44
46
  <Controller
45
47
  control={control}
46
48
  name={`${field}.value`}
47
- rules={{ required: true }}
49
+ rules={{
50
+ required: {
51
+ value: true,
52
+ message: formatMessage({ id: "quality_control.form.required" }),
53
+ },
54
+ }}
48
55
  render={({ field: { onBlur, onChange }, fieldState: { error } }) => (
49
- <Dropdown
50
- className="select-field-dropdown"
51
- selection
52
- fluid
53
- pointing
54
- scrolling
55
- onBlur={onBlur}
56
- error={!!error}
57
- value={selectedKey}
58
- options={fieldOptions}
59
- onChange={(_e, { value }) =>
60
- onChange(_.find(keyToField(value))(fields))
61
- }
62
- />
56
+ <Form.Field width={16}>
57
+ <Dropdown
58
+ className="select-field-dropdown"
59
+ error={!!error?.message}
60
+ selection
61
+ fluid
62
+ pointing
63
+ scrolling
64
+ onBlur={onBlur}
65
+ value={selectedKey}
66
+ options={fieldOptions}
67
+ onChange={(_e, { value }) =>
68
+ onChange(_.find(keyToField(value))(fields))
69
+ }
70
+ />
71
+ {error?.message && (
72
+ <Label prompt pointing>
73
+ {error?.message}
74
+ </Label>
75
+ )}
76
+ </Form.Field>
63
77
  )}
64
78
  />
65
79
  );