@truedat/qx 5.12.2 → 5.12.7

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 (52) hide show
  1. package/package.json +8 -3
  2. package/src/api.js +3 -1
  3. package/src/components/QxContext.js +3 -0
  4. package/src/components/QxRoutes.js +6 -1
  5. package/src/components/common/DescriptionInput.js +47 -0
  6. package/src/components/common/TestFormWrapper.js +33 -0
  7. package/src/components/common/TypeSelector.js +33 -0
  8. package/src/components/common/index.js +4 -0
  9. package/src/components/functions/FunctionEditor.js +200 -0
  10. package/src/components/functions/FunctionParams.js +122 -0
  11. package/src/components/functions/Functions.js +152 -0
  12. package/src/components/functions/__tests__/FunctionEditor.spec.js +195 -0
  13. package/src/components/functions/__tests__/FunctionParams.spec.js +108 -0
  14. package/src/components/functions/__tests__/Functions.spec.js +95 -0
  15. package/src/components/functions/__tests__/__snapshots__/FunctionEditor.spec.js.snap +1563 -0
  16. package/src/components/functions/__tests__/__snapshots__/FunctionParams.spec.js.snap +228 -0
  17. package/src/components/functions/__tests__/__snapshots__/Functions.spec.js.snap +86 -0
  18. package/src/components/functions/__tests__/useWatchParams.spec.js +23 -0
  19. package/src/components/functions/expressions/ConstantSelector.js +26 -0
  20. package/src/components/functions/expressions/Expression.js +40 -0
  21. package/src/components/functions/expressions/FieldSelector.js +56 -0
  22. package/src/components/functions/expressions/FunctionArgs.js +49 -0
  23. package/src/components/functions/expressions/FunctionSelector.js +95 -0
  24. package/src/components/functions/expressions/ParamSelector.js +51 -0
  25. package/src/components/functions/expressions/ShapeSelector.js +74 -0
  26. package/src/components/functions/expressions/__tests__/ConstantSelector.spec.js +64 -0
  27. package/src/components/functions/expressions/__tests__/Expression.spec.js +131 -0
  28. package/src/components/functions/expressions/__tests__/FunctionArgs.spec.js +86 -0
  29. package/src/components/functions/expressions/__tests__/FunctionSelector.spec.js +69 -0
  30. package/src/components/functions/expressions/__tests__/ParamSelector.spec.js +115 -0
  31. package/src/components/functions/expressions/__tests__/ShapeSelector.spec.js +107 -0
  32. package/src/components/functions/expressions/__tests__/__snapshots__/ConstantSelector.spec.js.snap +149 -0
  33. package/src/components/functions/expressions/__tests__/__snapshots__/Expression.spec.js.snap +904 -0
  34. package/src/components/functions/expressions/__tests__/__snapshots__/FunctionArgs.spec.js.snap +392 -0
  35. package/src/components/functions/expressions/__tests__/__snapshots__/FunctionSelector.spec.js.snap +377 -0
  36. package/src/components/functions/expressions/__tests__/__snapshots__/ParamSelector.spec.js.snap +95 -0
  37. package/src/components/functions/expressions/__tests__/__snapshots__/ShapeSelector.spec.js.snap +290 -0
  38. package/src/components/functions/expressions/constantInputs/AnySelector.js +29 -0
  39. package/src/components/functions/expressions/constantInputs/BooleanSelector.js +37 -0
  40. package/src/components/functions/expressions/constantInputs/DefaultSelector.js +34 -0
  41. package/src/components/functions/expressions/constantInputs/__tests__/AnySelector.spec.js +63 -0
  42. package/src/components/functions/expressions/constantInputs/__tests__/BooleanSelector.spec.js +51 -0
  43. package/src/components/functions/expressions/constantInputs/__tests__/DefaultSelector.spec.js +56 -0
  44. package/src/components/functions/expressions/constantInputs/__tests__/__snapshots__/AnySelector.spec.js.snap +236 -0
  45. package/src/components/functions/expressions/constantInputs/__tests__/__snapshots__/BooleanSelector.spec.js.snap +101 -0
  46. package/src/components/functions/expressions/constantInputs/__tests__/__snapshots__/DefaultSelector.spec.js.snap +39 -0
  47. package/src/components/functions/expressions/constantInputs/index.js +5 -0
  48. package/src/components/functions/useWatchParams.js +13 -0
  49. package/src/hooks/__tests__/useFunctions.spec.js +101 -0
  50. package/src/hooks/useFunctions.js +33 -0
  51. package/src/styles/Expression.less +102 -0
  52. package/src/types.js +38 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/qx",
