Skip to content

Commit 2bdbc10

Browse files
committed
feat: implement script component
Related to #1102
1 parent 601ec7c commit 2bdbc10

File tree

10 files changed

+269
-2
lines changed

10 files changed

+269
-2
lines changed

packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function Condition(props) {
5050
let description = 'Condition under which the field is hidden';
5151

5252
// special case for expression fields which do not render
53-
if (field.type === 'expression') {
53+
if ([ 'expression', 'script' ].includes(field.type)) {
5454
label = 'Deactivate if';
5555
description = 'Condition under which the field is deactivated';
5656
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited, ToggleSwitchEntry, isToggleSwitchEntryEdited } from '@bpmn-io/properties-panel';
2+
import { get } from 'min-dash';
3+
4+
import { useService, useVariables } from '../hooks';
5+
6+
export function JSFunctionEntry(props) {
7+
const {
8+
editField,
9+
field
10+
} = props;
11+
12+
const entries = [
13+
{
14+
id: 'variable-mappings',
15+
component: FunctionParameters,
16+
editField: editField,
17+
field: field,
18+
isEdited: isFeelEntryEdited,
19+
isDefaultVisible: (field) => field.type === 'script'
20+
},
21+
{
22+
id: 'function',
23+
component: FunctionDefinition,
24+
editField: editField,
25+
field: field,
26+
isEdited: isTextAreaEntryEdited,
27+
isDefaultVisible: (field) => field.type === 'script'
28+
},
29+
{
30+
id: 'on-load-only',
31+
component: OnLoadOnlyEntry,
32+
editField: editField,
33+
field: field,
34+
isEdited: isToggleSwitchEntryEdited,
35+
isDefaultVisible: (field) => field.type === 'script'
36+
}
37+
];
38+
39+
return entries;
40+
}
41+
42+
function FunctionParameters(props) {
43+
const {
44+
editField,
45+
field,
46+
id
47+
} = props;
48+
49+
const debounce = useService('debounce');
50+
51+
const variables = useVariables().map(name => ({ name }));
52+
53+
const path = [ 'functionParameters' ];
54+
55+
const getValue = () => {
56+
return get(field, path, '');
57+
};
58+
59+
const setValue = (value) => {
60+
return editField(field, path, value || '');
61+
};
62+
63+
const tooltip = <div>
64+
Functions parameters should be described as an object, e.g.:
65+
<pre><code>{`{
66+
name: user.name,
67+
age: user.age
68+
}`}</code></pre>
69+
</div>;
70+
71+
return FeelEntry({
72+
debounce,
73+
feel: 'required',
74+
element: field,
75+
getValue,
76+
id,
77+
label: 'Function parameters',
78+
tooltip,
79+
description: 'Define the parameters to pass to the javascript context.',
80+
setValue,
81+
variables
82+
});
83+
}
84+
85+
function FunctionDefinition(props) {
86+
const {
87+
editField,
88+
field,
89+
id
90+
} = props;
91+
92+
const debounce = useService('debounce');
93+
94+
const path = [ 'jsFunction' ];
95+
96+
const getValue = () => {
97+
return get(field, path, '');
98+
};
99+
100+
const setValue = (value) => {
101+
return editField(field, path, value || '');
102+
};
103+
104+
return TextAreaEntry({
105+
debounce,
106+
element: field,
107+
getValue,
108+
description: 'Access function parameters via `data`, set results with `setValue`, and register cleanup functions with `onCleanup`.',
109+
id,
110+
label: 'Javascript code',
111+
setValue
112+
});
113+
}
114+
115+
function OnLoadOnlyEntry(props) {
116+
const {
117+
editField,
118+
field,
119+
id
120+
} = props;
121+
122+
const path = [ 'onLoadOnly' ];
123+
124+
const getValue = () => {
125+
return !!get(field, path, false);
126+
};
127+
128+
const setValue = (value) => {
129+
editField(field, path, value);
130+
};
131+
132+
return ToggleSwitchEntry({
133+
element: field,
134+
id,
135+
label: 'Execute on load only',
136+
getValue,
137+
setValue
138+
});
139+
}

packages/form-js-editor/src/features/properties-panel/entries/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { IFrameUrlEntry } from './IFrameUrlEntry';
1414
export { ImageSourceEntry } from './ImageSourceEntry';
1515
export { TextEntry } from './TextEntry';
1616
export { HtmlEntry } from './HtmlEntry';
17+
export { JSFunctionEntry } from './JSFunctionEntry';
1718
export { HeightEntry } from './HeightEntry';
1819
export { NumberEntries } from './NumberEntries';
1920
export { ExpressionFieldEntries } from './ExpressionFieldEntries';

packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
HeightEntry,
2020
NumberEntries,
2121
ExpressionFieldEntries,
22+
JSFunctionEntry,
2223
DateTimeEntry,
2324
TableDataSourceEntry,
2425
PaginationEntry,
@@ -45,6 +46,7 @@ export function GeneralGroup(field, editField, getService) {
4546
...HeightEntry({ field, editField }),
4647
...NumberEntries({ field, editField }),
4748
...ExpressionFieldEntries({ field, editField }),
49+
...JSFunctionEntry({ field, editField }),
4850
...ImageSourceEntry({ field, editField }),
4951
...AltTextEntry({ field, editField }),
5052
...SelectEntries({ field, editField }),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { JSFunctionField, iconsByType } from '@bpmn-io/form-js-viewer';
2+
import { editorFormFieldClasses } from '../Util';
3+
4+
const type = 'script';
5+
6+
export function EditorJSFunctionField(props) {
7+
const { field } = props;
8+
const { jsFunction = '' } = field;
9+
10+
const Icon = iconsByType(type);
11+
12+
let placeholderContent = 'JS function is empty';
13+
14+
if (jsFunction.trim()) {
15+
placeholderContent = 'JS function';
16+
}
17+
18+
return (
19+
<div class={ editorFormFieldClasses(type) }>
20+
<div class="fjs-form-field-placeholder">
21+
<Icon viewBox="0 0 54 54" />{placeholderContent}
22+
</div>
23+
</div>
24+
);
25+
}
26+
27+
EditorJSFunctionField.config = {
28+
...JSFunctionField.config,
29+
escapeGridRender: false
30+
};

packages/form-js-editor/src/render/components/editor-form-fields/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { EditorText } from './EditorText';
33
import { EditorHtml } from './EditorHtml';
44
import { EditorTable } from './EditorTable';
55
import { EditorExpressionField } from './EditorExpressionField';
6+
import { EditorJSFunctionField } from './EditorJSFunctionField';
67

78
export const editorFormFields = [
89
EditorIFrame,
910
EditorText,
1011
EditorHtml,
1112
EditorTable,
12-
EditorExpressionField
13+
EditorExpressionField,
14+
EditorJSFunctionField
1315
];
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useCallback, useEffect, useState } from 'preact/hooks';
2+
import { useExpressionEvaluation, useDeepCompareMemoize, usePrevious } from '../../hooks';
3+
import { isObject } from 'min-dash';
4+
5+
const type = 'script';
6+
7+
export function JSFunctionField(props) {
8+
const { field, onChange } = props;
9+
const { jsFunction, functionParameters, onLoadOnly } = field;
10+
11+
const [ loadLatch, setLoadLatch ] = useState(false);
12+
13+
const paramsEval = useExpressionEvaluation(functionParameters);
14+
const params = useDeepCompareMemoize(isObject(paramsEval) ? paramsEval : {});
15+
16+
const functionMemo = useCallback((params) => {
17+
18+
const cleanupCallbacks = [];
19+
20+
try {
21+
22+
setLoadLatch(true);
23+
const func = new Function('data', 'setValue', 'onCleanup', jsFunction);
24+
func(params, value => onChange({ field, value }), callback => cleanupCallbacks.push(callback));
25+
26+
} catch (error) {
27+
28+
// invalid expression definition, may happen during editing
29+
if (error instanceof SyntaxError) {
30+
return;
31+
}
32+
33+
console.error('Error evaluating expression:', error);
34+
onChange({ field, value: null });
35+
}
36+
37+
return () => {
38+
cleanupCallbacks.forEach(fn => fn());
39+
};
40+
41+
}, [ jsFunction, field, onChange ]);
42+
43+
const previousFunctionMemo = usePrevious(functionMemo);
44+
const previousParams = usePrevious(params);
45+
46+
useEffect(() => {
47+
48+
// reset load latch
49+
if (!onLoadOnly && loadLatch) {
50+
setLoadLatch(false);
51+
}
52+
53+
const functionChanged = previousFunctionMemo !== functionMemo;
54+
const paramsChanged = previousParams !== params;
55+
const alreadyLoaded = onLoadOnly && loadLatch;
56+
57+
const shouldExecute = functionChanged || paramsChanged && !alreadyLoaded;
58+
59+
if (shouldExecute) {
60+
return functionMemo(params);
61+
}
62+
63+
}, [ previousFunctionMemo, functionMemo, previousParams, params, loadLatch, onLoadOnly ]);
64+
65+
return null;
66+
}
67+
68+
JSFunctionField.config = {
69+
type,
70+
label: 'JS Function',
71+
group: 'basic-input',
72+
keyed: true,
73+
escapeGridRender: true,
74+
create: (options = {}) => ({
75+
jsFunction: 'setValue(data.value)',
76+
functionParameters: '={\n value: 42\n}',
77+
...options,
78+
})
79+
};
Lines changed: 9 additions & 0 deletions
Loading

packages/form-js-viewer/src/render/components/icons/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import SpacerIcon from './Spacer.svg';
1313
import DynamicListIcon from './DynamicList.svg';
1414
import TextIcon from './Text.svg';
1515
import HTMLIcon from './HTML.svg';
16+
import JsFunctionIcon from './JSFunction.svg';
1617
import ExpressionFieldIcon from './ExpressionField.svg';
1718
import TextfieldIcon from './Textfield.svg';
1819
import TextareaIcon from './Textarea.svg';
@@ -41,6 +42,7 @@ export const iconsByType = (type) => {
4142
taglist: TaglistIcon,
4243
text: TextIcon,
4344
html: HTMLIcon,
45+
script: JsFunctionIcon,
4446
textfield: TextfieldIcon,
4547
textarea: TextareaIcon,
4648
table: TableIcon,

packages/form-js-viewer/src/render/components/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Taglist } from './form-fields/Taglist';
1616
import { Text } from './form-fields/Text';
1717
import { Html } from './form-fields/Html';
1818
import { ExpressionField } from './form-fields/ExpressionField';
19+
import { JSFunctionField } from './form-fields/JSFunctionField';
1920
import { Textfield } from './form-fields/Textfield';
2021
import { Textarea } from './form-fields/Textarea';
2122
import { Table } from './form-fields/Table';
@@ -46,6 +47,7 @@ export {
4647
Image,
4748
Numberfield,
4849
ExpressionField,
50+
JSFunctionField,
4951
Radio,
5052
Select,
5153
Separator,
@@ -72,6 +74,7 @@ export const formFields = [
7274
Textfield,
7375
Textarea,
7476
ExpressionField,
77+
JSFunctionField,
7578
Text,
7679
Image,
7780
Table,

0 commit comments

Comments
 (0)