diff --git a/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-chromium-linux.png b/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-chromium-linux.png
index 038be1118..0cd66f217 100644
Binary files a/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-chromium-linux.png and b/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-chromium-linux.png differ
diff --git a/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-firefox-linux.png b/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-firefox-linux.png
index b30648458..b39f215f2 100644
Binary files a/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-firefox-linux.png and b/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-firefox-linux.png differ
diff --git a/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-webkit-linux.png b/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-webkit-linux.png
index 7feed52a1..8d58aa599 100644
Binary files a/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-webkit-linux.png and b/e2e/visual/empty-ui.spec.js-snapshots/empty-playground-1-webkit-linux.png differ
diff --git a/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-chromium-linux.png b/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-chromium-linux.png
index b126a68eb..f237afde4 100644
Binary files a/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-chromium-linux.png and b/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-chromium-linux.png differ
diff --git a/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-firefox-linux.png b/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-firefox-linux.png
index 66b2c6cba..fef0dc247 100644
Binary files a/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-firefox-linux.png and b/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-firefox-linux.png differ
diff --git a/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-webkit-linux.png b/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-webkit-linux.png
index 8b00ad6ba..a56ffdc33 100644
Binary files a/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-webkit-linux.png and b/e2e/visual/groups-ui.spec.js-snapshots/groups-playground-1-webkit-linux.png differ
diff --git a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-chromium-linux.png b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-chromium-linux.png
index 620e77fa3..938f37532 100644
Binary files a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-chromium-linux.png and b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-chromium-linux.png differ
diff --git a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png
index 424e38368..d3711c048 100644
Binary files a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png and b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png differ
diff --git a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-webkit-linux.png b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-webkit-linux.png
index 808d46560..a19ebd821 100644
Binary files a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-webkit-linux.png and b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-webkit-linux.png differ
diff --git a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-chromium-linux.png b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-chromium-linux.png
index 4592badcc..9993cd868 100644
Binary files a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-chromium-linux.png and b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-chromium-linux.png differ
diff --git a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png
index 0a40f3fbc..5cec00e7e 100644
Binary files a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png and b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png differ
diff --git a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-webkit-linux.png b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-webkit-linux.png
index 802ffdf1e..5b8823629 100644
Binary files a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-webkit-linux.png and b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-webkit-linux.png differ
diff --git a/packages/form-js-editor/src/features/palette/components/Palette.js b/packages/form-js-editor/src/features/palette/components/Palette.js
index 3543d82fa..87ae0fe59 100644
--- a/packages/form-js-editor/src/features/palette/components/Palette.js
+++ b/packages/form-js-editor/src/features/palette/components/Palette.js
@@ -26,7 +26,7 @@ import { PaletteEntry } from './PaletteEntry';
export const PALETTE_GROUPS = [
{
- label: 'Basic input',
+ label: 'Input',
id: 'basic-input'
},
{
diff --git a/packages/form-js-editor/src/features/palette/index.js b/packages/form-js-editor/src/features/palette/index.js
index 729dc9335..d9a7dfe73 100644
--- a/packages/form-js-editor/src/features/palette/index.js
+++ b/packages/form-js-editor/src/features/palette/index.js
@@ -1,5 +1,6 @@
import { PaletteRenderer } from './PaletteRenderer';
export const PaletteModule = {
+ __init__: [ 'palette' ],
palette: [ 'type', PaletteRenderer ]
};
diff --git a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js
index 241630020..ca74e5e02 100644
--- a/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js
+++ b/packages/form-js-editor/src/features/properties-panel/PropertiesPanelHeaderProvider.js
@@ -11,6 +11,7 @@ import { useService } from './hooks';
const headerlessTypes = [
'spacer',
'separator',
+ 'expression',
'html'
];
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 ca1b318c4..ee6331f2e 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
@@ -46,14 +46,23 @@ function Condition(props) {
return editField(field, 'conditional', { hide: value });
};
+ let label = 'Hide if';
+ let description = 'Condition under which the field is hidden';
+
+ // special case for expression fields which do not render
+ if (field.type === 'expression') {
+ label = 'Deactivate if';
+ description = 'Condition under which the field is deactivated';
+ }
+
return FeelEntry({
debounce,
- description: 'Condition under which the field is hidden',
+ description,
element: field,
feel: 'required',
getValue,
id,
- label: 'Hide if',
+ label,
setValue,
variables
});
diff --git a/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js b/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js
new file mode 100644
index 000000000..6825ddce2
--- /dev/null
+++ b/packages/form-js-editor/src/features/properties-panel/entries/ExpressionFieldEntries.js
@@ -0,0 +1,76 @@
+import { FeelEntry, isFeelEntryEdited, SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel';
+import { useService, useVariables } from '../hooks';
+
+export function ExpressionFieldEntries(props) {
+ const { editField, field, id } = props;
+
+ const entries = [];
+
+ entries.push({
+ id: `${id}-expression`,
+ component: ExpressionFieldExpression,
+ isEdited: isFeelEntryEdited,
+ editField,
+ field,
+ isDefaultVisible: (field) => field.type === 'expression'
+ });
+
+ entries.push({
+ id: `${id}-computeOn`,
+ component: ExpressionFieldComputeOn,
+ isEdited: isSelectEntryEdited,
+ editField,
+ field,
+ isDefaultVisible: (field) => field.type === 'expression'
+ });
+
+ return entries;
+}
+
+function ExpressionFieldExpression(props) {
+ const { editField, field, id } = props;
+
+ const debounce = useService('debounce');
+ const variables = useVariables().map(name => ({ name }));
+
+ const getValue = () => field.expression || '';
+
+ const setValue = (value) => {
+ editField(field, [ 'expression' ], value);
+ };
+
+ return FeelEntry({
+ debounce,
+ description: 'Define an expression to calculate the value of this field',
+ element: field,
+ feel: 'required',
+ getValue,
+ id,
+ label: 'Target value',
+ setValue,
+ variables
+ });
+}
+
+function ExpressionFieldComputeOn(props) {
+ const { editField, field, id } = props;
+
+ const getValue = () => field.computeOn || '';
+
+ const setValue = (value) => {
+ editField(field, [ 'computeOn' ], value);
+ };
+
+ const getOptions = () => ([
+ { value: 'change', label: 'Value changes' },
+ { value: 'presubmit', label: 'Form submission' }
+ ]);
+
+ return SelectEntry({
+ id,
+ label: 'Compute on',
+ getValue,
+ setValue,
+ getOptions
+ });
+}
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 f3768c559..bb6c79066 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
@@ -16,6 +16,7 @@ export { TextEntry } from './TextEntry';
export { HtmlEntry } from './HtmlEntry';
export { HeightEntry } from './HeightEntry';
export { NumberEntries } from './NumberEntries';
+export { ExpressionFieldEntries } from './ExpressionFieldEntries';
export { NumberSerializationEntry } from './NumberSerializationEntry';
export { DateTimeEntry } from './DateTimeEntry';
export { DateTimeConstraintsEntry } from './DateTimeConstraintsEntry';
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 fdb3999bf..79e497715 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
@@ -18,6 +18,7 @@ import {
HtmlEntry,
HeightEntry,
NumberEntries,
+ ExpressionFieldEntries,
DateTimeEntry,
TableDataSourceEntry,
PaginationEntry,
@@ -43,6 +44,7 @@ export function GeneralGroup(field, editField, getService) {
...IFrameHeightEntry({ field, editField }),
...HeightEntry({ field, editField }),
...NumberEntries({ field, editField }),
+ ...ExpressionFieldEntries({ field, editField }),
...ImageSourceEntry({ field, editField }),
...AltTextEntry({ field, editField }),
...SelectEntries({ field, editField }),
diff --git a/packages/form-js-editor/src/render/components/editor-form-fields/EditorExpressionField.js b/packages/form-js-editor/src/render/components/editor-form-fields/EditorExpressionField.js
new file mode 100644
index 000000000..e6f8ae59d
--- /dev/null
+++ b/packages/form-js-editor/src/render/components/editor-form-fields/EditorExpressionField.js
@@ -0,0 +1,32 @@
+import { ExpressionField, iconsByType } from '@bpmn-io/form-js-viewer';
+import { editorFormFieldClasses } from '../Util';
+import { useService } from '../../hooks';
+
+const type = 'expression';
+
+export function EditorExpressionField(props) {
+ const { field } = props;
+ const { expression = '' } = field;
+
+ const Icon = iconsByType('expression');
+ const expressionLanguage = useService('expressionLanguage');
+
+ let placeholderContent = 'Expression is empty';
+
+ if (expression.trim() && expressionLanguage.isExpression(expression)) {
+ placeholderContent = 'Expression';
+ }
+
+ return (
+
+
+ {placeholderContent}
+
+
+ );
+}
+
+EditorExpressionField.config = {
+ ...ExpressionField.config,
+ escapeGridRender: false
+};
diff --git a/packages/form-js-editor/src/render/components/editor-form-fields/index.js b/packages/form-js-editor/src/render/components/editor-form-fields/index.js
index 126ed795c..bbd17c9e7 100644
--- a/packages/form-js-editor/src/render/components/editor-form-fields/index.js
+++ b/packages/form-js-editor/src/render/components/editor-form-fields/index.js
@@ -2,10 +2,12 @@ import { EditorIFrame } from './EditorIFrame';
import { EditorText } from './EditorText';
import { EditorHtml } from './EditorHtml';
import { EditorTable } from './EditorTable';
+import { EditorExpressionField } from './EditorExpressionField';
export const editorFormFields = [
EditorIFrame,
EditorText,
EditorHtml,
- EditorTable
+ EditorTable,
+ EditorExpressionField
];
\ No newline at end of file
diff --git a/packages/form-js-editor/test/spec/FormEditor.spec.js b/packages/form-js-editor/test/spec/FormEditor.spec.js
index 9c580fc77..663eb111c 100644
--- a/packages/form-js-editor/test/spec/FormEditor.spec.js
+++ b/packages/form-js-editor/test/spec/FormEditor.spec.js
@@ -403,10 +403,10 @@ describe('FormEditor', function() {
const exportedSchema = formEditor.saveSchema();
// then
- expect(exportedSchema).to.eql(exportTagged(schema));
+ const tagged = exportTagged(schema);
+ expect(exportedSchema).to.eql(tagged);
const exportedString = JSON.stringify(exportedSchema);
-
expect(exportedString).not.to.contain('"_path"');
expect(exportedString).not.to.contain('"_parent"');
});
diff --git a/packages/form-js-editor/test/spec/features/palette/PaletteModule.spec.js b/packages/form-js-editor/test/spec/features/palette/PaletteModule.spec.js
index 9b8e95045..213a762db 100644
--- a/packages/form-js-editor/test/spec/features/palette/PaletteModule.spec.js
+++ b/packages/form-js-editor/test/spec/features/palette/PaletteModule.spec.js
@@ -116,45 +116,6 @@ describe('features/palette', function() {
});
- it('should attach when section rendered late', async function() {
-
- // given
- const node = document.createElement('div');
- document.body.appendChild(node);
-
- let formEditor;
-
- await act(async () => {
- const result = await createEditor(schema);
- formEditor = result.formEditor;
- });
-
- const palette = formEditor.get('palette');
- const eventBus = formEditor.get('eventBus');
-
- // when
- await act(() => {
- return palette.attachTo(node);
- });
-
- // then
- expect(domQuery('.fjs-palette', paletteContainer)).to.exist;
- expect(domQuery('.fjs-palette', node)).to.not.exist;
-
- // when
- await act(() => {
- eventBus.fire('palette.section.rendered');
- });
-
- // then
- expect(domQuery('.fjs-palette', paletteContainer)).to.not.exist;
- expect(domQuery('.fjs-palette', node)).to.exist;
-
- // cleanup
- document.body.removeChild(node);
- });
-
-
it('should detach', async function() {
// given
diff --git a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js
index dd99d5c61..f9157d738 100644
--- a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js
+++ b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js
@@ -136,13 +136,11 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'ID'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'ID'
+ ]
+ });
});
@@ -293,16 +291,14 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Action'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Action'
+ ],
+ 'Condition': [],
+ 'Custom properties': []
+ });
});
@@ -352,25 +348,21 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Default value',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Default value',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
});
@@ -420,36 +412,28 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Static options',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Default value',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Static options', [
- [ 'Label', 2 ],
- [ 'Value', 2 ]
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Default value',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Static options': [
+ [ 'Label', 2 ],
+ [ 'Value', 2 ]
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
});
@@ -918,34 +902,23 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Dynamic options',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Dynamic options', [
- 'Input values key'
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Dynamic options': [
+ 'Input values key'
+ ],
+ 'Custom properties': []
+ });
});
});
@@ -966,36 +939,27 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Static options',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Static options', [
- [ 'Label', 3 ],
- [ 'Value', 3 ]
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
-
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Static options': [
+ [ 'Label', 3 ],
+ [ 'Value', 3 ]
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
});
@@ -1196,29 +1160,24 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Dynamic options',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Dynamic options': [
+ 'Input values key'
+ ],
+ 'Custom properties': []
+ });
- expectGroupEntries(container, 'Dynamic options', [
- 'Input values key'
- ]);
});
});
@@ -1239,35 +1198,28 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Static options',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Static options', [
- [ 'Label', 11 ],
- [ 'Value', 11 ]
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Static options': [
+ [ 'Label', 11 ],
+ [ 'Value', 11 ]
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -1507,29 +1459,23 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Dynamic options',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Dynamic options', [
- 'Input values key'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Dynamic options': [
+ 'Input values key'
+ ],
+ 'Custom properties': []
+ });
});
@@ -1649,34 +1595,27 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Options expression',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Options expression', [
- 'Options expression'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Options expression': [
+ 'Options expression'
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
});
});
@@ -1697,40 +1636,33 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Serialization',
- 'Constraints',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Date label',
- 'Time label',
- 'Field description',
- 'Key',
- 'Subtype',
- 'Use 24h',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Serialization', [
- 'Time format'
- ]);
-
- expectGroupEntries(container, 'Constraints', [
- 'Time interval',
- 'Disallow past dates'
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Date label',
+ 'Time label',
+ 'Field description',
+ 'Key',
+ 'Subtype',
+ 'Use 24h',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Serialization': [
+ 'Time format'
+ ],
+ 'Constraints': [
+ 'Time interval',
+ 'Disallow past dates'
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
});
+
});
@@ -1747,37 +1679,30 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Static options',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Default value',
- 'Searchable',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Static options', [
- [ 'Label', 2 ],
- [ 'Value', 2 ]
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Default value',
+ 'Searchable',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Static options': [
+ [ 'Label', 2 ],
+ [ 'Value', 2 ]
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -2173,34 +2098,27 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Dynamic options',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Dynamic options', [
- 'Input values key'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Dynamic options': [
+ 'Input values key'
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
});
});
@@ -2317,34 +2235,27 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Options source',
- 'Options expression',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Options source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Options expression', [
- 'Options expression'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Options source': [
+ 'Type'
+ ],
+ 'Options expression': [
+ 'Options expression'
+ ],
+ 'Validation': [
+ 'Required'
+ ],
+ 'Custom properties': []
+ });
- expectGroupEntries(container, 'Validation', [
- 'Required'
- ]);
});
});
@@ -2365,15 +2276,14 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Text'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Text'
+ ],
+ 'Condition': [],
+ 'Custom properties': []
+ });
+
});
});
@@ -2392,16 +2302,16 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Layout',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Height'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Height'
+ ],
+ 'Condition': [],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Custom properties': []
+ });
});
@@ -2421,31 +2331,23 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Layout',
- 'Appearance',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Group label',
- 'Path'
- ]);
-
- expectGroupEntries(container, 'Condition', [
- 'Hide if'
- ]);
-
- expectGroupEntries(container, 'Layout', [
- 'Columns'
- ]);
-
- expectGroupEntries(container, 'Appearance', [
- 'Show outline',
- 'Vertical alignment'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Group label',
+ 'Path'
+ ],
+ 'Condition': [
+ 'Hide if'
+ ],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Appearance': [
+ 'Show outline',
+ 'Vertical alignment'
+ ],
+ 'Custom properties': []
+ });
});
@@ -2465,35 +2367,27 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Layout',
- 'Appearance',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Group label',
- 'Path',
- 'Default number of items',
- 'Allow add/delete items',
- 'Disable collapse',
- 'Number of non-collapsing items'
- ]);
-
- expectGroupEntries(container, 'Condition', [
- 'Hide if'
- ]);
-
- expectGroupEntries(container, 'Layout', [
- 'Columns'
- ]);
-
- expectGroupEntries(container, 'Appearance', [
- 'Show outline',
- 'Vertical alignment'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Group label',
+ 'Path',
+ 'Default number of items',
+ 'Allow add/delete items',
+ 'Disable collapse',
+ 'Number of non-collapsing items'
+ ],
+ 'Condition': [
+ 'Hide if'
+ ],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Appearance': [
+ 'Show outline',
+ 'Vertical alignment'
+ ],
+ 'Custom properties': []
+ });
});
@@ -2513,29 +2407,26 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Default value',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required',
- 'Minimum length',
- 'Maximum length',
- 'Validation pattern',
- 'Custom regular expression'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Default value',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Validation': [
+ 'Required',
+ 'Minimum length',
+ 'Maximum length',
+ 'Validation pattern',
+ 'Custom regular expression'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -2819,34 +2710,29 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Serialization',
- 'Validation',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Field label',
- 'Field description',
- 'Key',
- 'Default value',
- 'Decimal digits',
- 'Increment',
- 'Disabled',
- 'Read only'
- ]);
-
- expectGroupEntries(container, 'Serialization', [
- 'Output as string'
- ]);
-
- expectGroupEntries(container, 'Validation', [
- 'Required',
- 'Minimum',
- 'Maximum'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Field label',
+ 'Field description',
+ 'Key',
+ 'Default value',
+ 'Decimal digits',
+ 'Increment',
+ 'Disabled',
+ 'Read only'
+ ],
+ 'Condition': [],
+ 'Serialization': [
+ 'Output as string'
+ ],
+ 'Validation': [
+ 'Required',
+ 'Minimum',
+ 'Maximum'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -3505,16 +3391,15 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Condition',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Image source',
- 'Alternative text',
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Image source',
+ 'Alternative text'
+ ],
+ 'Condition': [],
+ 'Custom properties': []
+ });
+
});
@@ -3636,6 +3521,39 @@ describe('properties panel', function() {
});
+ describe('expression field', function() {
+
+ it('entries', function() {
+
+ // given
+ const field = schema.components.find(({ type }) => type === 'expression');
+
+ bootstrapPropertiesPanel({
+ container,
+ field
+ });
+
+ // then
+ expectPanelStructure(container, {
+ 'General': [
+ 'Key',
+ 'Target value',
+ 'Compute on'
+ ],
+ 'Condition': [
+ 'Deactivate if'
+ ],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Custom properties': []
+ });
+
+ });
+
+ });
+
+
describe('iframe', function() {
it('entries', function() {
@@ -3649,18 +3567,19 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Security attributes',
- 'Layout',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Title',
- 'URL',
- 'Height',
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Title',
+ 'URL',
+ 'Height'
+ ],
+ 'Security attributes': [],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -3786,30 +3705,27 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Headers source',
- 'Header items',
- 'Condition',
- 'Layout',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Table label',
- 'Data source',
- 'Pagination',
- 'Number of rows per page'
- ]);
-
- expectGroupEntries(container, 'Headers source', [
- 'Type'
- ]);
-
- expectGroupEntries(container, 'Header items', [
- [ 'Label', 3 ],
- [ 'Key', 3 ]
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Table label',
+ 'Data source',
+ 'Pagination',
+ 'Number of rows per page'
+ ],
+ 'Headers source': [
+ 'Type'
+ ],
+ 'Header items': [
+ [ 'Label', 3 ],
+ [ 'Key', 3 ]
+ ],
+ 'Condition': [],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -3824,25 +3740,24 @@ describe('properties panel', function() {
});
// then
- expectGroups(container, [
- 'General',
- 'Headers source',
- 'Condition',
- 'Layout',
- 'Custom properties'
- ]);
-
- expectGroupEntries(container, 'General', [
- 'Table label',
- 'Data source',
- 'Pagination',
- 'Number of rows per page'
- ]);
-
- expectGroupEntries(container, 'Headers source', [
- 'Type',
- 'Expression'
- ]);
+ expectPanelStructure(container, {
+ 'General': [
+ 'Table label',
+ 'Data source',
+ 'Pagination',
+ 'Number of rows per page'
+ ],
+ 'Headers source': [
+ 'Type',
+ 'Expression'
+ ],
+ 'Condition': [],
+ 'Layout': [
+ 'Columns'
+ ],
+ 'Custom properties': []
+ });
+
});
@@ -4474,6 +4389,18 @@ function createPropertiesPanel({ services, ...restOptions } = {}, renderFn = ren
});
}
+
+function expectPanelStructure(container, panelStructure) {
+ const groupNames = Object.keys(panelStructure);
+
+ expectGroups(container, groupNames);
+
+ groupNames.forEach(group => {
+ const entries = panelStructure[group];
+ expectGroupEntries(container, group, entries);
+ });
+}
+
function expectGroups(container, groupLabels) {
groupLabels.forEach(groupLabel => {
expect(findGroup(container, groupLabel)).to.exist;
diff --git a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanelModule.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanelModule.spec.js
index ab4e7b2e4..25e58dd20 100644
--- a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanelModule.spec.js
+++ b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanelModule.spec.js
@@ -117,45 +117,6 @@ describe('features/propertiesPanel', function() {
});
- it.skip('should attach when section rendered late', async function() {
-
- // given
- const node = document.createElement('div');
- document.body.appendChild(node);
-
- let formEditor;
-
- await act(async () => {
- const result = await createEditor(schema);
- formEditor = result.formEditor;
- });
-
- const propertiesPanel = formEditor.get('propertiesPanel');
- const eventBus = formEditor.get('eventBus');
-
- // when
- await act(() => {
- return propertiesPanel.attachTo(node);
- });
-
- // then
- expect(domQuery('.fjs-properties-panel', propertiesPanelContainer)).to.exist;
- expect(domQuery('.fjs-properties-panel', node)).to.not.exist;
-
- // when
- await act(() => {
- eventBus.fire('propertiesPanel.section.rendered');
- });
-
- // then
- expect(domQuery('.fjs-properties-panel', propertiesPanelContainer)).to.not.exist;
- expect(domQuery('.fjs-properties-panel', node)).to.exist;
-
- // cleanup
- document.body.removeChild(node);
- });
-
-
it('should detach', async function() {
// given
diff --git a/packages/form-js-editor/test/spec/form.json b/packages/form-js-editor/test/spec/form.json
index 0781cdf57..62589717b 100644
--- a/packages/form-js-editor/test/spec/form.json
+++ b/packages/form-js-editor/test/spec/form.json
@@ -3,6 +3,13 @@
"id": "Form_1",
"type": "default",
"components": [
+ {
+ "id": "ExpressionField_1",
+ "type": "expression",
+ "key": "expression",
+ "expression": "= 3 + 4",
+ "computeOn": "change"
+ },
{
"id": "Text_1",
"type": "text",
diff --git a/packages/form-js-playground/src/Playground.js b/packages/form-js-playground/src/Playground.js
index 57d861f8c..2f53fb240 100644
--- a/packages/form-js-playground/src/Playground.js
+++ b/packages/form-js-playground/src/Playground.js
@@ -29,20 +29,17 @@ import { PlaygroundRoot } from './components/PlaygroundRoot';
/**
* @param {FormPlaygroundOptions} options
*/
-export function Playground(options) {
+function Playground(options) {
const {
container: parent,
- schema,
- data,
+ schema: initialSchema,
+ data: initialData,
...rest
} = options;
const emitter = mitt();
- let state = { data, schema };
- let ref;
-
const container = document.createElement('div');
container.classList.add('fjs-pgl-parent');
@@ -56,7 +53,7 @@ export function Playground(options) {
if (file) {
try {
- ref.setSchema(JSON.parse(file.contents));
+ this.api.setSchema(JSON.parse(file.contents));
} catch (err) {
// TODO(nikku): indicate JSON parse error
@@ -64,9 +61,9 @@ export function Playground(options) {
}
});
- const withRef = function(fn) {
+ const safe = function(fn) {
return function(...args) {
- if (!ref) {
+ if (!this.api) {
throw new Error('Playground is not initialized.');
}
@@ -74,20 +71,14 @@ export function Playground(options) {
};
};
- const onInit = function(_ref) {
- ref = _ref;
- emitter.emit('formPlayground.init');
- };
-
container.addEventListener('dragover', handleDrop);
render(
state = _state }
- schema={ schema }
+ apiLinkTarget={ this }
{ ...rest }
/>,
container
@@ -98,47 +89,42 @@ export function Playground(options) {
this.emit = emitter.emit;
- this.on('destroy', function() {
+ this.on('destroy', () => {
render(null, container);
- });
-
- this.on('destroy', function() {
parent.removeChild(container);
});
- this.getState = function() {
- return state;
- };
+ this.destroy = () => this.emit('destroy');
- this.getSchema = withRef(() => ref.getSchema());
+ this.getState = safe(() => this.api.getState());
- this.setSchema = withRef((schema) => ref.setSchema(schema));
+ this.getSchema = safe(() => this.api.getSchema());
- this.saveSchema = withRef(() => ref.saveSchema());
+ this.setSchema = safe((schema) => this.api.setSchema(schema));
- this.get = withRef((name, strict) => ref.get(name, strict));
+ this.saveSchema = safe(() => this.api.saveSchema());
- this.getDataEditor = withRef(() => ref.getDataEditor());
+ this.get = safe((name, strict) => this.api.get(name, strict));
- this.getEditor = withRef(() => ref.getEditor());
+ this.getDataEditor = safe(() => this.api.getDataEditor());
- this.getForm = withRef((name, strict) => ref.getForm(name, strict));
+ this.getEditor = safe(() => this.api.getEditor());
- this.getResultView = withRef(() => ref.getResultView());
+ this.getForm = safe((name, strict) => this.api.getForm(name, strict));
- this.destroy = function() {
- this.emit('destroy');
- };
+ this.getResultView = safe(() => this.api.getResultView());
+
+ this.attachEditorContainer = safe((node) => this.api.attachEditorContainer(node));
- this.attachEditorContainer = withRef((node) => ref.attachEditorContainer(node));
+ this.attachPreviewContainer = safe((node) => this.api.attachFormContainer(node));
- this.attachPreviewContainer = withRef((node) => ref.attachFormContainer(node));
+ this.attachDataContainer = safe((node) => this.api.attachDataContainer(node));
- this.attachDataContainer = withRef((node) => ref.attachDataContainer(node));
+ this.attachResultContainer = safe((node) => this.api.attachResultContainer(node));
- this.attachResultContainer = withRef((node) => ref.attachResultContainer(node));
+ this.attachPaletteContainer = safe((node) => this.api.attachPaletteContainer(node));
- this.attachPaletteContainer = withRef((node) => ref.attachPaletteContainer(node));
+ this.attachPropertiesPanelContainer = safe((node) => this.api.attachPropertiesPanelContainer(node));
+}
- this.attachPropertiesPanelContainer = withRef((node) => ref.attachPropertiesPanelContainer(node));
-}
\ No newline at end of file
+export { Playground };
\ No newline at end of file
diff --git a/packages/form-js-playground/src/components/PlaygroundRoot.js b/packages/form-js-playground/src/components/PlaygroundRoot.js
index 8e2529913..528f3e383 100644
--- a/packages/form-js-playground/src/components/PlaygroundRoot.js
+++ b/packages/form-js-playground/src/components/PlaygroundRoot.js
@@ -17,104 +17,77 @@ import { EmbedModal } from './EmbedModal';
import { JSONEditor } from './JSONEditor';
import { Section } from './Section';
-
import './FileDrop.css';
import './PlaygroundRoot.css';
-
-export function PlaygroundRoot(props) {
+export function PlaygroundRoot(config) {
const {
- additionalModules = [], // goes into both editor + viewer
- actions: actionsConfig = {},
+ additionalModules, // goes into both editor + viewer
+ actions: actionsConfig,
emit,
- exporter: exporterConfig = {},
- viewerProperties = {},
- editorProperties = {},
- viewerAdditionalModules = [],
- editorAdditionalModules = [],
- propertiesPanel: propertiesPanelConfig = {},
- onInit: onPlaygroundInit,
- onStateChanged
- } = props;
+ exporter: exporterConfig,
+ viewerProperties,
+ editorProperties,
+ viewerAdditionalModules,
+ editorAdditionalModules,
+ propertiesPanel: propertiesPanelConfig,
+ apiLinkTarget
+ } = config;
const {
display: displayActions = true
- } = actionsConfig;
+ } = actionsConfig || {};
- const paletteContainerRef = useRef();
const editorContainerRef = useRef();
- const formContainerRef = useRef();
- const dataContainerRef = useRef();
- const resultContainerRef = useRef();
+ const paletteContainerRef = useRef();
const propertiesPanelContainerRef = useRef();
+ const viewerContainerRef = useRef();
+ const inputDataContainerRef = useRef();
+ const outputDataContainerRef = useRef();
- const paletteRef = useRef();
const formEditorRef = useRef();
- const formRef = useRef();
- const dataEditorRef = useRef();
- const resultViewRef = useRef();
- const propertiesPanelRef = useRef();
+ const formViewerRef = useRef();
+ const inputDataRef = useRef();
+ const outputDataRef = useRef();
const [ showEmbed, setShowEmbed ] = useState(false);
+ const [ schema, setSchema ] = useState();
+ const [ data, setData ] = useState();
+
+ const load = useCallback((schema, data) => {
+ formEditorRef.current.importSchema(schema, data);
+ inputDataRef.current.setValue(toString(data));
+ setSchema(schema);
+ setData(data);
+ }, []);
- const [ initialData ] = useState(props.data || undefined);
- const [ initialSchema, setInitialSchema ] = useState(props.schema);
-
- const [ data, setData ] = useState(props.data || {});
- const [ schema, setSchema ] = useState(props.schema);
-
- const [ resultData, setResultData ] = useState({});
-
- // pipe to playground API
- useEffect(() => {
- onPlaygroundInit({
- attachDataContainer: (node) => dataEditorRef.current.attachTo(node),
- attachEditorContainer: (node) => formEditorRef.current.attachTo(node),
- attachFormContainer: (node) => formRef.current.attachTo(node),
- attachPaletteContainer: (node) => paletteRef.current.attachTo(node),
- attachPropertiesPanelContainer: (node) => propertiesPanelRef.current.attachTo(node),
- attachResultContainer: (node) => resultViewRef.current.attachTo(node),
- get: (name, strict) => formEditorRef.current.get(name, strict),
- getDataEditor: () => dataEditorRef.current,
- getEditor: () => formEditorRef.current,
- getForm: () => formRef.current,
- getResultView: () => resultViewRef.current,
- getSchema: () => formEditorRef.current.getSchema(),
- setSchema: setInitialSchema,
- saveSchema: () => formEditorRef.current.saveSchema()
- });
- }, [ onPlaygroundInit ]);
-
- useEffect(() => {
- setInitialSchema(props.schema || {});
- }, [ props.schema ]);
-
+ // initialize and link the editors
useEffect(() => {
- const dataEditor = dataEditorRef.current = new JSONEditor({
- value: toString(data),
+ const inputDataEditor = inputDataRef.current = new JSONEditor({
contentAttributes: { 'aria-label': 'Form Input', tabIndex: 0 },
placeholder: createDataEditorPlaceholder()
});
- const resultView = resultViewRef.current = new JSONEditor({
+ const outputDataEditor = outputDataRef.current = new JSONEditor({
readonly: true,
- value: toString(resultData),
contentAttributes: { 'aria-label': 'Form Output', tabIndex: 0 }
});
- const form = formRef.current = new Form({
+ const formViewer = formViewerRef.current = new Form({
+ container: viewerContainerRef.current,
additionalModules: [
- ...additionalModules,
- ...viewerAdditionalModules
+ ...(additionalModules || []),
+ ...(viewerAdditionalModules || [])
],
properties: {
- ...viewerProperties,
+ ...(viewerProperties || {}),
'ariaLabel': 'Form Preview'
}
});
const formEditor = formEditorRef.current = new FormEditor({
+ container: editorContainerRef.current,
renderer: {
compact: true
},
@@ -123,22 +96,19 @@ export function PlaygroundRoot(props) {
},
propertiesPanel: {
parent: propertiesPanelContainerRef.current,
- ...propertiesPanelConfig
+ ...(propertiesPanelConfig || {})
},
exporter: exporterConfig,
properties: {
- ...editorProperties,
+ ...(editorProperties || {}),
'ariaLabel': 'Form Definition'
},
additionalModules: [
- ...additionalModules,
- ...editorAdditionalModules
+ ...(additionalModules || []),
+ ...(editorAdditionalModules || [])
]
});
- paletteRef.current = formEditor.get('palette');
- propertiesPanelRef.current = formEditor.get('propertiesPanel');
-
formEditor.on('formField.add', ({ formField }) => {
const formFields = formEditor.get('formFields');
const { config } = formFields.get(formField.type);
@@ -161,7 +131,7 @@ export function PlaygroundRoot(props) {
[id]: initialDemoData,
};
- dataEditorRef.current.setValue(
+ inputDataRef.current.setValue(
toString(newData)
);
@@ -179,11 +149,13 @@ export function PlaygroundRoot(props) {
emit('formPlayground.rendered');
});
- form.on('changed', () => {
- setResultData(form._getSubmitData());
+ // pipe viewer changes to output data editor
+ formViewer.on('changed', () => {
+ const submitData = formViewer._getSubmitData();
+ outputDataEditor.setValue(toString(submitData));
});
- dataEditor.on('changed', event => {
+ inputDataEditor.on('changed', event => {
try {
setData(JSON.parse(event.value));
} catch (error) {
@@ -193,59 +165,77 @@ export function PlaygroundRoot(props) {
}
});
- const formContainer = formContainerRef.current;
- const editorContainer = editorContainerRef.current;
- const dataContainer = dataContainerRef.current;
- const resultContainer = resultContainerRef.current;
-
- dataEditor.attachTo(dataContainer);
- resultView.attachTo(resultContainer);
- form.attachTo(formContainer);
- formEditor.attachTo(editorContainer);
+ inputDataEditor.attachTo(inputDataContainerRef.current);
+ outputDataEditor.attachTo(outputDataContainerRef.current);
return () => {
- dataEditor.destroy();
- resultView.destroy();
- form.destroy();
+ inputDataEditor.destroy();
+ outputDataEditor.destroy();
+ formViewer.destroy();
formEditor.destroy();
};
- }, []);
+ }, [ additionalModules, editorAdditionalModules, editorProperties, emit, exporterConfig, propertiesPanelConfig, viewerAdditionalModules, viewerProperties ]);
+ // initialize data through props
useEffect(() => {
- dataEditorRef.current.setValue(toString(initialData));
- }, [ initialData ]);
+ if (!config.initialSchema) {
+ return;
+ }
+
+ load(config.initialSchema, config.initialData || {});
+ }, [ config.initialSchema, config.initialData, load ]);
useEffect(() => {
- if (initialSchema) {
- formEditorRef.current.importSchema(initialSchema);
- dataEditorRef.current.setVariables(getSchemaVariables(initialSchema));
- }
- }, [ initialSchema ]);
+ schema && formViewerRef.current.importSchema(schema, data);
+ }, [ schema, data ]);
useEffect(() => {
- if (schema && dataContainerRef.current) {
+ if (schema && inputDataContainerRef.current) {
const variables = getSchemaVariables(schema);
- dataEditorRef.current.setVariables(variables);
+ inputDataRef.current.setVariables(variables);
}
}, [ schema ]);
+ // exposes api to parent
useEffect(() => {
- schema && formRef.current.importSchema(schema, data);
- }, [ schema, data ]);
- useEffect(() => {
- resultViewRef.current.setValue(toString(resultData));
- }, [ resultData ]);
+ if (!apiLinkTarget) {
+ return;
+ }
+
+ apiLinkTarget.api = {
+ attachDataContainer: (node) => inputDataRef.current.attachTo(node),
+ attachResultContainer: (node) => outputDataRef.current.attachTo(node),
+ attachFormContainer: (node) => formViewerRef.current.attachTo(node),
+ attachEditorContainer: (node) => formEditorRef.current.attachTo(node),
+ attachPaletteContainer: (node) => formEditorRef.current.get('palette').attachTo(node),
+ attachPropertiesPanelContainer: (node) => formEditorRef.current.get('propertiesPanel').attachTo(node),
+ get: (name, strict) => formEditorRef.current.get(name, strict),
+ getDataEditor: () => inputDataRef.current,
+ getEditor: () => formEditorRef.current,
+ getForm: () => formViewerRef.current,
+ getResultView: () => outputDataRef.current,
+ getSchema: () => formEditorRef.current.getSchema(),
+ saveSchema: () => formEditorRef.current.saveSchema(),
+ setSchema: setSchema,
+ setData: setData
+ };
+ }, [ apiLinkTarget ]);
+
+ // separate effect for state to avoid re-creating the api object every time
useEffect(() => {
- onStateChanged && onStateChanged({
- schema,
- data
- });
- }, [ onStateChanged, schema, data ]);
- const handleDownload = useCallback(() => {
+ if (!apiLinkTarget) {
+ return;
+ }
+
+ apiLinkTarget.api.getState = () => ({ schema, data });
+ apiLinkTarget.api.load = load;
+ }, [ apiLinkTarget, schema, data, load ]);
+
+ const handleDownload = useCallback(() => {
download(JSON.stringify(schema, null, ' '), 'form.json', 'text/json');
}, [ schema ]);
@@ -294,13 +284,13 @@ export function PlaygroundRoot(props) {
diff --git a/packages/form-js-playground/test/spec/form.json b/packages/form-js-playground/test/spec/form.json
index aef7c4fc3..b507bed89 100644
--- a/packages/form-js-playground/test/spec/form.json
+++ b/packages/form-js-playground/test/spec/form.json
@@ -1,6 +1,12 @@
{
"$schema": "../../../form-json-schema/resources/schema.json",
"components": [
+ {
+ "type": "expression",
+ "key": "expressionResult",
+ "expression": "= 3 + 4",
+ "computeOn": "change"
+ },
{
"type": "text",
"text": "# Invoice\nLorem _ipsum_ __dolor__ `sit`.\n \n \nA list of BPMN symbols:\n* Start Event\n* Task\nLearn more about [forms](https://bpmn.io).\n \n \nThis [malicious link](javascript:throw onerror=alert,'some string',123,'haha') __should not work__.",
@@ -19,8 +25,8 @@
},
{
"type": "iframe",
- "label": "The bpmn-io web page",
- "url": "https://bpmn.io/"
+ "label": "An example page rendered in an iframe",
+ "url": "https://example.com/"
},
{
"type": "group",
diff --git a/packages/form-js-viewer/src/Form.js b/packages/form-js-viewer/src/Form.js
index a8bc11ff8..5fcaae12f 100644
--- a/packages/form-js-viewer/src/Form.js
+++ b/packages/form-js-viewer/src/Form.js
@@ -175,6 +175,8 @@ export class Form {
throw new Error('form is read-only');
}
+ this._emit('presubmit');
+
const data = this._getSubmitData();
const errors = this.validate();
diff --git a/packages/form-js-viewer/src/index.js b/packages/form-js-viewer/src/index.js
index 1596ad56d..ca6295950 100644
--- a/packages/form-js-viewer/src/index.js
+++ b/packages/form-js-viewer/src/index.js
@@ -5,7 +5,7 @@ export * from './render';
export * from './util';
export * from './features';
-const schemaVersion = 15;
+const schemaVersion = 16;
export {
Form,
diff --git a/packages/form-js-viewer/src/render/components/FormField.js b/packages/form-js-viewer/src/render/components/FormField.js
index 06490a332..027641ac3 100644
--- a/packages/form-js-viewer/src/render/components/FormField.js
+++ b/packages/form-js-viewer/src/render/components/FormField.js
@@ -53,6 +53,8 @@ export function FormField(props) {
throw new Error(`cannot render field <${field.type}>`);
}
+ const fieldConfig = FormFieldComponent.config;
+
const valuePath = useMemo(() => pathRegistry.getValuePath(field, { indexes }), [ field, indexes, pathRegistry ]);
const initialValue = useMemo(() => get(initialData, valuePath), [ initialData, valuePath ]);
@@ -121,8 +123,8 @@ export function FormField(props) {
setInitialValidationTrigger(false);
// add indexes of the keyed field to the update, if any
- onChange(FormFieldComponent.config.keyed ? { ...update, indexes } : update);
- }, [ onChange, FormFieldComponent.config.keyed, indexes ]);
+ onChange(fieldConfig.keyed ? { ...update, indexes } : update);
+ }, [ onChange, fieldConfig.keyed, indexes ]);
if (hidden) {
return ;
@@ -131,19 +133,28 @@ export function FormField(props) {
const domId = `${prefixId(field.id, formId, indexes)}`;
const fieldErrors = get(errors, [ field.id, ...Object.values(indexes || {}) ]) || [];
+ const formFieldElement = (
+
+ );
+
+ if (fieldConfig.escapeGridRender) {
+ return formFieldElement;
+ }
+
return (
-
+ { formFieldElement }
);
diff --git a/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js b/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js
new file mode 100644
index 000000000..5cd6d5321
--- /dev/null
+++ b/packages/form-js-viewer/src/render/components/form-fields/ExpressionField.js
@@ -0,0 +1,50 @@
+import { useCallback, useEffect } from 'preact/hooks';
+import { useExpressionEvaluation, useDeepCompareMemoize, useService, useEffectOnChange } from '../../hooks';
+
+const type = 'expression';
+
+export function ExpressionField(props) {
+ const {
+ field,
+ onChange,
+ } = props;
+
+ const {
+ computeOn,
+ expression
+ } = field;
+
+ const evaluation = useExpressionEvaluation(expression);
+ const evaluationMemo = useDeepCompareMemoize(evaluation);
+ const eventBus = useService('eventBus');
+
+ const sendValue = useCallback(() => {
+ onChange && onChange({ field, value: evaluationMemo });
+ }, [ field, evaluationMemo, onChange ]);
+
+ useEffectOnChange(evaluationMemo, () => {
+ if (computeOn !== 'change') { return; }
+ sendValue();
+ }, [ computeOn, sendValue ]);
+
+ useEffect(() => {
+ if (computeOn === 'presubmit') {
+ eventBus.on('presubmit', sendValue);
+ return () => eventBus.off('presubmit', sendValue);
+ }
+ }, [ computeOn, sendValue, eventBus ]);
+
+ return null;
+}
+
+ExpressionField.config = {
+ type,
+ label: 'Expression',
+ group: 'basic-input',
+ keyed: true,
+ escapeGridRender: true,
+ create: (options = {}) => ({
+ computeOn: 'change',
+ ...options,
+ })
+};
\ No newline at end of file
diff --git a/packages/form-js-viewer/src/render/components/icons/ExpressionField.svg b/packages/form-js-viewer/src/render/components/icons/ExpressionField.svg
new file mode 100644
index 000000000..a968184d5
--- /dev/null
+++ b/packages/form-js-viewer/src/render/components/icons/ExpressionField.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/form-js-viewer/src/render/components/icons/index.js b/packages/form-js-viewer/src/render/components/icons/index.js
index 1523c6a1a..f10b67051 100644
--- a/packages/form-js-viewer/src/render/components/icons/index.js
+++ b/packages/form-js-viewer/src/render/components/icons/index.js
@@ -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 ExpressionFieldIcon from './ExpressionField.svg';
import TextfieldIcon from './Textfield.svg';
import TextareaIcon from './Textarea.svg';
import IFrameIcon from './IFrame.svg';
@@ -31,6 +32,7 @@ export const iconsByType = (type) => {
iframe: IFrameIcon,
image: ImageIcon,
number: NumberIcon,
+ expression: ExpressionFieldIcon,
radio: RadioIcon,
select: SelectIcon,
separator: SeparatorIcon,
diff --git a/packages/form-js-viewer/src/render/components/index.js b/packages/form-js-viewer/src/render/components/index.js
index 093ea5cf6..feaeea903 100644
--- a/packages/form-js-viewer/src/render/components/index.js
+++ b/packages/form-js-viewer/src/render/components/index.js
@@ -15,6 +15,7 @@ import { DynamicList } from './form-fields/DynamicList';
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 { Textfield } from './form-fields/Textfield';
import { Textarea } from './form-fields/Textarea';
import { Table } from './form-fields/Table';
@@ -44,6 +45,7 @@ export {
DynamicList,
Image,
Numberfield,
+ ExpressionField,
Radio,
Select,
Separator,
@@ -69,6 +71,7 @@ export const formFields = [
Taglist,
Textfield,
Textarea,
+ ExpressionField,
Text,
Image,
Table,
diff --git a/packages/form-js-viewer/src/render/hooks/index.js b/packages/form-js-viewer/src/render/hooks/index.js
index 2185d0324..0a9fc17e9 100644
--- a/packages/form-js-viewer/src/render/hooks/index.js
+++ b/packages/form-js-viewer/src/render/hooks/index.js
@@ -11,6 +11,7 @@ export { useReadonly } from './useReadonly';
export { useService } from './useService';
export { usePrevious } from './usePrevious';
export { useFlushDebounce } from './useFlushDebounce';
+export { useEffectOnChange } from './useEffectOnChange';
export { useDeepCompareMemoize } from './useDeepCompareMemoize';
export { useSingleLineTemplateEvaluation } from './useSingleLineTemplateEvaluation';
export { useTemplateEvaluation } from './useTemplateEvaluation';
diff --git a/packages/form-js-viewer/src/render/hooks/useEffectOnChange.js b/packages/form-js-viewer/src/render/hooks/useEffectOnChange.js
new file mode 100644
index 000000000..ed66987b2
--- /dev/null
+++ b/packages/form-js-viewer/src/render/hooks/useEffectOnChange.js
@@ -0,0 +1,16 @@
+import { useEffect } from 'react';
+import { usePrevious } from './usePrevious';
+
+function useEffectOnChange(value, callback, dependencies = []) {
+ const previousValue = usePrevious(value);
+
+ useEffect(() => {
+ if (value !== previousValue) {
+ callback();
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ value, ...dependencies ]);
+}
+
+export { useEffectOnChange };
diff --git a/packages/form-js-viewer/test/spec/render/components/form-fields/ExpressionField.spec.js b/packages/form-js-viewer/test/spec/render/components/form-fields/ExpressionField.spec.js
new file mode 100644
index 000000000..2763cfe55
--- /dev/null
+++ b/packages/form-js-viewer/test/spec/render/components/form-fields/ExpressionField.spec.js
@@ -0,0 +1,203 @@
+import {
+ render
+} from '@testing-library/preact/pure';
+
+import { ExpressionField } from '../../../../../src/render/components/form-fields/ExpressionField';
+
+import { MockFormContext } from '../helper';
+import { EventBusMock } from '../helper/mocks';
+import { act } from 'preact/test-utils';
+
+import {
+ createFormContainer
+} from '../../../../TestHelper';
+
+let container;
+
+describe('ExpressionField', function() {
+
+ beforeEach(function() {
+ container = createFormContainer();
+ });
+
+
+ afterEach(function() {
+ container.remove();
+ });
+
+
+ it('should evaluate its expression on initialization when set to onChange', function() {
+
+ // given
+ const field = {
+ ...defaultField,
+ expression: '=1 + 1'
+ };
+
+ const services = {
+ expressionLanguage: {
+ isExpression: () => true,
+ evaluate: () => {
+ return 1 + 1;
+ }
+ }
+ };
+
+ const onChangeSpy = sinon.spy();
+
+ // when
+ act(() => {
+ createExpressionField({ field, onChange: onChangeSpy, services });
+ });
+
+ // then
+ expect(onChangeSpy.calledWith({ field, value: 2 })).to.be.true;
+
+ });
+
+
+ it('should re-evaluate when the expression result changes', function() {
+
+ // given
+ const field = {
+ ...defaultField,
+ expression: '=1 + 1'
+ };
+
+ const services = {
+ expressionLanguage: {
+ isExpression: () => true,
+ evaluate: () => {
+ return 1 + 1;
+ }
+ }
+ };
+
+ const onChangeSpy = sinon.spy();
+
+ services.expressionLanguage.evaluate = () => {
+ return 1 + 2;
+ };
+
+ const { rerender } = createExpressionField({ field, onChange: onChangeSpy, services });
+
+ // when
+ act(() => {
+ rerender(
+
+
+
+ );
+ });
+
+ // then
+ expect(onChangeSpy.calledWith({ field, value: 3 })).to.be.true;
+
+ });
+
+
+ it('should not evaluate on intialization if computeOn presubmit', function() {
+
+ // given
+ const field = {
+ ...defaultField,
+ computeOn: 'presubmit',
+ expression: '=1 + 1'
+ };
+
+ const services = {
+ expressionLanguage: {
+ isExpression: () => true,
+ evaluate: () => {
+ return 1 + 1;
+ }
+ },
+ eventBus: new EventBusMock()
+ };
+
+ const onChangeSpy = sinon.spy();
+
+ // when
+ act(() => {
+ createExpressionField({ field, onChange: onChangeSpy, services });
+ });
+
+ // then
+ expect(onChangeSpy.called).to.be.false;
+
+ });
+
+
+ it('should evaluate on presubmit', function() {
+
+ // given
+ const field = {
+ ...defaultField,
+ computeOn: 'presubmit',
+ expression: '=1 + 1'
+ };
+
+ const services = {
+ expressionLanguage: {
+ isExpression: () => true,
+ evaluate: () => {
+ return 1 + 1;
+ }
+ },
+ eventBus: new EventBusMock()
+ };
+
+ const onChangeSpy = sinon.spy();
+
+ createExpressionField({ field, onChange: onChangeSpy, services });
+
+ // when
+ act(() => {
+ services.eventBus.fire('presubmit');
+ });
+
+ // then
+ expect(onChangeSpy.calledWith({ field, value: 2 })).to.be.true;
+
+ });
+
+});
+
+// helpers //////////
+
+const defaultField = {
+ type: 'expression',
+ key: 'expressionResult',
+ computeOn: 'change',
+ expression: ''
+};
+
+function createExpressionField({ services, ...restOptions } = {}) {
+ const options = {
+ field: defaultField,
+ onChange: () => {},
+ ...restOptions
+ };
+
+ return render(
+
+
+ , {
+ container: options.container || container.querySelector('.fjs-form')
+ }
+ );
+}
\ No newline at end of file
diff --git a/packages/form-js-viewer/test/spec/render/components/form-fields/Table.spec.js b/packages/form-js-viewer/test/spec/render/components/form-fields/Table.spec.js
index 3b5a97234..a8190ddf2 100644
--- a/packages/form-js-viewer/test/spec/render/components/form-fields/Table.spec.js
+++ b/packages/form-js-viewer/test/spec/render/components/form-fields/Table.spec.js
@@ -355,7 +355,7 @@ describe('Table', function() {
});
- it('should render iframe title (expression)', function() {
+ it('should render table title (expression)', function() {
// when
const label = 'foo';
diff --git a/packages/form-js/test/spec/Form.spec.js b/packages/form-js/test/spec/Form.spec.js
index c5853fb21..3d1327dfa 100644
--- a/packages/form-js/test/spec/Form.spec.js
+++ b/packages/form-js/test/spec/Form.spec.js
@@ -128,7 +128,7 @@ describe('viewer exports', function() {
it('should expose schemaVersion', function() {
expect(typeof schemaVersion).to.eql('number');
- expect(schemaVersion).to.eql(15);
+ expect(schemaVersion).to.eql(16);
});
diff --git a/packages/form-js/test/spec/FormPlayground.spec.js b/packages/form-js/test/spec/FormPlayground.spec.js
index 83c3e457f..efc6ed660 100644
--- a/packages/form-js/test/spec/FormPlayground.spec.js
+++ b/packages/form-js/test/spec/FormPlayground.spec.js
@@ -4,6 +4,8 @@ import {
import schema from './form.json';
+import { act } from '@testing-library/preact/pure';
+
import { insertStyles } from '../TestHelper';
import {
@@ -24,7 +26,7 @@ describe('playground exports', function() {
});
- it('should render', function() {
+ it('should render', async function() {
// given
const data = {
@@ -53,16 +55,21 @@ describe('playground exports', function() {
language: 'english'
};
+ let playground;
+
// when
- const playground = new FormPlayground({
- container,
- schema,
- data
+ await act(() => {
+ playground = new FormPlayground({
+ container,
+ schema,
+ data
+ });
});
// then
expect(playground).to.exist;
+ // @ts-ignore
expect(playground.getState()).to.eql({
schema,
data
diff --git a/packages/form-json-schema/src/defs/examples/components.json b/packages/form-json-schema/src/defs/examples/components.json
index b89ae3e0e..b792197f1 100644
--- a/packages/form-json-schema/src/defs/examples/components.json
+++ b/packages/form-json-schema/src/defs/examples/components.json
@@ -1,167 +1,175 @@
-{
- "examples": [
- [
- {
- "text": "Create a text",
- "type": "text"
- }
- ],
- [
- {
- "content": "HTML
",
- "type": "html"
- }
- ],
- [
- {
- "label": "Create a text field",
- "type": "textfield",
- "key": "textfield"
- }
- ],
- [
- {
- "label": "Create a number field",
- "type": "number",
- "key": "number"
- }
- ],
- [
- {
- "label": "Create a check box",
- "type": "checkbox",
- "key": "checkbox"
- }
- ],
- [
- {
- "label": "Create a check list",
- "values": [
- {
- "label": "Option 1",
- "value": "option_1"
- },
- {
- "label": "Option 2",
- "value": "option_2"
- }
- ],
- "type": "checklist",
- "key": "checklist"
- }
- ],
- [
- {
- "label": "Create a tag list",
- "values": [
- {
- "label": "Option 1",
- "value": "option_1"
- },
- {
- "label": "Option 2",
- "value": "option_2"
- }
- ],
- "type": "taglist",
- "key": "taglist"
- }
- ],
- [
- {
- "label": "Create a radio button",
- "values": [
- {
- "label": "Option 1",
- "value": "option_1"
- },
- {
- "label": "Option 2",
- "value": "option_2"
- }
- ],
- "type": "radio",
- "key": "radio"
- }
- ],
- [
- {
- "label": "Create a select",
- "values": [
- {
- "label": "Option 1",
- "value": "option_1"
- },
- {
- "label": "Option 2",
- "value": "option_2"
- }
- ],
- "type": "select",
- "key": "select"
- }
- ],
- [
- {
- "alt": "Create an image",
- "type": "image"
- }
- ],
- [
- {
- "label": "Create a text area",
- "type": "textarea",
- "key": "textarea"
- }
- ],
- [
- {
- "dateLabel": "Create a date time picker",
- "subtype": "date",
- "type": "datetime",
- "key": "date"
- }
- ],
- [
- {
- "type": "spacer",
- "height": 60
- }
- ],
- [
- {
- "label": "Create a button",
- "type": "button",
- "action": "submit"
- }
- ],
- [
- {
- "label": "Create a group",
- "type": "group",
- "components": []
- }
- ],
- [
- {
- "label": "Create a dynamic list",
- "type": "dymamiclist",
- "components": []
- }
- ],
- [
- {
- "label": "Create an iframe",
- "type": "iframe",
- "url": "https://bpmn.io"
- }
- ],
- [
- {
- "label": "Create a dynamic table",
- "type": "table",
- "rowCount": 10
- }
- ]
- ]
+{
+ "examples": [
+ [
+ {
+ "text": "Create a text",
+ "type": "text"
+ }
+ ],
+ [
+ {
+ "type": "expression",
+ "expression": "=1 + 1",
+ "computeOn": "change",
+ "key": "expression"
+ }
+ ],
+ [
+ {
+ "content": "HTML
",
+ "type": "html"
+ }
+ ],
+ [
+ {
+ "label": "Create a text field",
+ "type": "textfield",
+ "key": "textfield"
+ }
+ ],
+ [
+ {
+ "label": "Create a number field",
+ "type": "number",
+ "key": "number"
+ }
+ ],
+ [
+ {
+ "label": "Create a check box",
+ "type": "checkbox",
+ "key": "checkbox"
+ }
+ ],
+ [
+ {
+ "label": "Create a check list",
+ "values": [
+ {
+ "label": "Option 1",
+ "value": "option_1"
+ },
+ {
+ "label": "Option 2",
+ "value": "option_2"
+ }
+ ],
+ "type": "checklist",
+ "key": "checklist"
+ }
+ ],
+ [
+ {
+ "label": "Create a tag list",
+ "values": [
+ {
+ "label": "Option 1",
+ "value": "option_1"
+ },
+ {
+ "label": "Option 2",
+ "value": "option_2"
+ }
+ ],
+ "type": "taglist",
+ "key": "taglist"
+ }
+ ],
+ [
+ {
+ "label": "Create a radio button",
+ "values": [
+ {
+ "label": "Option 1",
+ "value": "option_1"
+ },
+ {
+ "label": "Option 2",
+ "value": "option_2"
+ }
+ ],
+ "type": "radio",
+ "key": "radio"
+ }
+ ],
+ [
+ {
+ "label": "Create a select",
+ "values": [
+ {
+ "label": "Option 1",
+ "value": "option_1"
+ },
+ {
+ "label": "Option 2",
+ "value": "option_2"
+ }
+ ],
+ "type": "select",
+ "key": "select"
+ }
+ ],
+ [
+ {
+ "alt": "Create an image",
+ "type": "image"
+ }
+ ],
+ [
+ {
+ "label": "Create a text area",
+ "type": "textarea",
+ "key": "textarea"
+ }
+ ],
+ [
+ {
+ "dateLabel": "Create a date time picker",
+ "subtype": "date",
+ "type": "datetime",
+ "key": "date"
+ }
+ ],
+ [
+ {
+ "type": "spacer",
+ "height": 60
+ }
+ ],
+ [
+ {
+ "label": "Create a button",
+ "type": "button",
+ "action": "submit"
+ }
+ ],
+ [
+ {
+ "label": "Create a group",
+ "type": "group",
+ "components": []
+ }
+ ],
+ [
+ {
+ "label": "Create a dynamic list",
+ "type": "dymamiclist",
+ "components": []
+ }
+ ],
+ [
+ {
+ "label": "Create an iframe",
+ "type": "iframe",
+ "url": "https://bpmn.io"
+ }
+ ],
+ [
+ {
+ "label": "Create a dynamic table",
+ "type": "table",
+ "rowCount": 10
+ }
+ ]
+ ]
}
\ No newline at end of file
diff --git a/packages/form-json-schema/src/defs/field-types/inputs.json b/packages/form-json-schema/src/defs/field-types/inputs.json
index 7353e5682..75148ef7d 100644
--- a/packages/form-json-schema/src/defs/field-types/inputs.json
+++ b/packages/form-json-schema/src/defs/field-types/inputs.json
@@ -1,20 +1,21 @@
-{
- "properties": {
- "type": {
- "enum": [
- "checkbox",
- "checklist",
- "datetime",
- "number",
- "radio",
- "select",
- "taglist",
- "textfield",
- "textarea"
- ]
- }
- },
- "required": [
- "type"
- ]
+{
+ "properties": {
+ "type": {
+ "enum": [
+ "checkbox",
+ "checklist",
+ "datetime",
+ "number",
+ "radio",
+ "select",
+ "taglist",
+ "textfield",
+ "textarea",
+ "expression"
+ ]
+ }
+ },
+ "required": [
+ "type"
+ ]
}
\ No newline at end of file
diff --git a/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json b/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json
index 1145c6e7b..8d370aaa1 100644
--- a/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json
+++ b/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json
@@ -1,442 +1,442 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "allOf": [
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "text"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "text": false
- }
- }
- },
- {
- "if": {
- "not": {
- "anyOf": [
- {
- "$ref": "../field-types/inputs.json"
- },
- {
- "properties": {
- "type": {
- "enum":[
- "button",
- "iframe"
- ]
- }
- },
- "required": [
- "type"
- ]
- },
- {
- "$ref": "../field-types/containers.json"
- },
- {
- "$ref": "../field-types/presentation-components.json"
- }
- ]
- }
- },
- "then": {
- "properties": {
- "label": false
- }
- }
- },
- {
- "if": {
- "not": {
- "$ref": "../field-types/inputs.json"
- }
- },
- "then": {
- "properties": {
- "description": false,
- "disabled": false,
- "readonly": false,
- "validate": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "button"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "action": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "image"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "source": false,
- "alt": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "datetime"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "subtype": false,
- "dateLabel": false,
- "timeLabel": false,
- "use24h": false,
- "timeSerializingFormat": false,
- "timeInterval": false,
- "disallowPassedDates": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "number"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "increment": false,
- "decimalDigits": false,
- "serializeToString": false,
- "validate": {
- "properties": {
- "min": false,
- "max": false
- }
- }
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "select"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "searchable": false
- }
- }
- },
- {
- "if": {
- "not": {
- "$ref": "../field-types/multi-inputs.json"
- }
- },
- "then": {
- "properties": {
- "values": false,
- "valuesKey": false,
- "valuesExpression": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "textfield"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "validate": {
- "properties": {
- "validationType": false,
- "pattern": false
- }
- }
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "enum": [
- "textfield",
- "textarea"
- ]
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "validate": {
- "properties": {
- "minLength": false,
- "maxLength": false
- }
- }
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "enum": [
- "textfield",
- "number"
- ]
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "appearance": {
- "properties": {
- "prefixAdorner": false,
- "suffixAdorner": false
- }
- }
- }
- }
- },
- {
- "if": {
- "anyOf": [
- {
- "not": {
- "$ref": "../field-types/inputs.json"
- }
- },
- {
- "allOf": [
- {
- "$ref": "../field-types/multi-inputs.json"
- },
- {
- "properties": {
- "valuesKey": {
- "type": "string"
- }
- },
- "required": [
- "valuesKey"
- ]
- }
- ]
- }
- ]
- },
- "then": {
- "properties": {
- "defaultValue": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "enum": [
- "spacer",
- "iframe"
- ]
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "height": false
- }
- }
- },
- {
- "if": {
- "not": {
- "$ref": "../field-types/containers.json"
- }
- },
- "then": {
- "properties": {
- "path": false,
- "showOutline": false,
- "verticalAlignment": false,
- "components": false
- }
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "dynamiclist"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "isRepeating": false,
- "defaultRepetitions": false,
- "allowAddRemove": false,
- "disableCollapse": false,
- "nonCollapsedItems": false
- }
- }
- },
- {
- "if": {
- "not": {
- "$ref": "../field-types/inputs.json"
- }
- },
- "then": {
- "properties": {
- "key": false
- }
- }
- },
- {
- "if": {
- "not": {
- "$ref": "../field-types/presentation-components.json"
- }
- },
- "then": {
- "properties": {
- "columns": false,
- "columnsExpression": false,
- "rowCount": false,
- "dataSource": false
- }
- }
- },
- {
- "if": {
- "$ref": "../field-types/presentation-components.json"
- },
- "then": {
- "oneOf": [
- {
- "properties": {
- "columns": true,
- "rowCount": true,
- "columnsExpression": false,
- "dataSource": true
- },
- "errorMessage": "Invalid combination of properties. 'columns' and 'columnsExpression' should not exist together."
- },
- {
- "properties": {
- "columns": false,
- "rowCount": true,
- "columnsExpression": true,
- "dataSource": true
- },
- "errorMessage": "Invalid combination of properties. 'columns' and 'columnsExpression' not should exist together."
- }
- ]
- }
- },
- {
- "if": {
- "not": {
- "properties": {
- "type": {
- "const": "html"
- }
- },
- "required": [
- "type"
- ]
- }
- },
- "then": {
- "properties": {
- "content": false
- }
- }
- }
- ]
-}
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "allOf": [
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "text"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "text": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "anyOf": [
+ {
+ "$ref": "../field-types/inputs.json"
+ },
+ {
+ "properties": {
+ "type": {
+ "enum":[
+ "button",
+ "iframe"
+ ]
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "$ref": "../field-types/containers.json"
+ },
+ {
+ "$ref": "../field-types/presentation-components.json"
+ }
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "label": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "$ref": "../field-types/inputs.json"
+ }
+ },
+ "then": {
+ "properties": {
+ "description": false,
+ "disabled": false,
+ "readonly": false,
+ "validate": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "button"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "action": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "image"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "source": false,
+ "alt": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "datetime"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "subtype": false,
+ "dateLabel": false,
+ "timeLabel": false,
+ "use24h": false,
+ "timeSerializingFormat": false,
+ "timeInterval": false,
+ "disallowPassedDates": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "number"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "increment": false,
+ "decimalDigits": false,
+ "serializeToString": false,
+ "validate": {
+ "properties": {
+ "min": false,
+ "max": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "select"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "searchable": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "$ref": "../field-types/multi-inputs.json"
+ }
+ },
+ "then": {
+ "properties": {
+ "values": false,
+ "valuesKey": false,
+ "valuesExpression": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "textfield"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "validate": {
+ "properties": {
+ "validationType": false,
+ "pattern": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "enum": [
+ "textfield",
+ "textarea"
+ ]
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "validate": {
+ "properties": {
+ "minLength": false,
+ "maxLength": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "enum": [
+ "textfield",
+ "number"
+ ]
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "appearance": {
+ "properties": {
+ "prefixAdorner": false,
+ "suffixAdorner": false
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "anyOf": [
+ {
+ "not": {
+ "$ref": "../field-types/inputs.json"
+ }
+ },
+ {
+ "allOf": [
+ {
+ "$ref": "../field-types/multi-inputs.json"
+ },
+ {
+ "properties": {
+ "valuesKey": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "valuesKey"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "then": {
+ "properties": {
+ "defaultValue": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "enum": [
+ "spacer",
+ "iframe"
+ ]
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "height": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "$ref": "../field-types/containers.json"
+ }
+ },
+ "then": {
+ "properties": {
+ "path": false,
+ "showOutline": false,
+ "verticalAlignment": false,
+ "components": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "dynamiclist"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "isRepeating": false,
+ "defaultRepetitions": false,
+ "allowAddRemove": false,
+ "disableCollapse": false,
+ "nonCollapsedItems": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "$ref": "../field-types/inputs.json"
+ }
+ },
+ "then": {
+ "properties": {
+ "key": false
+ }
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "$ref": "../field-types/presentation-components.json"
+ }
+ },
+ "then": {
+ "properties": {
+ "columns": false,
+ "columnsExpression": false,
+ "rowCount": false,
+ "dataSource": false
+ }
+ }
+ },
+ {
+ "if": {
+ "$ref": "../field-types/presentation-components.json"
+ },
+ "then": {
+ "oneOf": [
+ {
+ "properties": {
+ "columns": true,
+ "rowCount": true,
+ "columnsExpression": false,
+ "dataSource": true
+ },
+ "errorMessage": "Invalid combination of properties. 'columns' and 'columnsExpression' should not exist together."
+ },
+ {
+ "properties": {
+ "columns": false,
+ "rowCount": true,
+ "columnsExpression": true,
+ "dataSource": true
+ },
+ "errorMessage": "Invalid combination of properties. 'columns' and 'columnsExpression' not should exist together."
+ }
+ ]
+ }
+ },
+ {
+ "if": {
+ "not": {
+ "properties": {
+ "type": {
+ "const": "html"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ },
+ "then": {
+ "properties": {
+ "content": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/form-json-schema/src/defs/rules/rules-required-properties.json b/packages/form-json-schema/src/defs/rules/rules-required-properties.json
index 2f17ced87..08f2fd7e8 100644
--- a/packages/form-json-schema/src/defs/rules/rules-required-properties.json
+++ b/packages/form-json-schema/src/defs/rules/rules-required-properties.json
@@ -1,36 +1,54 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "allOf": [
- {
- "if": {
- "$ref": "../field-types/inputs.json"
- },
- "then": {
- "required": [
- "key"
- ]
- }
- },
- {
- "if": {
- "$ref": "../field-types/presentation-components.json"
- },
- "then": {
- "oneOf": [
- {
- "required": [
- "dataSource",
- "columns"
- ]
- },
- {
- "required": [
- "dataSource",
- "columnsExpression"
- ]
- }
- ]
- }
- }
- ]
-}
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "allOf": [
+ {
+ "if": {
+ "$ref": "../field-types/inputs.json"
+ },
+ "then": {
+ "required": [
+ "key"
+ ]
+ }
+ },
+ {
+ "if": {
+ "$ref": "../field-types/presentation-components.json"
+ },
+ "then": {
+ "oneOf": [
+ {
+ "required": [
+ "dataSource",
+ "columns"
+ ]
+ },
+ {
+ "required": [
+ "dataSource",
+ "columnsExpression"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "expression"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "required": [
+ "expression",
+ "computeOn"
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/form-json-schema/src/defs/type.json b/packages/form-json-schema/src/defs/type.json
index 6fe1fd172..edd87a18b 100644
--- a/packages/form-json-schema/src/defs/type.json
+++ b/packages/form-json-schema/src/defs/type.json
@@ -1,26 +1,27 @@
-{
- "$schema": "http://json-schema.org/draft-07/schema",
- "$id": "#/component/type",
- "description": "The type of a form field.",
- "enum": [
- "textfield",
- "number",
- "datetime",
- "textarea",
- "checkbox",
- "radio",
- "select",
- "checklist",
- "taglist",
- "image",
- "text",
- "html",
- "button",
- "spacer",
- "group",
- "dynamiclist",
- "separator",
- "table",
- "iframe"
- ]
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "#/component/type",
+ "description": "The type of a form field.",
+ "enum": [
+ "textfield",
+ "number",
+ "datetime",
+ "textarea",
+ "checkbox",
+ "radio",
+ "select",
+ "checklist",
+ "taglist",
+ "image",
+ "text",
+ "html",
+ "button",
+ "spacer",
+ "group",
+ "dynamiclist",
+ "separator",
+ "table",
+ "iframe",
+ "expression"
+ ]
}
\ No newline at end of file
diff --git a/packages/form-json-schema/test/fixtures/expression-field-expression-required.js b/packages/form-json-schema/test/fixtures/expression-field-expression-required.js
new file mode 100644
index 000000000..3288bff72
--- /dev/null
+++ b/packages/form-json-schema/test/fixtures/expression-field-expression-required.js
@@ -0,0 +1,39 @@
+export const form = {
+ type: 'default',
+ 'components': [
+ {
+ type: 'expression',
+ key: 'expressionField'
+ }
+ ]
+};
+
+export const errors = [
+ {
+ instancePath: '/components/0',
+ schemaPath: '#/properties/components/items/allOf/0/allOf/2/then/required',
+ keyword: 'required',
+ params: {
+ missingProperty: 'expression'
+ },
+ message: "must have required property 'expression'"
+ },
+ {
+ instancePath: '/components/0',
+ schemaPath: '#/properties/components/items/allOf/0/allOf/2/then/required',
+ keyword: 'required',
+ params: {
+ missingProperty: 'computeOn'
+ },
+ message: "must have required property 'computeOn'"
+ },
+ {
+ instancePath: '/components/0',
+ keyword: 'if',
+ message: 'must match "then" schema',
+ params: {
+ 'failingKeyword': 'then'
+ },
+ schemaPath: '#/properties/components/items/allOf/0/allOf/2/if'
+ }
+];
diff --git a/packages/form-json-schema/test/spec/validation.spec.js b/packages/form-json-schema/test/spec/validation.spec.js
index 7a5244f85..58d585907 100644
--- a/packages/form-json-schema/test/spec/validation.spec.js
+++ b/packages/form-json-schema/test/spec/validation.spec.js
@@ -136,6 +136,9 @@ describe('validation', function() {
testForm('disabled-not-allowed');
+ testForm('expression-field-expression-required');
+
+
testForm('action-not-allowed');