From cddea211cb955c678a8c5354d550b0d874fbecac Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Thu, 29 Feb 2024 07:57:59 +0100 Subject: [PATCH 1/4] chore: expression field schema changes Related to #1073 --- .../form-js-playground/test/spec/form.json | 6 + packages/form-js-viewer/src/index.js | 2 +- packages/form-js/test/spec/Form.spec.js | 2 +- .../src/defs/examples/components.json | 340 +++---- .../src/defs/field-types/inputs.json | 39 +- .../defs/rules/rules-allowed-properties.json | 884 +++++++++--------- .../defs/rules/rules-required-properties.json | 90 +- packages/form-json-schema/src/defs/type.json | 51 +- .../expression-field-expression-required.js | 39 + .../test/spec/validation.spec.js | 3 + 10 files changed, 766 insertions(+), 690 deletions(-) create mode 100644 packages/form-json-schema/test/fixtures/expression-field-expression-required.js diff --git a/packages/form-js-playground/test/spec/form.json b/packages/form-js-playground/test/spec/form.json index f0de27d51..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__.", 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/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-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'); From 80faadad22c8afc8a5926086d551d47f70156c99 Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Thu, 29 Feb 2024 08:45:08 +0100 Subject: [PATCH 2/4] chore: playground root refactor / prevent double form load Closes #1073 Related to #1076 --- packages/form-js-playground/src/Playground.js | 72 +++--- .../src/components/PlaygroundRoot.js | 212 +++++++++--------- .../form-js/test/spec/FormPlayground.spec.js | 17 +- 3 files changed, 142 insertions(+), 159 deletions(-) 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/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 From b8d3a0e0f4ad6a5ada4a3db2ed305f7f7579749a Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Mon, 4 Mar 2024 03:32:35 +0100 Subject: [PATCH 3/4] fix: ensure palette renderer is immediately initialised Related to #1073 --- packages/form-js-editor/src/features/palette/index.js | 1 + 1 file changed, 1 insertion(+) 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 ] }; From ae6d1d5f26f5a4f03e69e525de05515feb9c05cb Mon Sep 17 00:00:00 2001 From: Valentin Serra Date: Mon, 4 Mar 2024 03:43:35 +0100 Subject: [PATCH 4/4] chore: remove unreliable tests Related to #1073 --- .../features/palette/PaletteModule.spec.js | 39 ------------------- .../PropertiesPanelModule.spec.js | 39 ------------------- 2 files changed, 78 deletions(-) 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/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