From 773111ef507890516bd2f2bcaa2ceee44b343877 Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Fri, 22 Mar 2024 08:02:15 +0100 Subject: [PATCH] wip --- package-lock.json | 61 ++++----- .../entries/JSFunctionEntry.js | 40 +----- .../form-js-viewer/assets/form-js-base.css | 4 + packages/form-js-viewer/package.json | 3 +- .../components/form-fields/JSFunctionField.js | 117 ++++++++++++++---- 5 files changed, 120 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3a7b406f..c25ac82b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5058,19 +5058,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@lerna/create/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@lerna/create/node_modules/validate-npm-package-name": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", @@ -15690,19 +15677,6 @@ "node": ">= 10.0.0" } }, - "node_modules/lerna/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/lerna/node_modules/validate-npm-package-name": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", @@ -21218,6 +21192,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "dev": true, @@ -22009,7 +21995,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" } }, "packages/form-js-viewer/node_modules/big.js": { @@ -23585,7 +23572,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" }, "dependencies": { "big.js": { @@ -25717,12 +25705,6 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true }, - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true - }, "validate-npm-package-name": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", @@ -33101,12 +33083,6 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true }, - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true - }, "validate-npm-package-name": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", @@ -36875,6 +36851,11 @@ "version": "1.0.1", "dev": true }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, "v8-compile-cache": { "version": "2.3.0", "dev": true diff --git a/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js index 0169c2af8..afa479f2a 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js @@ -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'; @@ -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' } ]; @@ -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 }); @@ -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 - }); -} diff --git a/packages/form-js-viewer/assets/form-js-base.css b/packages/form-js-viewer/assets/form-js-base.css index 677f8ca6e..975956ca8 100644 --- a/packages/form-js-viewer/assets/form-js-base.css +++ b/packages/form-js-viewer/assets/form-js-base.css @@ -1203,6 +1203,10 @@ margin-right: 4px; } +.fjs-container .fjs-sandbox-iframe-container { + display: none; +} + /** * Flatpickr style adjustments */ diff --git a/packages/form-js-viewer/package.json b/packages/form-js-viewer/package.json index ac78b3bed..b8fd2f95d 100644 --- a/packages/form-js-viewer/package.json +++ b/packages/form-js-viewer/package.json @@ -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" diff --git a/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js b/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js index 000330f8b..3ae1e0d44 100644 --- a/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js +++ b/packages/form-js-viewer/src/render/components/form-fields/JSFunctionField.js @@ -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 ( +
+ ); } JSFunctionField.config = { @@ -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); \ No newline at end of file