Skip to content

Commit

Permalink
feat: implement script component (viewer)
Browse files Browse the repository at this point in the history
Related to #1102
  • Loading branch information
Skaiir committed Mar 25, 2024
1 parent 601ec7c commit 363ec4d
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 54 deletions.
73 changes: 33 additions & 40 deletions package-lock.json

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

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
4 changes: 3 additions & 1 deletion packages/form-js-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"@carbon/grid": "^11.11.0",
"@jetbrains/websandbox": "^1.0.10",
"big.js": "^6.2.1",
"classnames": "^2.3.1",
"didi": "^10.0.1",
Expand All @@ -56,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
22 changes: 12 additions & 10 deletions packages/form-js-viewer/src/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,42 +446,44 @@ export class Form {
const pathRegistry = this.get('pathRegistry');
const formData = this._getState().data;

function collectSubmitDataRecursively(submitData, formField, indexes) {
const { disabled, type } = formField;
function collectSubmitDataRecursively(submitData, field, indexes) {
const { disabled, type } = field;
const { config: fieldConfig } = formFields.get(type);

// (1) Process keyed fields
if (!disabled && fieldConfig.keyed) {
const valuePath = pathRegistry.getValuePath(formField, { indexes });
const isSubmittedKeyedField = fieldConfig.keyed && !disabled && !(fieldConfig.allowDoNotSubmit && field.doNotSubmit);

if (isSubmittedKeyedField) {
const valuePath = pathRegistry.getValuePath(field, { indexes });
const value = get(formData, valuePath);
set(submitData, valuePath, value);
}

// (2) Process parents
if (!Array.isArray(formField.components)) {
if (!Array.isArray(field.components)) {
return;
}

// (3a) Recurse repeatable parents both across the indexes of repetition and the children
if (fieldConfig.repeatable && formField.isRepeating) {
if (fieldConfig.repeatable && field.isRepeating) {

const valueData = get(formData, pathRegistry.getValuePath(formField, { indexes }));
const valueData = get(formData, pathRegistry.getValuePath(field, { indexes }));

if (!Array.isArray(valueData)) {
return;
}

valueData.forEach((_, index) => {
formField.components.forEach((component) => {
collectSubmitDataRecursively(submitData, component, { ...indexes, [formField.id]: index });
field.components.forEach((component) => {
collectSubmitDataRecursively(submitData, component, { ...indexes, [field.id]: index });
});
});

return;
}

// (3b) Recurse non-repeatable parents only across the children
formField.components.forEach((component) => collectSubmitDataRecursively(submitData, component, indexes));
field.components.forEach((component) => collectSubmitDataRecursively(submitData, component, indexes));
}

const workingSubmitData = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export function ExpressionField(props) {
ExpressionField.config = {
type,
label: 'Expression',
group: 'basic-input',
group: 'advanced',
keyed: true,
allowDoNotSubmit: true,
escapeGridRender: true,
create: (options = {}) => ({
computeOn: 'change',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Sandbox from 'websandbox';
import { useCallback, 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: functionDefinition,
functionParameters: paramsDefinition,
computeOn,
interval
} = field;

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

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

const clearValue = useCallback(() => onChange({ field, value: undefined }), [ field, onChange ]);

const safeSetValue = useCallback((value) => {

if (value !== undefined) {

// strip out functions and handle unserializeable objects
try {
value = JSON.parse(JSON.stringify(value));
onChange({ field, value });
} catch (e) {
clearValue();
}
}

}, [ field, onChange, clearValue ]);

useEffect(() => {
const hostAPI = {
setValue: safeSetValue,
error: (e) => {
clearValue();
}
};

// @ts-ignore
const _sandbox = Sandbox.create(hostAPI, {
frameContainer: `#${iframeContainerId}`,
frameClassName: 'fjs-sandbox-iframe'
});

const wrappedUserCode = `
const computeCallThisFunctionIfYouWantToCrashYourBrowser = (data) => {
try {
const setValue = Websandbox.connection.remote.setValue;
${functionDefinition}
}
catch (e) {
Websandbox.connection.remote.error(e);
}
}
Websandbox.connection.setLocalApi({ compute: computeCallThisFunctionIfYouWantToCrashYourBrowser });
`;

_sandbox.promise.then((sandboxInstance) => {
sandboxInstance
.run(wrappedUserCode)
.catch(() => { onChange({ field, value: null }); })
.then(() => { setSandbox(sandboxInstance); setHasRunLoad(false); });
});

return () => {
_sandbox.destroy();
};
}, [ iframeContainerId, functionDefinition, onChange, field, paramsDefinition, computeOn, interval, safeSetValue, clearValue ]);

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

useEffect(() => {

if (!sandbox || !sandbox.connection.remote.compute) {
return;
}

const runCompute = () => {
sandbox.connection.remote.compute(params)
.catch(clearValue)
.then(safeSetValue);
};

if (computeOn === 'load' && !hasRunLoad) {
runCompute();
setHasRunLoad(true);
}
else if (computeOn === 'change' && (params !== prevParams || sandbox !== prevSandbox)) {
runCompute();
}
else if (computeOn === 'interval') {
const intervalId = setInterval(runCompute, interval);
return () => clearInterval(intervalId);
}

}, [ params, prevParams, sandbox, prevSandbox, onChange, field, computeOn, hasRunLoad, interval, clearValue, safeSetValue ]);

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

JSFunctionField.config = {
type: 'script',
label: 'JS Function',
group: 'advanced',
keyed: true,
allowDoNotSubmit: true,
escapeGridRender: true,
create: (options = {}) => ({
jsFunction: 'setValue(data.value)',
functionParameters: '={\n value: 42\n}',
computeOn: 'load',
interval: 1000,
...options,
})
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 363ec4d

Please sign in to comment.