Skip to content

Commit

Permalink
feat: implement script component
Browse files Browse the repository at this point in the history
Related to #1102
  • Loading branch information
Skaiir committed Mar 20, 2024
1 parent 601ec7c commit 2bdbc10
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function Condition(props) {
let description = 'Condition under which the field is hidden';

// special case for expression fields which do not render
if (field.type === 'expression') {
if ([ 'expression', 'script' ].includes(field.type)) {
label = 'Deactivate if';
description = 'Condition under which the field is deactivated';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited, ToggleSwitchEntry, isToggleSwitchEntryEdited } from '@bpmn-io/properties-panel';
import { get } from 'min-dash';

import { useService, useVariables } from '../hooks';

export function JSFunctionEntry(props) {
const {
editField,
field
} = props;

const entries = [
{
id: 'variable-mappings',
component: FunctionParameters,
editField: editField,
field: field,
isEdited: isFeelEntryEdited,
isDefaultVisible: (field) => field.type === 'script'
},
{
id: 'function',
component: FunctionDefinition,
editField: editField,
field: field,
isEdited: isTextAreaEntryEdited,
isDefaultVisible: (field) => field.type === 'script'
},
{
id: 'on-load-only',
component: OnLoadOnlyEntry,
editField: editField,
field: field,
isEdited: isToggleSwitchEntryEdited,
isDefaultVisible: (field) => field.type === 'script'
}
];

return entries;
}

function FunctionParameters(props) {
const {
editField,
field,
id
} = props;

const debounce = useService('debounce');

const variables = useVariables().map(name => ({ name }));

const path = [ 'functionParameters' ];

const getValue = () => {
return get(field, path, '');
};

const setValue = (value) => {
return editField(field, path, value || '');
};

const tooltip = <div>
Functions parameters should be described as an object, e.g.:
<pre><code>{`{
name: user.name,
age: user.age
}`}</code></pre>
</div>;

return FeelEntry({
debounce,
feel: 'required',
element: field,
getValue,
id,
label: 'Function parameters',
tooltip,
description: 'Define the parameters to pass to the javascript context.',
setValue,
variables
});
}

function FunctionDefinition(props) {
const {
editField,
field,
id
} = props;

const debounce = useService('debounce');

const path = [ 'jsFunction' ];

const getValue = () => {
return get(field, path, '');
};

const setValue = (value) => {
return editField(field, path, value || '');
};

return TextAreaEntry({
debounce,
element: field,
getValue,
description: 'Access function parameters via `data`, set results with `setValue`, and register cleanup functions with `onCleanup`.',
id,
label: 'Javascript code',
setValue
});
}

function OnLoadOnlyEntry(props) {
const {
editField,
field,
id
} = props;

const path = [ 'onLoadOnly' ];

const getValue = () => {
return !!get(field, path, false);
};

const setValue = (value) => {
editField(field, path, value);
};

return ToggleSwitchEntry({
element: field,
id,
label: 'Execute on load only',
getValue,
setValue
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { IFrameUrlEntry } from './IFrameUrlEntry';
export { ImageSourceEntry } from './ImageSourceEntry';
export { TextEntry } from './TextEntry';
export { HtmlEntry } from './HtmlEntry';
export { JSFunctionEntry } from './JSFunctionEntry';
export { HeightEntry } from './HeightEntry';
export { NumberEntries } from './NumberEntries';
export { ExpressionFieldEntries } from './ExpressionFieldEntries';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
HeightEntry,
NumberEntries,
ExpressionFieldEntries,
JSFunctionEntry,
DateTimeEntry,
TableDataSourceEntry,
PaginationEntry,
Expand All @@ -45,6 +46,7 @@ export function GeneralGroup(field, editField, getService) {
...HeightEntry({ field, editField }),
...NumberEntries({ field, editField }),
...ExpressionFieldEntries({ field, editField }),
...JSFunctionEntry({ field, editField }),
...ImageSourceEntry({ field, editField }),
...AltTextEntry({ field, editField }),
...SelectEntries({ field, editField }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { JSFunctionField, iconsByType } from '@bpmn-io/form-js-viewer';
import { editorFormFieldClasses } from '../Util';

const type = 'script';

export function EditorJSFunctionField(props) {
const { field } = props;
const { jsFunction = '' } = field;

const Icon = iconsByType(type);

let placeholderContent = 'JS function is empty';

if (jsFunction.trim()) {
placeholderContent = 'JS function';
}

return (
<div class={ editorFormFieldClasses(type) }>
<div class="fjs-form-field-placeholder">
<Icon viewBox="0 0 54 54" />{placeholderContent}
</div>
</div>
);
}

EditorJSFunctionField.config = {
...JSFunctionField.config,
escapeGridRender: false
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { EditorText } from './EditorText';
import { EditorHtml } from './EditorHtml';
import { EditorTable } from './EditorTable';
import { EditorExpressionField } from './EditorExpressionField';
import { EditorJSFunctionField } from './EditorJSFunctionField';

export const editorFormFields = [
EditorIFrame,
EditorText,
EditorHtml,
EditorTable,
EditorExpressionField
EditorExpressionField,
EditorJSFunctionField
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useCallback, useEffect, useState } from 'preact/hooks';
import { useExpressionEvaluation, useDeepCompareMemoize, usePrevious } from '../../hooks';
import { isObject } from 'min-dash';

const type = 'script';

export function JSFunctionField(props) {
const { field, onChange } = props;
const { jsFunction, functionParameters, onLoadOnly } = field;

const [ loadLatch, setLoadLatch ] = useState(false);

const paramsEval = useExpressionEvaluation(functionParameters);
const params = useDeepCompareMemoize(isObject(paramsEval) ? paramsEval : {});

const functionMemo = useCallback((params) => {

const cleanupCallbacks = [];

try {

setLoadLatch(true);
const func = new Function('data', 'setValue', 'onCleanup', jsFunction);
func(params, value => onChange({ field, value }), callback => cleanupCallbacks.push(callback));

} catch (error) {

// invalid expression definition, may happen during editing
if (error instanceof SyntaxError) {
return;
}

console.error('Error evaluating expression:', error);
onChange({ field, value: null });
}

return () => {
cleanupCallbacks.forEach(fn => fn());
};

}, [ jsFunction, field, onChange ]);

const previousFunctionMemo = usePrevious(functionMemo);
const previousParams = usePrevious(params);

useEffect(() => {

// reset load latch
if (!onLoadOnly && loadLatch) {
setLoadLatch(false);
}

const functionChanged = previousFunctionMemo !== functionMemo;
const paramsChanged = previousParams !== params;
const alreadyLoaded = onLoadOnly && loadLatch;

const shouldExecute = functionChanged || paramsChanged && !alreadyLoaded;

if (shouldExecute) {
return functionMemo(params);
}

}, [ previousFunctionMemo, functionMemo, previousParams, params, loadLatch, onLoadOnly ]);

return null;
}

JSFunctionField.config = {
type,
label: 'JS Function',
group: 'basic-input',
keyed: true,
escapeGridRender: true,
create: (options = {}) => ({
jsFunction: 'setValue(data.value)',
functionParameters: '={\n value: 42\n}',
...options,
})
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/form-js-viewer/src/render/components/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SpacerIcon from './Spacer.svg';
import DynamicListIcon from './DynamicList.svg';
import TextIcon from './Text.svg';
import HTMLIcon from './HTML.svg';
import JsFunctionIcon from './JSFunction.svg';
import ExpressionFieldIcon from './ExpressionField.svg';
import TextfieldIcon from './Textfield.svg';
import TextareaIcon from './Textarea.svg';
Expand Down Expand Up @@ -41,6 +42,7 @@ export const iconsByType = (type) => {
taglist: TaglistIcon,
text: TextIcon,
html: HTMLIcon,
script: JsFunctionIcon,
textfield: TextfieldIcon,
textarea: TextareaIcon,
table: TableIcon,
Expand Down
3 changes: 3 additions & 0 deletions packages/form-js-viewer/src/render/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Taglist } from './form-fields/Taglist';
import { Text } from './form-fields/Text';
import { Html } from './form-fields/Html';
import { ExpressionField } from './form-fields/ExpressionField';
import { JSFunctionField } from './form-fields/JSFunctionField';
import { Textfield } from './form-fields/Textfield';
import { Textarea } from './form-fields/Textarea';
import { Table } from './form-fields/Table';
Expand Down Expand Up @@ -46,6 +47,7 @@ export {
Image,
Numberfield,
ExpressionField,
JSFunctionField,
Radio,
Select,
Separator,
Expand All @@ -72,6 +74,7 @@ export const formFields = [
Textfield,
Textarea,
ExpressionField,
JSFunctionField,
Text,
Image,
Table,
Expand Down

0 comments on commit 2bdbc10

Please sign in to comment.