3
- "version": "5.12.2",
3
+ "version": "5.12.7",
4
4
  "description": "Truedat Web Quality Experience package",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -31,6 +31,10 @@
31
31
  "@babel/plugin-transform-modules-commonjs": "^7.19.6",
32
32
  "@babel/preset-env": "^7.20.2",
33
33
  "@babel/preset-react": "^7.18.6",
34
+ "@testing-library/jest-dom": "^5.16.5",
35
+ "@testing-library/react": "^12.0.0",
36
+ "@testing-library/react-hooks": "^8.0.1",
37
+ "@testing-library/user-event": "^13.2.1",
34
38
  "@truedat/test": "5.12.2",
35
39
  "babel-jest": "^28.1.0",
36
40
  "babel-plugin-dynamic-import-node": "^2.3.3",
@@ -80,8 +84,9 @@
80
84
  ]
81
85
  },
82
86
  "dependencies": {
83
- "@truedat/core": "5.12.2",
87
+ "@truedat/core": "5.12.7",
84
88
  "prop-types": "^15.8.1",
89
+ "react-hook-form": "^7.45.4",
85
90
  "react-intl": "^5.20.10",
86
91
  "react-router-dom": "^5.2.0",
87
92
  "semantic-ui-react": "^2.1.4",
@@ -92,5 +97,5 @@
92
97
  "react-dom": ">= 16.8.6 < 17",
93
98
  "semantic-ui-react": ">= 2.0.3 < 2.2"
94
99
  },
95
- "gitHead": "1121d357f2a54a2ad2e77adbd43096004316453d"
100
+ "gitHead": "940dfaecf8852eaa4a345b30ba690dc441adaccf"
96
101
  }
package/src/api.js CHANGED
@@ -1,3 +1,5 @@
1
1
  const API_DATA_SETS = "/api/data_sets";
2
+ const API_FUNCTIONS = "/api/quality_functions";
3
+ const API_FUNCTION = "/api/quality_functions/:id";
2
4
 
3
- export { API_DATA_SETS };
5
+ export { API_DATA_SETS, API_FUNCTIONS, API_FUNCTION };
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+
3
+ export default createContext();
@@ -2,8 +2,9 @@ import React from "react";
2
2
  import { Route } from "react-router-dom";
3
3
  import { Unauthorized } from "@truedat/core/components";
4
4
  import { useAuthorized } from "@truedat/core/hooks";
5
- import { DATA_SETS } from "@truedat/core/routes";
5
+ import { DATA_SETS, FUNCTIONS } from "@truedat/core/routes";
6
6
  import DataSets from "./DataSets";
7
+ import Functions from "./functions/Functions";
7
8
 
8
9
  export default function QxRoutes() {
9
10
  const authorized = useAuthorized();
@@ -14,6 +15,10 @@ export default function QxRoutes() {
14
15
  path={DATA_SETS}
15
16
  render={() => (authorized ? <DataSets /> : <Unauthorized />)}
16
17
  />
18
+ <Route
19
+ path={FUNCTIONS}
20
+ render={() => (authorized ? <Functions /> : <Unauthorized />)}
21
+ />
17
22
  </>
18
23
  );
19
24
  }
