Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Skaiir committed Mar 22, 2024
1 parent 3a2259f commit 773111e
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 105 deletions.
61 changes: 21 additions & 40 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited, ToggleSwitchEntry, isToggleSwitchEntryEdited } from '@bpmn-io/properties-panel';
import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited } from '@bpmn-io/properties-panel';
import { get } from 'min-dash';

import { useService, useVariables } from '../hooks';
Expand All @@ -25,14 +25,6 @@ export function JSFunctionEntry(props) {
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'
}
];

Expand Down Expand Up @@ -76,7 +68,7 @@ function FunctionParameters(props) {
id,
label: 'Function parameters',
tooltip,
description: 'Define the parameters to pass to the javascript context.',
description: 'Define the parameters to pass to the javascript sandbox.',
setValue,
variables
});
Expand Down Expand Up @@ -105,35 +97,9 @@ function FunctionDefinition(props) {
debounce,
element: field,
getValue,
description: 'Access function parameters via `data`, set results with `setValue`, and register cleanup functions with `onCleanup`.',
description: 'Define the javascript function to execute. Register lifecycle hooks with onLoad({data}) and onData({data}). Use setValue(value) to return the result.',
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
});
}
4 changes: 4 additions & 0 deletions packages/form-js-viewer/assets/form-js-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,10 @@
margin-right: 4px;
}

.fjs-container .fjs-sandbox-iframe-container {
display: none;
}

/**
* Flatpickr style adjustments
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/form-js-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"lodash": "^4.5.0",
"marked": "^12.0.1",
"min-dash": "^4.2.1",
"preact": "^10.5.14"
"preact": "^10.5.14",
"uuid": "^9.0.1"
},
"sideEffects": [
"*.css"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,118 @@
import Sandbox from 'websandbox';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { useExpressionEvaluation, useDeepCompareMemoize } from '../../hooks';
import { useEffect, useState } from 'preact/hooks';
import { useExpressionEvaluation, useDeepCompareMemoize, usePrevious } from '../../hooks';
import { isObject } from 'min-dash';

import { v4 as uuidv4 } from 'uuid';

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

const [ sandbox, setSandbox ] = useState(null);
const [ iframeContainerId ] = useState(`fjs-sandbox-iframe-container_${uuidv4()}`);
const [ hasLoaded , setHasLoaded ] = useState(false);

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

const rebuildSandbox = useCallback(() => {
const localApi = {
// setup the sandbox
useEffect(() => {
const hostAPI = {
setValue: function(value) {
onChange({ field, value });
if (isValidData(value)) {
onChange({ field, value });
}
}
};

const newSandbox = Sandbox.create(localApi, {
frameContainer: '.iframe__container',
frameClassName: 'simple__iframe'
const _sandbox = Sandbox.create(hostAPI, {
frameContainer: `#${iframeContainerId}`,
frameClassName: 'fjs-sandbox-iframe'
});

newSandbox.promise.then((sandboxInstance) => {
setSandbox(sandboxInstance);
sandboxInstance.run(`
Websandbox.connection.setLocalApi({
onInit: () => Websandbox.connection.remote.onInit(),
onData: (data) => Websandbox.connection.remote.onData(data),
});
const wrappedUserCode = `
const dataCallbacks = [];
const loadCallbacks = [];
const api = {
onData: (callback) => datacallbacks.push(callback),
offData: (callback) => dataCallbacks.splice(dataCallbacks.indexOf(callback), 1),
onLoad: (callback) => loadCallbacks.push(callback),
offLoad: (callback) => loadCallbacks.splice(loadCallbacks.indexOf(callback), 1),
}
const onData = (callback) => {
dataCallbacks.push(callback);
}
// Custom user code
const offData = (callback) => {
dataCallbacks.splice(dataCallbacks.indexOf(callback), 1);
}
Websandbox.connection.setLocalApi({
sendData: ({data}) => dataCallbacks.forEach(callback => callback(data)),
load: ({data}) => loadCallbacks.forEach(callback => callback(data))
});
const setValue = (value) => {
Websandbox.connection.remote.setValue(value);
}
// Custom user code
try {
${jsFunction}
`);
}
catch (e) {
setValue(null);
}
`;

sandboxInstance.connection.remote.onInit();
_sandbox.promise.then((sandboxInstance) => {
setSandbox(sandboxInstance);
sandboxInstance
.run(wrappedUserCode)
.then(() => setHasLoaded(false))
.catch(() => { onChange({ field, value: null }); });
});
}, [ jsFunction, onChange, field ]);

useEffect(() => {
rebuildSandbox();
}, [ rebuildSandbox ]);
return () => {
_sandbox.destroy();
};

}, [ iframeContainerId, jsFunction, onChange, field, functionParameters ]);

const prevParams = usePrevious(params);
const prevSandbox = usePrevious(sandbox);

// make calls to the sandbox
useEffect(() => {
if (sandbox && sandbox.connection && sandbox.connection.remote.onData) {
const hasChanged = prevParams !== params || prevSandbox !== sandbox;
const hasConnection = sandbox && sandbox.connection && sandbox.connection.remote.onData;

if (hasChanged && hasConnection) {

if (!hasLoaded) {
setHasLoaded(true);
const loadResult = sandbox.connection.remote.onLoad();

if (isValidData(loadResult)) {
onChange({ field, value: loadResult });
}
}

sandbox.connection.remote.onData(params);
const dataResult = sandbox.connection.remote.onData();

if (isValidData(dataResult)) {
onChange({ field, value: dataResult });
}

}
}, [ params, sandbox ]);
}, [ params, sandbox, hasLoaded, prevParams, prevSandbox, onChange, field ]);

return null;
return (
<div id={ iframeContainerId } className="fjs-sandbox-iframe-container"></div>
);
}

JSFunctionField.config = {
Expand All @@ -61,8 +122,10 @@ JSFunctionField.config = {
keyed: true,
escapeGridRender: true,
create: (options = {}) => ({
jsFunction: 'setValue(data.value)',
jsFunction: 'onData((data) => setValue(data.value))',
functionParameters: '={\n value: 42\n}',
...options,
})
};

const isValidData = (data) => [ 'object', 'boolean', 'number', 'string' ].includes(typeof data);

0 comments on commit 773111e

Please sign in to comment.