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');