@@ -0,0 +1,47 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { useState } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import { Input, List } from "semantic-ui-react";
6
+
7
+ export default function DescriptionInput({
8
+ value,
9
+ onChange,
10
+ onBlur,
11
+ name = "description",
12
+ }) {
13
+ const { formatMessage } = useIntl();
14
+ const [editionMode, setEditMode] = useState();
15
+ return (
16
+ <List link horizontal>
17
+ {!editionMode ? (
18
+ <List.Item
19
+ as="a"
20
+ tabIndex={0}
21
+ onClick={() => setEditMode(true)}
22
+ onFocus={() => setEditMode(true)}
23
+ style={{ fontStyle: "italic", fontSize: 13, padding: "9.5px 14px" }}
24
+ >
25
+ {_.isEmpty(value)
26
+ ? formatMessage({ id: "functions.form.add_description" })
27
+ : value}
28
+ </List.Item>
29
+ ) : null}
30
+ {editionMode ? (
31
+ <List.Item>
32
+ <Input
33
+ onBlur={() => {
34
+ onBlur && onBlur();
35
+ setEditMode(false);
36
+ }}
37
+ placeholder={formatMessage({ id: "functions.form.description" })}
38
+ name={name}
39
+ onChange={onChange}
40
+ value={value}
41
+ autoFocus
42
+ />
43
+ </List.Item>
44
+ ) : null}
45
+ </List>
46
+ );
47
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { FormProvider, useForm } from "react-hook-form";
4
+ import QxContext from "@truedat/qx/components/QxContext";
5
+
6
+ export default function TestFormWrapper({
7
+ type = "string",
8
+ field = "test",
9
+ functions = [],
10
+ children,
11
+ watcher,
12
+ defaultValues,
13
+ }) {
14
+ const form = useForm({ mode: "onChange", defaultValues });
15
+ watcher && watcher(form.watch());
16
+
17
+ return (
18
+ <FormProvider {...form}>
19
+ <QxContext.Provider value={{ functions, type, field }}>
20
+ {children}
21
+ </QxContext.Provider>
22
+ </FormProvider>
23
+ );
24
+ }
25
+
26
+ TestFormWrapper.propTypes = {
27
+ type: PropTypes.string,
28
+ field: PropTypes.string,
29
+ functions: PropTypes.array,
30
+ children: PropTypes.node,
31
+ watcher: PropTypes.func,
32
+ defaultValues: PropTypes.object,
33
+ };
@@ -0,0 +1,33 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { Dropdown } from "semantic-ui-react";
4
+ import { typeToIcon, types } from "@truedat/qx/types";
5
+
6
+ export default function TypeSelector({
7
+ value,
8
+ onChange,
9
+ onBlur,
10
+ withoutTypeAny,
11
+ name = "type",
12
+ }) {
13
+ const options = _.flow(
14
+ _.reject((value) => withoutTypeAny && value == "any"),
15
+ _.map((value) => ({
16
+ key: value,
17
+ text: value,
18
+ value,
19
+ icon: typeToIcon(value),
20
+ }))
21
+ )(types);
22
+ return (
23
+ <Dropdown
24
+ button
25
+ basic
26
+ onBlur={onBlur}
27
+ name={name}
28
+ options={options}
29
+ value={value}
30
+ onChange={onChange}
31
+ />
32
+ );
33
+ }
@@ -0,0 +1,4 @@
1
+ import DescriptionInput from "./DescriptionInput";
2
+ import TypeSelector from "./TypeSelector";
3
+
4
+ export { DescriptionInput, TypeSelector };
@@ -0,0 +1,200 @@
1
+ import React, { Fragment, useEffect } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import {
5
+ Button,
6
+ Divider,
7
+ Grid,
8
+ GridColumn,
9
+ Header,
10
+ Form,
11
+ Label,
12
+ } from "semantic-ui-react";
13
+ import { FormProvider, useForm, Controller } from "react-hook-form";
14
+ import { ConfirmModal } from "@truedat/core/components";
15
+ import QxContext from "@truedat/qx/components/QxContext";
16
+ import { typeToColor } from "@truedat/qx/types";
17
+ import { DescriptionInput, TypeSelector } from "@truedat/qx/components/common";
18
+ import Expression from "./expressions/Expression";
19
+
20
+ import FunctionParams from "./FunctionParams";
21
+
22
+ export default function FunctionEditor({
23
+ selectedFunction,
24
+ functions,
25
+ onSubmit,
26
+ onCancel,
27
+ onDelete,
28
+ isSubmitting,
29
+ setDirty,
30
+ }) {
31
+ const { formatMessage } = useIntl();
32
+ const form = useForm({
33
+ mode: "onTouched",
34
+ defaultValues: selectedFunction,
35
+ });
36
+ const { control, handleSubmit, watch, formState, setValue } = form;
37
+ const { isDirty, isValid } = formState;
38
+
39
+ useEffect(() => {
40
+ setDirty(isDirty);
41
+ }, [setDirty, isDirty]);
42
+
43
+ if (!selectedFunction) return null;
44
+
45
+ const type = watch("type");
46
+ const params = watch("params");
47
+
48
+ return (
49
+ <Fragment>
50
+ <FormProvider {...form}>
51
+ <Form>
52
+ <Header as="h3" dividing>
53
+ {watch("name") || "Function"}/{params.length}
54
+ <Label color={typeToColor(type)}>{type}</Label>
55
+ </Header>
56
+ <Grid>
57
+ <GridColumn>
58
+ <Grid.Row>
59
+ <Controller
60
+ control={control}
61
+ name="name"
62
+ rules={{
63
+ required: formatMessage(
64
+ { id: "form.validation.required" },
65
+ { prop: formatMessage({ id: "functions.form.name" }) }
66
+ ),
67
+ }}
68
+ render={({
69
+ field: { onBlur, onChange, value },
70
+ fieldState: { error },
71
+ }) => (
72
+ <Form.Input
73
+ autoComplete="off"
74
+ action={
75
+ <Controller
76
+ name="type"
77
+ control={control}
78
+ render={({ field: { onBlur, onChange, value } }) => (
79
+ <TypeSelector
80
+ onBlur={onBlur}
81
+ value={value}
82
+ onChange={(_e, { value }) => {
83
+ onChange(value);
84
+ setValue("expression.value", null, {
85
+ shouldValidate: true,
86
+ });
87
+ }}
88
+ />
89
+ )}
90
+ />
91
+ }
92
+ placeholder={formatMessage({ id: "functions.form.name" })}
93
+ error={error?.message}
94
+ label={formatMessage({ id: "group.props.name" })}
95
+ onBlur={onBlur}
96
+ onChange={(_e, { value }) => onChange(value)}
97
+ value={value}
98
+ required
99
+ />
100
+ )}
101
+ />
102
+ </Grid.Row>
103
+ <Grid.Row>
104
+ <Controller
105
+ name="description"
106
+ control={control}
107
+ render={({ field: { onBlur, onChange, value } }) => (
108
+ <DescriptionInput
109
+ onBlur={onBlur}
110
+ value={value}
111
+ onChange={(_e, { value }) => onChange(value)}
112
+ />
113
+ )}
114
+ />
115
+ </Grid.Row>
116
+ </GridColumn>
117
+ </Grid>
118
+
119
+ <Header as="h5" dividing>
120
+ {formatMessage({ id: "functions.form.params" })}
121
+ </Header>
122
+ <FunctionParams />
123
+
124
+ <Header as="h5" dividing>
125
+ {formatMessage({ id: "functions.form.output" })}
126
+ </Header>
127
+ <QxContext.Provider value={{ functions, type, field: "expression" }}>
128
+ <Expression />
129
+ </QxContext.Provider>
130
+ <Divider hidden />
131
+ <div className="actions">
132
+ {onDelete ? (
133
+ <ConfirmModal
134
+ trigger={
135
+ <Button
136
+ color="red"
137
+ content={formatMessage({ id: "actions.delete" })}
138
+ disabled={isSubmitting}
139
+ />
140
+ }
141
+ header={formatMessage({
142
+ id: "functions.action.delete.header",
143
+ })}
144
+ content={formatMessage({
145
+ id: "functions.action.delete.content",
146
+ })}
147
+ onConfirm={onDelete}
148
+ onOpen={(e) => e.stopPropagation()}
149
+ onClose={(e) => e.stopPropagation()}
150
+ />
151
+ ) : null}
152
+ {isDirty ? (
153
+ <ConfirmModal
154
+ trigger={
155
+ <Button
156
+ content={formatMessage({ id: "actions.cancel" })}
157
+ disabled={isSubmitting}
158
+ />
159
+ }
160
+ header={formatMessage({
161
+ id: "actions.discard.confirmation.header",
162
+ })}
163
+ content={formatMessage({
164
+ id: "actions.discard.confirmation.content",
165
+ })}
166
+ onConfirm={onCancel}
167
+ onOpen={(e) => e.stopPropagation()}
168
+ onClose={(e) => e.stopPropagation()}
169
+ />
170
+ ) : (
171
+ <Button
172
+ content={formatMessage({ id: "actions.cancel" })}
173
+ disabled={isSubmitting}
174
+ onClick={onCancel}
175
+ />
176
+ )}
177
+
178
+ <Button
179
+ onClick={handleSubmit(onSubmit)}
180
+ primary
181
+ loading={isSubmitting}
182
+ disabled={!isValid || !isDirty}
183
+ content={formatMessage({ id: "actions.save" })}
184
+ />
185
+ </div>
186
+ </Form>
187
+ </FormProvider>
188
+ </Fragment>
189
+ );
190
+ }
191
+
192
+ FunctionEditor.propTypes = {
193
+ selectedFunction: PropTypes.object,
194
+ functions: PropTypes.array,
195
+ onSubmit: PropTypes.func,
196
+ onCancel: PropTypes.func,
197
+ onDelete: PropTypes.func,
198
+ isSubmitting: PropTypes.bool,
199
+ setDirty: PropTypes.func,
200
+ };
@@ -0,0 +1,122 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
5
+ import { Controller, useFormContext, useFieldArray } from "react-hook-form";
6
+ import { Button, Grid, Form, List } from "semantic-ui-react";
7
+ import { DescriptionInput, TypeSelector } from "@truedat/qx/components/common";
8
+
9
+ export default function FunctionParams() {
10
+ const { formatMessage } = useIntl();
11
+ const { fields, append, remove } = useFieldArray({
12
+ name: "params",
13
+ keyName: "key",
14
+ });
15
+
16
+ const maxParamId = _.flow(_.map("id"), _.max)(fields) || 0;
17
+ const newParam = () => ({
18
+ name: "",
19
+ type: "boolean",
20
+ description: "",
21
+ id: maxParamId + 1,
22
+ });
23
+
24
+ return (
25
+ <List>
26
+ {fields.map((param, index) => (
27
+ <List.Item key={param.key}>
28
+ <ParamItem index={index} onDelete={() => remove(index)} />
29
+ </List.Item>
30
+ ))}
31
+ <List.Item>
32
+ <Button onClick={() => append(newParam())}>
33
+ {formatMessage({ id: "functions.form.add_param" })}
34
+ </Button>
35
+ </List.Item>
36
+ </List>
37
+ );
38
+ }
39
+
40
+ const ParamItem = ({ index, onDelete }) => {
41
+ const { formatMessage } = useIntl();
42
+ const { control } = useFormContext();
43
+ const [isShownDelete, setIsShownDelete] = useState();
44
+ return (
45
+ <div
46
+ onMouseEnter={() => setIsShownDelete(true)}
47
+ onMouseLeave={() => setIsShownDelete(false)}
48
+ >
49
+ <Grid>
50
+ <Grid.Row>
51
+ <Grid.Column></Grid.Column>
52
+ <Grid.Column width={15}>
53
+ <Grid.Row>
54
+ <Form.Group inline className="no-margin">
55
+ <Controller
56
+ control={control}
57
+ name={`params[${index}].name`}
58
+ rules={{
59
+ required: formatMessage({ id: "functions.form.required" }),
60
+ }}
61
+ render={({
62
+ field: { onBlur, onChange, value },
63
+ fieldState: { error },
64
+ }) => (
65
+ <Form.Input
66
+ autoComplete="off"
67
+ action={
68
+ <Controller
69
+ name={`params[${index}].type`}
70
+ control={control}
71
+ render={({ field: { onBlur, onChange, value } }) => (
72
+ <TypeSelector
73
+ onBlur={onBlur}
74
+ value={value}
75
+ onChange={(_e, { value }) => onChange(value)}
76
+ />
77
+ )}
78
+ />
79
+ }
80
+ placeholder={formatMessage({ id: "functions.form.name" })}
81
+ error={error?.message}
82
+ onBlur={onBlur}
83
+ onChange={(_e, { value }) => onChange(value)}
84
+ value={value}
85
+ />
86
+ )}
87
+ />
88
+ {isShownDelete ? (
89
+ <Button
90
+ onClick={onDelete}
91
+ icon="trash alternate outline"
92
+ basic
93
+ color="red"
94
+ aria-label="delete"
95
+ />
96
+ ) : null}
97
+ </Form.Group>
98
+ </Grid.Row>
99
+ <Grid.Row>
100
+ <Controller
101
+ name={`params[${index}].description`}
102
+ control={control}
103
+ render={({ field: { onBlur, onChange, value } }) => (
104
+ <DescriptionInput
105
+ onBlur={onBlur}
106
+ value={value}
107
+ onChange={(_e, { value }) => onChange(value)}
108
+ />
109
+ )}
110
+ />
111
+ </Grid.Row>
112
+ </Grid.Column>
113
+ </Grid.Row>
114
+ </Grid>
115
+ </div>
116
+ );
117
+ };
118
+
119
+ ParamItem.propTypes = {
120
+ index: PropTypes.number,
121
+ onDelete: PropTypes.func,
122
+ };
@@ -0,0 +1,152 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { useIntl, FormattedMessage } from "react-intl";
4
+ import {
5
+ Button,
6
+ Grid,
7
+ GridColumn,
8
+ Header,
9
+ Icon,
10
+ Label,
11
+ List,
12
+ Segment,
13
+ } from "semantic-ui-react";
14
+
15
+ import { typeToColor } from "@truedat/qx/types";
16
+ import {
17
+ useFunctions,
18
+ useFunctionCreate,
19
+ useFunctionDelete,
20
+ useFunctionUpdate,
21
+ } from "@truedat/qx/hooks/useFunctions";
22
+ import FunctionEditor from "./FunctionEditor";
23
+
24
+ const NEW_FUNCTION = {
25
+ name: "",
26
+ description: "",
27
+ type: "boolean",
28
+ params: [],
29
+ expression: { shape: "function", value: null },
30
+ };
31
+
32
+ export default function Functions() {
33
+ const { formatMessage } = useIntl();
34
+ const { data, loading, mutate } = useFunctions();
35
+ const [selectedFunction, setSelectedFunction] = useState();
36
+ const [isDirty, setDirty] = useState(false);
37
+
38
+ const functions = data?.data;
39
+ const userFunctions = _.reject(({ expression }) => _.isNil(expression))(
40
+ functions
41
+ );
42
+
43
+ const changeSelectedFunction = (selection) => {
44
+ setSelectedFunction(null);
45
+ setSelectedFunction(selection);
46
+ };
47
+ const setStateNewFunction = () => changeSelectedFunction(NEW_FUNCTION);
48
+ const clearForm = () => {
49
+ setSelectedFunction(null);
50
+ setDirty(false);
51
+ };
52
+
53
+ const { trigger: createFunction, isMutating: isCreating } =
54
+ useFunctionCreate();
55
+
56
+ const { trigger: updateFunction, isMutating: isUpdating } =
57
+ useFunctionUpdate(selectedFunction);
58
+
59
+ const { trigger: deleteFunction, isMutating: isDeleting } =
60
+ useFunctionDelete(selectedFunction);
61
+
62
+ const isSubmitting = isCreating || isUpdating || isDeleting;
63
+ const onFunctionCreate = (func) => {
64
+ const mutateFunction = selectedFunction?.id
65
+ ? updateFunction
66
+ : createFunction;
67
+ mutateFunction({ function: func }).then(() => {
68
+ clearForm();
69
+ mutate();
70
+ });
71
+ };
72
+ const onFunctionDelete = (func) => {
73
+ deleteFunction({ function: func }).then(() => {
74
+ clearForm();
75
+ mutate();
76
+ });
77
+ };
78
+ return (
79
+ <Segment loading={loading}>
80
+ <Header as="h2">
81
+ <Icon circular name="archive" />
82
+ <Header.Content>
83
+ <FormattedMessage id="functions.header" />
84
+ <Header.Subheader>
85
+ <FormattedMessage id="functions.subheader" />
86
+ </Header.Subheader>
87
+ </Header.Content>
88
+ </Header>
89
+
90
+ <Grid>
91
+ <GridColumn width={4}>
92
+ <Button fluid onClick={setStateNewFunction} disabled={isDirty}>
93
+ {formatMessage({ id: "functions.action.new" })}
94
+ </Button>
95
+ <List divided selection={!isDirty}>
96
+ {!_.isEmpty(userFunctions) ? (
97
+ userFunctions.map((func, key) => (
98
+ <List.Item
99
+ key={key}
100
+ onClick={() => !isDirty && changeSelectedFunction(func)}
101
+ >
102
+ <List.Content>
103
+ <List.Header>
104
+ <Label
105
+ size="tiny"
106
+ circular
107
+ color={typeToColor(func.type)}
108
+ width="100px"
109
+ >
110
+ {func.type[0]}
111
+ </Label>{" "}
112
+ {func.name || "Function"}/{func.params.length}
113
+ </List.Header>
114
+ </List.Content>
115
+ </List.Item>
116
+ ))
117
+ ) : (
118
+ <List.Item>
119
+ <List.Content>
120
+ <List.Header>
121
+ {formatMessage({ id: "functions.empty_list" })}
122
+ </List.Header>
123
+ </List.Content>
124
+ </List.Item>
125
+ )}
126
+ </List>
127
+ </GridColumn>
128
+ <GridColumn width={11}>
129
+ {selectedFunction ? (
130
+ <FunctionEditor
131
+ key={selectedFunction?.id || "new"}
132
+ selectedFunction={selectedFunction}
133
+ functions={functions}
134
+ onCancel={clearForm}
135
+ onSubmit={onFunctionCreate}
136
+ onDelete={selectedFunction?.id ? onFunctionDelete : null}
137
+ isSubmitting={isSubmitting}
138
+ setDirty={setDirty}
139
+ />
140
+ ) : (
141
+ <Header as="h2" icon textAlign="center">
142
+ <Icon name="hand pointer outline" />
143
+ <Header.Subheader>
144
+ {formatMessage({ id: "functions.no_selection" })}
145
+ </Header.Subheader>
146
+ </Header>
147
+ )}
148
+ </GridColumn>
149
+ </Grid>
150
+ </Segment>
151
+ );
152
+ }