From 2c5f4de2e8aa602fa0225bd1b7101602d0c9fa5a Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Mon, 25 Mar 2024 18:30:08 +0100 Subject: [PATCH] feat: implement script component properties panel Related to #1102 --- .../entries/ConditionEntry.js | 2 +- .../entries/DoNotSubmitEntry.js | 31 ++++ .../entries/JSFunctionEntry.js | 164 ++++++++++++++++++ .../properties-panel/entries/KeyEntry.js | 28 +-- .../factories/simpleBoolEntryFactory.js | 2 + .../simpleRangeIntegerEntryFactory.js | 6 +- .../properties-panel/entries/index.js | 2 + .../properties-panel/groups/GeneralGroup.js | 6 +- 8 files changed, 223 insertions(+), 18 deletions(-) create mode 100644 packages/form-js-editor/src/features/properties-panel/entries/DoNotSubmitEntry.js create mode 100644 packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js index ee6331f2e..4c7f70649 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/ConditionEntry.js @@ -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'; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DoNotSubmitEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DoNotSubmitEntry.js new file mode 100644 index 000000000..ee4bcf33b --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/DoNotSubmitEntry.js @@ -0,0 +1,31 @@ +import { simpleBoolEntryFactory } from './factories'; + +export function DoNotSubmitEntry(props) { + const { + field, + getService + } = props; + + const formFields = getService('formFields'); + + const fieldDescriptors = { + script: "function's", + expression: "expression's", + }; + + const entries = [ + simpleBoolEntryFactory({ + id: 'doNotSubmit', + label: `Do not submit the ${fieldDescriptors[field.type] || "field's"} result with the form submission`, + tooltip: 'Prevents the data associated with this form element from being submitted by the form. Use for intermediate calculations.', + path: [ 'doNotSubmit' ], + props, + isDefaultVisible: (field) => { + const { config } = formFields.get(field.type); + return config.keyed && config.allowDoNotSubmit; + } + }) + ]; + + return entries; +} \ No newline at end of file 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 new file mode 100644 index 000000000..86f13fd87 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/JSFunctionEntry.js @@ -0,0 +1,164 @@ +import { FeelEntry, isFeelEntryEdited, TextAreaEntry, isTextAreaEntryEdited, SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; +import { get } from 'min-dash'; +import { simpleRangeIntegerEntryFactory } from './factories'; + +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: 'computeOn', + component: JSFunctionComputeOn, + isEdited: isSelectEntryEdited, + editField, + field, + isDefaultVisible: (field) => field.type === 'script' + }, + simpleRangeIntegerEntryFactory({ + id: 'interval', + label: 'Time interval (ms)', + path: [ 'interval' ], + min: 100, + max: 60000, + props, + isDefaultVisible: (field) => field.type === 'script' && field.computeOn === 'interval' + }) + ]; + + 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 =
+ Functions parameters should be described as an object, e.g.: +
{`{
+      name: user.name,
+      age: user.age
+    }`}
+
; + + return FeelEntry({ + debounce, + feel: 'required', + element: field, + getValue, + id, + label: 'Function parameters', + tooltip, + description: 'Define the parameters to pass to the javascript function.', + 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, error) => { + if (error) { + return; + } + + return editField(field, path, value || ''); + }; + + const validate = (value) => { + + try { + new Function(value); + } catch (e) { + return `Invalid syntax: ${e.message}`; + } + + return null; + }; + + return TextAreaEntry({ + debounce, + element: field, + getValue, + validate, + description: 'Define the javascript function to execute.\nAccess the `data` object and use `setValue` to update the form state.', + id, + label: 'Javascript code', + setValue + }); +} + +function JSFunctionComputeOn(props) { + const { editField, field, id } = props; + + const getValue = () => field.computeOn || ''; + + const setValue = (value) => { + editField(field, [ 'computeOn' ], value); + }; + + const getOptions = () => ([ + { value: 'load', label: 'Form load' }, + { value: 'change', label: 'Value change' }, + { value: 'interval', label: 'Time interval' } + ]); + + return SelectEntry({ + id, + label: 'Compute on', + description: 'Define when to execute the function', + getValue, + setValue, + getOptions + }); +} diff --git a/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js index 27c5adcd4..ba89ec320 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/KeyEntry.js @@ -7,7 +7,6 @@ import { useService } from '../hooks'; import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; import { useCallback } from 'preact/hooks'; - export function KeyEntry(props) { const { editField, @@ -15,20 +14,21 @@ export function KeyEntry(props) { getService } = props; - const entries = []; - - entries.push({ - id: 'key', - component: Key, - editField: editField, - field: field, - isEdited: isTextFieldEntryEdited, - isDefaultVisible: (field) => { - const formFields = getService('formFields'); - const { config } = formFields.get(field.type); - return config.keyed; + const formFields = getService('formFields'); + + const entries = [ + { + id: 'key', + component: Key, + editField: editField, + field: field, + isEdited: isTextFieldEntryEdited, + isDefaultVisible: (field) => { + const { config } = formFields.get(field.type); + return config.keyed; + } } - }); + ]; return entries; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js b/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js index 8daef0835..9d720f73e 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleBoolEntryFactory.js @@ -6,6 +6,7 @@ export function simpleBoolEntryFactory(options) { id, label, description, + tooltip, path, props, getValue, @@ -25,6 +26,7 @@ export function simpleBoolEntryFactory(options) { field, editField, description, + tooltip, component: SimpleBoolComponent, isEdited: isToggleSwitchEntryEdited, isDefaultVisible, diff --git a/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleRangeIntegerEntryFactory.js b/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleRangeIntegerEntryFactory.js index f45b58704..833ff7798 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleRangeIntegerEntryFactory.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/factories/simpleRangeIntegerEntryFactory.js @@ -13,7 +13,8 @@ export function simpleRangeIntegerEntryFactory(options) { path, props, min, - max + max, + isDefaultVisible } = options; const { @@ -30,7 +31,8 @@ export function simpleRangeIntegerEntryFactory(options) { min, max, component: SimpleRangeIntegerEntry, - isEdited: isTextFieldEntryEdited + isEdited: isTextFieldEntryEdited, + isDefaultVisible }; } diff --git a/packages/form-js-editor/src/features/properties-panel/entries/index.js b/packages/form-js-editor/src/features/properties-panel/entries/index.js index bb6c79066..673c7e5f4 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/index.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/index.js @@ -6,6 +6,7 @@ export { DefaultValueEntry } from './DefaultValueEntry'; export { DisabledEntry } from './DisabledEntry'; export { IdEntry } from './IdEntry'; export { KeyEntry } from './KeyEntry'; +export { DoNotSubmitEntry } from './DoNotSubmitEntry'; export { PathEntry } from './PathEntry'; export { GroupAppearanceEntry } from './GroupAppearanceEntry'; export { LabelEntry } from './LabelEntry'; @@ -14,6 +15,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'; diff --git a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js index 79e497715..eb278a450 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js @@ -9,6 +9,7 @@ import { IFrameHeightEntry, ImageSourceEntry, KeyEntry, + DoNotSubmitEntry, PathEntry, RepeatableEntry, LabelEntry, @@ -19,6 +20,7 @@ import { HeightEntry, NumberEntries, ExpressionFieldEntries, + JSFunctionEntry, DateTimeEntry, TableDataSourceEntry, PaginationEntry, @@ -45,6 +47,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 }), @@ -52,7 +55,8 @@ export function GeneralGroup(field, editField, getService) { ...ReadonlyEntry({ field, editField }), ...TableDataSourceEntry({ field, editField }), ...PaginationEntry({ field, editField }), - ...RowCountEntry({ field, editField }) + ...RowCountEntry({ field, editField }), + ...DoNotSubmitEntry({ field, editField, getService }), ]; if (entries.length === 0) {