From a34f57bd22c04ca6664fd892685cdefb7b3f5607 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 22 Jul 2025 12:43:49 -0400 Subject: [PATCH 01/10] Add Monaco Editor integration to Storybook --- .storybook/main.js | 24 ++++++++++++++++++++++++ .storybook/preview.js | 6 ++++++ 2 files changed, 30 insertions(+) diff --git a/.storybook/main.js b/.storybook/main.js index 16a711f67..702d3b6a8 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,4 +1,7 @@ /** @type { import('@storybook/vue-vite').StorybookConfig } */ +import monacoEditorPlugin from "vite-plugin-monaco-editor"; +const monacoLanguages = ["editorWorkerService", "typescript", "css", "json"]; + const config = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ @@ -13,6 +16,27 @@ const config = { }, docs: { autodocs: "tag" + }, + viteFinal: async (config) => { + // Configure Monaco Editor for Storybook + config.define = { + ...config.define, + 'process.env.NODE_DEBUG': false, + }; + + // Ensure proper worker configuration for Monaco Editor + config.worker = { + ...config.worker, + format: 'es', + }; + + // Add Monaco Editor plugin with language workers + config.plugins = [ + ...(config.plugins || []), + monacoEditorPlugin({ languageWorkers: monacoLanguages }), + ]; + + return config; } }; export default config; diff --git a/.storybook/preview.js b/.storybook/preview.js index f26ee0a2f..106e643a7 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,10 @@ /** @type { import('@storybook/vue').Preview } */ + +import Vue from "vue"; +import MonacoEditor from "vue-monaco"; + +Vue.component("MonacoEditor", MonacoEditor); + const preview = { parameters: { actions: { argTypesRegex: "^on[A-Z].*" }, From f133e935cfbe0f101e6fe6528788f87bc3e7cbe9 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 22 Jul 2025 12:45:14 -0400 Subject: [PATCH 02/10] Test button click handler --- tests/e2e/fixtures/button_click_handler.json | 1821 ++++++++++++++++++ tests/e2e/specs/ButtonClickHandler.spec.js | 108 ++ 2 files changed, 1929 insertions(+) create mode 100644 tests/e2e/fixtures/button_click_handler.json create mode 100644 tests/e2e/specs/ButtonClickHandler.spec.js diff --git a/tests/e2e/fixtures/button_click_handler.json b/tests/e2e/fixtures/button_click_handler.json new file mode 100644 index 000000000..504cac5a5 --- /dev/null +++ b/tests/e2e/fixtures/button_click_handler.json @@ -0,0 +1,1821 @@ +{ + "type": "screen_package", + "version": "2", + "screens": [ + { + "id": 1, + "screen_category_id": "1", + "title": "ClickHandler", + "description": "ClickHandler", + "type": "FORM", + "config": [ + { + "name": "Default", + "items": [ + { + "uuid": "57508483-a641-4685-8d10-8fa56cf05b07", + "config": { + "name": "form_record_list_1", + "icon": "fas fa-th-list", + "label": "New Record List", + "editable": true, + "fields": { + "dataSource": "provideData", + "jsonData": "[{\"content\":\"form_input_1\",\"value\":\"form_input_1\"}]", + "optionsList": [ + { + "content": "form_input_1", + "value": "form_input_1" + } + ], + "showOptionCard": false, + "showRemoveWarning": false, + "showJsonEditor": false, + "editIndex": null, + "removeIndex": null + }, + "form": "1", + "source": { + "collectionFields": [], + "collectionFieldsColumns": [], + "pmql": null, + "sourceOptions": "Variable", + "variableStore": null, + "dataSelectionOptions": "no-selection", + "singleField": null + } + }, + "inspector": [ + { + "type": "FormInput", + "field": "name", + "config": { + "label": "Variable Name", + "name": "Variable Name", + "validation": "dot_notation|required|not_in:null,break,case,catch,continue,debugger,default,delete,do,else,finally,for,function,if,in,instanceof,new,return,switch,this,throw,try,typeof,var,void,while,with,class,const,enum,export,extends,import,super,true,false", + "helper": "A variable name is a symbolic name to reference information." + } + }, + { + "type": "FormInput", + "field": "label", + "config": { + "label": "List Label", + "helper": "The label describes this record list" + } + }, + { + "type": "collectionDataSource", + "field": "source", + "config": { + "label": "Source of Record List", + "helper": "A record list can display the data of a defined variable or a collection" + } + }, + { + "type": "FormCheckbox", + "field": "editable", + "config": { + "label": "Editable?", + "helper": "Should records be editable/removable and can new records be added" + }, + "if": "hideControl" + }, + { + "type": "ColumnSetup", + "field": "fields", + "config": { + "label": "Columns", + "helper": "List of columns to display in the record list" + } + }, + { + "type": "FormMultiselect", + "field": "paginationOption", + "config": { + "icon": "fas", + "label": "Pagination", + "options": [ + { + "content": "No Pagination (show all)", + "value": 0 + }, + { + "content": "5 items per page", + "value": 5 + }, + { + "content": "10 items per page", + "value": 10 + }, + { + "content": "15 items per page", + "value": 15 + }, + { + "content": "25 items per page", + "value": 25 + }, + { + "content": "50 items per page", + "value": 50 + } + ], + "helper": "" + } + }, + { + "type": "PageSelect", + "field": "form", + "config": { + "label": "Record Form", + "helper": "The form to use for adding/editing records" + }, + "if": "hideControl" + }, + { + "type": "collectionDesignerMode", + "field": "designerMode", + "config": { + "label": "Table Style", + "helper": "" + } + }, + { + "type": "ColorSelectRecord", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [ + { + "value": "text-primary", + "content": "primary" + }, + { + "value": "text-secondary", + "content": "secondary" + }, + { + "value": "text-success", + "content": "success" + }, + { + "value": "text-danger", + "content": "danger" + }, + { + "value": "text-warning", + "content": "warning" + }, + { + "value": "text-info", + "content": "info" + }, + { + "value": "text-light", + "content": "light" + }, + { + "value": "text-dark", + "content": "dark" + } + ] + } + }, + { + "type": "ColorSelectRecord", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [ + { + "value": "alert alert-primary", + "content": "primary" + }, + { + "value": "alert alert-secondary", + "content": "secondary" + }, + { + "value": "alert alert-success", + "content": "success" + }, + { + "value": "alert alert-danger", + "content": "danger" + }, + { + "value": "alert alert-warning", + "content": "warning" + }, + { + "value": "alert alert-info", + "content": "info" + }, + { + "value": "alert alert-light", + "content": "light" + }, + { + "value": "alert alert-dark", + "content": "dark" + } + ] + } + }, + { + "type": "ColorSelectModern", + "field": "bgcolormodern", + "config": { + "label": "", + "helper": "", + "options": [ + { + "value": "alert alert-primary", + "content": "primary" + }, + { + "value": "alert alert-success", + "content": "success" + }, + { + "value": "alert alert-warning", + "content": "warning" + }, + { + "value": "alert alert-secondary", + "content": "secondary" + } + ] + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "DeviceVisibility", + "field": "deviceVisibility", + "config": { + "label": "Device Visibility", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##/##/####
SSN ###-##-####
Phone (###) ###-####", + "validation": "" + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }, + { + "type": "FormInput", + "field": "ariaLabel", + "config": { + "label": "Aria Label", + "helper": "Attribute designed to help assistive technology (e.g. screen readers) attach a label" + } + }, + { + "type": "FormInput", + "field": "tabindex", + "config": { + "label": "Tab Order", + "helper": "Order in which a user will move focus from one control to another by pressing the Tab key", + "validation": "regex: [0-9]*" + } + }, + { + "type": "EncryptedConfig", + "field": "encryptedConfig", + "config": { + "label": "Encrypted", + "helper": "" + } + } + ], + "component": "FormRecordList", + "editor-component": "FormText", + "editor-control": "FormRecordList", + "label": "Record List" + }, + { + "uuid": "0835489e-799c-452b-b71b-0d1de323695b", + "config": { + "name": "loop_1", + "icon": "fas fa-redo", + "settings": { + "type": "new", + "varname": "loop_1", + "times": "3", + "add": true + }, + "label": "" + }, + "inspector": [ + { + "type": "LoopInspector", + "field": "settings", + "config": { + "label": "", + "helper": "" + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "DeviceVisibility", + "field": "deviceVisibility", + "config": { + "label": "Device Visibility", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##/##/####
SSN ###-##-####
Phone (###) ###-####", + "validation": "" + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }, + { + "type": "FormInput", + "field": "ariaLabel", + "config": { + "label": "Aria Label", + "helper": "Attribute designed to help assistive technology (e.g. screen readers) attach a label" + } + }, + { + "type": "FormInput", + "field": "tabindex", + "config": { + "label": "Tab Order", + "helper": "Order in which a user will move focus from one control to another by pressing the Tab key", + "validation": "regex: [0-9]*" + } + }, + { + "type": "EncryptedConfig", + "field": "encryptedConfig", + "config": { + "label": "Encrypted", + "helper": "" + } + } + ], + "component": "FormLoop", + "editor-component": "Loop", + "editor-control": "Loop", + "label": "Loop", + "items": [ + { + "uuid": "34ec8f3c-26fc-456b-9f9a-7d3c154246b2", + "config": { + "icon": "far fa-square", + "label": "New Input", + "name": "form_input_2", + "placeholder": "", + "validation": [], + "helper": null, + "type": "text", + "dataFormat": "string", + "readonly": false + }, + "inspector": [ + { + "type": "FormInput", + "field": "name", + "config": { + "label": "Variable Name", + "name": "Variable Name", + "validation": "dot_notation|required|not_in:null,break,case,catch,continue,debugger,default,delete,do,else,finally,for,function,if,in,instanceof,new,return,switch,this,throw,try,typeof,var,void,while,with,class,const,enum,export,extends,import,super,true,false", + "helper": "A variable name is a symbolic name to reference information." + } + }, + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, + { + "type": "FormMultiselect", + "field": "dataFormat", + "config": { + "label": "Data Type", + "name": "Data Type", + "helper": "The data type specifies what kind of data is stored in the variable.", + "validation": "required", + "options": [ + { + "value": "string", + "content": "Text" + }, + { + "value": "int", + "content": "Integer" + }, + { + "value": "currency", + "content": "Currency" + }, + { + "value": "percentage", + "content": "Percentage" + }, + { + "value": "float", + "content": "Decimal" + }, + { + "value": "datetime", + "content": "Datetime" + }, + { + "value": "date", + "content": "Date" + }, + { + "value": "password", + "content": "Password" + } + ] + } + }, + { + "type": "SelectDataTypeMask", + "field": "dataMask", + "config": { + "label": "Data Format", + "name": "Data Format", + "helper": "The data format for the selected type." + } + }, + { + "type": "ValidationSelect", + "field": "validation", + "config": { + "label": "Validation Rules", + "helper": "The validation rules needed for this field" + } + }, + { + "type": "FormInput", + "field": "placeholder", + "config": { + "label": "Placeholder Text", + "helper": "The placeholder is what is shown in the field when no value is provided yet" + } + }, + { + "type": "FormInput", + "field": "helper", + "config": { + "label": "Helper Text", + "helper": "Help text is meant to provide additional guidance on the field's value" + } + }, + { + "type": "FormCheckbox", + "field": "readonly", + "config": { + "label": "Read Only", + "helper": "" + } + }, + { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [ + { + "value": "text-primary", + "content": "primary" + }, + { + "value": "text-secondary", + "content": "secondary" + }, + { + "value": "text-success", + "content": "success" + }, + { + "value": "text-danger", + "content": "danger" + }, + { + "value": "text-warning", + "content": "warning" + }, + { + "value": "text-info", + "content": "info" + }, + { + "value": "text-light", + "content": "light" + }, + { + "value": "text-dark", + "content": "dark" + } + ] + } + }, + { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [ + { + "value": "alert alert-primary", + "content": "primary" + }, + { + "value": "alert alert-secondary", + "content": "secondary" + }, + { + "value": "alert alert-success", + "content": "success" + }, + { + "value": "alert alert-danger", + "content": "danger" + }, + { + "value": "alert alert-warning", + "content": "warning" + }, + { + "value": "alert alert-info", + "content": "info" + }, + { + "value": "alert alert-light", + "content": "light" + }, + { + "value": "alert alert-dark", + "content": "dark" + } + ] + } + }, + { + "type": "default-value-editor", + "field": "defaultValue", + "config": { + "label": "Default Value", + "helper": "The default value is pre populated using the existing request data. This feature will allow you to modify the value displayed on screen load if needed." + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "DeviceVisibility", + "field": "deviceVisibility", + "config": { + "label": "Device Visibility", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##/##/####
SSN ###-##-####
Phone (###) ###-####", + "validation": "" + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }, + { + "type": "FormInput", + "field": "ariaLabel", + "config": { + "label": "Aria Label", + "helper": "Attribute designed to help assistive technology (e.g. screen readers) attach a label" + } + }, + { + "type": "FormInput", + "field": "tabindex", + "config": { + "label": "Tab Order", + "helper": "Order in which a user will move focus from one control to another by pressing the Tab key", + "validation": "regex: [0-9]*" + } + }, + { + "type": "EncryptedConfig", + "field": "encryptedConfig", + "config": { + "label": "Encrypted", + "helper": "" + } + } + ], + "component": "FormInput", + "editor-component": "FormInput", + "editor-control": "FormInput", + "label": "Line Input" + }, + { + "uuid": "83e49b57-e415-42eb-a2c0-a3847baeb4fb", + "config": { + "icon": "fas fa-share-square", + "label": "Button with handler", + "variant": "primary", + "event": "script", + "loading": false, + "loadingLabel": "Loading...", + "defaultSubmit": true, + "name": "button_with_handler", + "fieldValue": null, + "tooltip": {}, + "handler": "this.form_input_2=\"value changed by handler\";" + }, + "inspector": [ + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, + { + "type": "FormInput", + "field": "name", + "config": { + "label": "Variable Name", + "name": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:/^(?:[A-Za-z])(?:[0-9A-Z_.a-z])*(? Date ##/##/####
SSN ###-##-####
Phone (###) ###-####", + "validation": "" + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }, + { + "type": "FormInput", + "field": "ariaLabel", + "config": { + "label": "Aria Label", + "helper": "Attribute designed to help assistive technology (e.g. screen readers) attach a label" + } + }, + { + "type": "FormInput", + "field": "tabindex", + "config": { + "label": "Tab Order", + "helper": "Order in which a user will move focus from one control to another by pressing the Tab key", + "validation": "regex: [0-9]*" + } + }, + { + "type": "EncryptedConfig", + "field": "encryptedConfig", + "config": { + "label": "Encrypted", + "helper": "" + } + } + ], + "component": "FormButton", + "editor-component": "FormButton", + "editor-control": "FormSubmit", + "label": "Submit Button" + } + ], + "container": true + } + ], + "order": 1 + }, + { + "name": "test", + "order": 2, + "items": [ + { + "uuid": "f539505a-65cc-40d2-8257-666bf44970cc", + "config": { + "icon": "far fa-square", + "label": "New Input", + "name": "form_input_1", + "placeholder": "", + "validation": "", + "helper": null, + "type": "text", + "dataFormat": "string" + }, + "inspector": [ + { + "type": "FormInput", + "field": "name", + "config": { + "label": "Variable Name", + "name": "Variable Name", + "validation": "dot_notation|required|not_in:null,break,case,catch,continue,debugger,default,delete,do,else,finally,for,function,if,in,instanceof,new,return,switch,this,throw,try,typeof,var,void,while,with,class,const,enum,export,extends,import,super,true,false", + "helper": "A variable name is a symbolic name to reference information." + } + }, + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, + { + "type": "FormMultiselect", + "field": "dataFormat", + "config": { + "label": "Data Type", + "name": "Data Type", + "helper": "The data type specifies what kind of data is stored in the variable.", + "validation": "required", + "options": [ + { + "value": "string", + "content": "Text" + }, + { + "value": "int", + "content": "Integer" + }, + { + "value": "currency", + "content": "Currency" + }, + { + "value": "percentage", + "content": "Percentage" + }, + { + "value": "float", + "content": "Decimal" + }, + { + "value": "datetime", + "content": "Datetime" + }, + { + "value": "date", + "content": "Date" + }, + { + "value": "password", + "content": "Password" + } + ] + } + }, + { + "type": "SelectDataTypeMask", + "field": "dataMask", + "config": { + "label": "Data Format", + "name": "Data Format", + "helper": "The data format for the selected type." + } + }, + { + "type": "ValidationSelect", + "field": "validation", + "config": { + "label": "Validation Rules", + "helper": "The validation rules needed for this field" + } + }, + { + "type": "FormInput", + "field": "placeholder", + "config": { + "label": "Placeholder Text", + "helper": "The placeholder is what is shown in the field when no value is provided yet" + } + }, + { + "type": "FormInput", + "field": "helper", + "config": { + "label": "Helper Text", + "helper": "Help text is meant to provide additional guidance on the field's value" + } + }, + { + "type": "FormCheckbox", + "field": "readonly", + "config": { + "label": "Read Only" + } + }, + { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [ + { + "value": "text-primary", + "content": "primary" + }, + { + "value": "text-secondary", + "content": "secondary" + }, + { + "value": "text-success", + "content": "success" + }, + { + "value": "text-danger", + "content": "danger" + }, + { + "value": "text-warning", + "content": "warning" + }, + { + "value": "text-info", + "content": "info" + }, + { + "value": "text-light", + "content": "light" + }, + { + "value": "text-dark", + "content": "dark" + } + ] + } + }, + { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [ + { + "value": "alert alert-primary", + "content": "primary" + }, + { + "value": "alert alert-secondary", + "content": "secondary" + }, + { + "value": "alert alert-success", + "content": "success" + }, + { + "value": "alert alert-danger", + "content": "danger" + }, + { + "value": "alert alert-warning", + "content": "warning" + }, + { + "value": "alert alert-info", + "content": "info" + }, + { + "value": "alert alert-light", + "content": "light" + }, + { + "value": "alert alert-dark", + "content": "dark" + } + ] + } + }, + { + "type": "default-value-editor", + "field": "defaultValue", + "config": { + "label": "Default Value", + "helper": "The default value is pre populated using the existing request data. This feature will allow you to modify the value displayed on screen load if needed." + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "DeviceVisibility", + "field": "deviceVisibility", + "config": { + "label": "Device Visibility", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##/##/####
SSN ###-##-####
Phone (###) ###-####", + "validation": "" + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }, + { + "type": "FormInput", + "field": "ariaLabel", + "config": { + "label": "Aria Label", + "helper": "Attribute designed to help assistive technology (e.g. screen readers) attach a label" + } + }, + { + "type": "FormInput", + "field": "tabindex", + "config": { + "label": "Tab Order", + "helper": "Order in which a user will move focus from one control to another by pressing the Tab key", + "validation": "regex: [0-9]*" + } + }, + { + "type": "EncryptedConfig", + "field": "encryptedConfig", + "config": { + "label": "Encrypted", + "helper": "" + } + } + ], + "component": "FormInput", + "editor-component": "FormInput", + "editor-control": "FormInput", + "label": "Line Input" + }, + { + "uuid": "a50d6f9b-5798-49ab-8d0e-103930cdf000", + "config": { + "icon": "fas fa-share-square", + "label": "Button with handler", + "variant": "secondary", + "event": "script", + "loading": false, + "loadingLabel": "Loading...", + "defaultSubmit": true, + "name": "record_list_button_with_handler", + "fieldValue": null, + "tooltip": {}, + "handler": "this.form_input_1=\"value changed by handler\";console.log('handler executed')" + }, + "inspector": [ + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, + { + "type": "FormInput", + "field": "name", + "config": { + "label": "Variable Name", + "name": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:/^(?:[A-Za-z])(?:[0-9A-Z_.a-z])*(? Date ##/##/####
SSN ###-##-####
Phone (###) ###-####", + "validation": "" + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }, + { + "type": "FormInput", + "field": "ariaLabel", + "config": { + "label": "Aria Label", + "helper": "Attribute designed to help assistive technology (e.g. screen readers) attach a label" + } + }, + { + "type": "FormInput", + "field": "tabindex", + "config": { + "label": "Tab Order", + "helper": "Order in which a user will move focus from one control to another by pressing the Tab key", + "validation": "regex: [0-9]*" + } + }, + { + "type": "EncryptedConfig", + "field": "encryptedConfig", + "config": { + "label": "Encrypted", + "helper": "" + } + } + ], + "component": "FormButton", + "editor-component": "FormButton", + "editor-control": "FormSubmit", + "label": "Submit Button" + } + ] + } + ], + "computed": [], + "custom_css": null, + "created_at": "2020-06-17T22:30:48+00:00", + "updated_at": "2020-08-03T21:11:40+00:00", + "status": "ACTIVE", + "key": null, + "watchers": [], + "categories": [ + { + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2020-07-01T20:06:18+00:00", + "updated_at": "2020-07-01T20:06:18+00:00", + "pivot": { + "assignable_id": 5, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScreenCategory" + } + } + ] + } + ], + "screen_categories": [], + "scripts": [] +} \ No newline at end of file diff --git a/tests/e2e/specs/ButtonClickHandler.spec.js b/tests/e2e/specs/ButtonClickHandler.spec.js new file mode 100644 index 000000000..7b20fa0b9 --- /dev/null +++ b/tests/e2e/specs/ButtonClickHandler.spec.js @@ -0,0 +1,108 @@ +describe("Button click handler", () => { + + before(() => { + cy.visit("/"); + }); + + it("Test button click handler on main screen", () => { + cy.loadFromJson("button_click_handler.json", 0); + cy.get("[data-cy=mode-preview]").click(); + // Fill the first input + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(0).clear().type("12345678"); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(0).should( + "have.value", + "12345678" + ); + // Click on the first button + cy.get("[data-cy=preview-content] [name=button_with_handler]").eq(0).click(); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(0).should( + "have.value", + "value changed by handler" + ); + // Fill the second input + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(1).clear().type("12345678"); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(1).should( + "have.value", + "12345678" + ); + // Click on the second button + cy.get("[data-cy=preview-content] [name=button_with_handler]").eq(1).click(); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(1).should( + "have.value", + "value changed by handler" + ); + // Fill the third input + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(2).clear().type("12345678"); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(2).should( + "have.value", + "12345678" + ); + // Click on the third button + cy.get("[data-cy=preview-content] [name=button_with_handler]").eq(2).click(); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(2).should( + "have.value", + "value changed by handler" + ); + // Add new row (data-cy="loop-loop_1-add") + cy.get("[data-cy=preview-content] [data-cy=loop-loop_1-add]").click(); + // Fill the fourth input + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(3).clear().type("12345678"); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(3).should( + "have.value", + "12345678" + ); + // Click on the fourth button + cy.get("[data-cy=preview-content] [name=button_with_handler]").eq(3).click(); + cy.get("[data-cy=preview-content] [name=form_input_2]").eq(3).should( + "have.value", + "value changed by handler" + ); + }); + + it("Test button click handler on record list", () => { + cy.loadFromJson("button_click_handler.json", 0); + // wait for the screen to be loaded + cy.wait(1000); + cy.get("[data-cy=mode-preview]").click(); + // Add new row in record list (data-cy="add-row") + cy.get("[data-cy=preview-content] [data-cy=add-row]").click(); + // Fill the first input + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_1]").clear().type("12345678"); + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_1]").should( + "have.value", + "12345678" + ); + // Click on the first button + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=record_list_button_with_handler]").click(); + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_1]").should( + "have.value", + "value changed by handler" + ); + // Click on OK + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] button.btn-primary").click(); + + // Edit the row + cy.get( + "[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=edit-row]" + ).eq(0).click(); + // Fill the second input + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [name=form_input_1]").clear().type("12345678"); + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [name=form_input_1]").should( + "have.value", + "12345678" + ); + // Click on the button with handler + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [name=record_list_button_with_handler]").click(); + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [name=form_input_1]").should( + "have.value", + "value changed by handler" + ); + // Click on OK + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] button.btn-primary").click(); + // Check the value is updated in the table (data-cy="table") + cy.get("[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=table] [aria-rowindex=1] [aria-colindex=1]").should( + "contain.text", + "value changed by handler" + ); + }); +}); From f8be6dbbdad69b8365c3247033838d187545055d Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 22 Jul 2025 12:46:40 -0400 Subject: [PATCH 03/10] Implement code editor for button click handler --- .../button/handler-event-property.js | 11 ++ src/components/inspector/code-editor.vue | 126 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/components/inspector/button/handler-event-property.js create mode 100644 src/components/inspector/code-editor.vue diff --git a/src/components/inspector/button/handler-event-property.js b/src/components/inspector/button/handler-event-property.js new file mode 100644 index 000000000..df4d20ec9 --- /dev/null +++ b/src/components/inspector/button/handler-event-property.js @@ -0,0 +1,11 @@ +import CodeEditor from '../code-editor.vue'; + +export const handlerEventProperty = { + type: CodeEditor, + field: 'handler', + config: { + label: 'Click Handler', + helper: 'The handler is a JavaScript function that will be executed when the button is clicked.', + dataFeature: 'i1177', + }, +}; diff --git a/src/components/inspector/code-editor.vue b/src/components/inspector/code-editor.vue new file mode 100644 index 000000000..e14612531 --- /dev/null +++ b/src/components/inspector/code-editor.vue @@ -0,0 +1,126 @@ + + + + + \ No newline at end of file From b52dcaeab02fdaf179ee659df4b6cd15a0b71998 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 22 Jul 2025 12:47:09 -0400 Subject: [PATCH 04/10] Add CodeEditor stories to Storybook --- src/stories/CodeEditor.stories.js | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/stories/CodeEditor.stories.js diff --git a/src/stories/CodeEditor.stories.js b/src/stories/CodeEditor.stories.js new file mode 100644 index 000000000..fbc160771 --- /dev/null +++ b/src/stories/CodeEditor.stories.js @@ -0,0 +1,112 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { userEvent, expect, within } from "@storybook/test"; +import "../bootstrap"; +import CodeEditor from "../components/inspector/code-editor.vue"; + +export default { + title: "Components/CodeEditor", + component: CodeEditor, + tags: ["autodocs"], + argTypes: { + value: { + control: { type: 'text' }, + description: 'The code value to display in the editor' + }, + helper: { + control: { type: 'text' }, + description: 'Helper text displayed below the editor' + }, + dataFeature: { + control: { type: 'text' }, + description: 'Data test attribute prefix for testing' + } + }, + render: (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { CodeEditor }, + template: '', + data() { + return { inputValue: args.value }; + }, + methods: { + handleInput(value) { + this.inputValue = value; + } + }, + watch: { + // Updates the value when the property changes in storybook controls + value(value) { + this.inputValue = value; + } + } + }) +}; + +/** + * Stories of the component + */ +// Preview the component with basic JavaScript code +export const Preview = { + args: { + label: "Click Handler", + helper: "Enter your JavaScript code here", + dataFeature: "code-editor", + value: "console.log('Hello, World!');\n\nfunction greet(name) {\n return `Hello, ${name}!`;\n}" + } +}; + +// Story with empty value +export const EmptyEditor = { + args: { + label: "Empty Editor", + helper: "Start typing your code...", + dataFeature: "code-editor-empty", + value: "" + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByRole('textbox'), 'console.log("Hello, World!");'); + // Check if the code is displayed + expect(canvas.getByRole('textbox')).toHaveValue('console.log("Hello, World!");'); + } +}; + +// Story with long code +export const LongCode = { + args: { + label: "Long Code Example", + helper: "This editor contains a longer piece of code", + dataFeature: "code-editor-long", + value: `// This is a longer code example +function processUserData(users) { + return users + .filter(user => user.active) + .map(user => ({ + id: user.id, + name: user.name, + email: user.email, + role: user.role, + lastLogin: user.lastLogin, + permissions: user.permissions || [] + })) + .sort((a, b) => a.name.localeCompare(b.name)) + .reduce((acc, user) => { + if (!acc[user.role]) { + acc[user.role] = []; + } + acc[user.role].push(user); + return acc; + }, {}); +} + +// Example usage +const users = [ + { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin', active: true, lastLogin: new Date() }, + { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user', active: true, lastLogin: new Date() }, + { id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'admin', active: false, lastLogin: new Date() } +]; + +const processedUsers = processUserData(users); +console.log(processedUsers);` + } +}; \ No newline at end of file From de6600f427574616b28c8b9e62c7b80b6448c174 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 22 Jul 2025 12:47:24 -0400 Subject: [PATCH 05/10] Add handler support to FormButton component and integrate it into LoadFieldComponents mixin --- src/components/renderer/form-button.vue | 15 ++++++++++++++- src/form-builder-controls.js | 3 +++ src/mixins/extensions/LoadFieldComponents.js | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/renderer/form-button.vue b/src/components/renderer/form-button.vue index c8471e581..ed71859b1 100644 --- a/src/components/renderer/form-button.vue +++ b/src/components/renderer/form-button.vue @@ -22,7 +22,7 @@ import { getValidPath } from '@/mixins'; export default { mixins: [getValidPath], - props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel'], + props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel', 'handler'], data() { return { showSpinner: false @@ -80,6 +80,8 @@ export default { const trueValue = this.fieldValue || '1'; const value = (this.value == trueValue) ? null : trueValue; this.$emit('input', value); + // Run handler after setting the value + await this.runHandler(); } if (this.event !== 'pageNavigate' && this.name) { this.setValue(this.$parent, this.name, this.fieldValue); @@ -89,6 +91,8 @@ export default { this.showSpinner = true; } this.$emit('input', this.fieldValue); + // Run handler after setting the value + await this.runHandler(); this.$nextTick(() => { this.$emit('submit', this.eventData, this.loading, this.buttonInfo); }); @@ -99,6 +103,15 @@ export default { this.$emit('page-navigate', this.eventData); } }, + async runHandler() { + if (this.handler) { + try { + await new Function(this.handler).apply(this.transientData); + } catch (error) { + console.error('❌ There is an error in the button handler', error); + } + } + } }, }; diff --git a/src/form-builder-controls.js b/src/form-builder-controls.js index fa36cd71e..0943f6c3a 100755 --- a/src/form-builder-controls.js +++ b/src/form-builder-controls.js @@ -13,6 +13,7 @@ import FormListTable from './components/renderer/form-list-table'; import FormAnalyticsChart from "./components/renderer/form-analytics-chart"; import FormCollectionRecordControl from './components/renderer/form-collection-record-control.vue'; import FormCollectionViewControl from './components/renderer/form-collection-view-control.vue'; +import { handlerEventProperty } from './components/inspector/button/handler-event-property'; import {DataTypeProperty, DataFormatProperty, DataTypeDateTimeProperty} from './VariableDataTypeProperties'; import { FormInput, @@ -720,6 +721,7 @@ export default [ name: null, fieldValue: null, tooltip: {}, + handler: '', }, inspector: [ { @@ -742,6 +744,7 @@ export default [ }, }, buttonTypeEvent, + handlerEventProperty, LoadingSubmitButtonProperty, LabelSubmitButtonProperty, tooltipProperty, diff --git a/src/mixins/extensions/LoadFieldComponents.js b/src/mixins/extensions/LoadFieldComponents.js index e49ad461d..52632aecb 100644 --- a/src/mixins/extensions/LoadFieldComponents.js +++ b/src/mixins/extensions/LoadFieldComponents.js @@ -101,6 +101,10 @@ export default { properties[":disabled"] = isCalcProp || element.config.disabled; // Events properties['@submit'] = 'submitForm'; + // Add handler event if Button + if(componentName === 'FormButton') { + properties[':handler'] = this.byRef(element.config.handler); + } }, }, mounted() { From 03fc071b3c1b7364e214e7c5ee8c9e1394adf6dd Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Tue, 22 Jul 2025 14:02:16 -0400 Subject: [PATCH 06/10] Remove non required code --- .storybook/preview.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.storybook/preview.js b/.storybook/preview.js index 106e643a7..328375f2f 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,10 +1,5 @@ /** @type { import('@storybook/vue').Preview } */ -import Vue from "vue"; -import MonacoEditor from "vue-monaco"; - -Vue.component("MonacoEditor", MonacoEditor); - const preview = { parameters: { actions: { argTypesRegex: "^on[A-Z].*" }, From 18d8741ad4c3820724eb3aa32683ae781bbb1bf0 Mon Sep 17 00:00:00 2001 From: David Callizaya Date: Wed, 23 Jul 2025 13:35:55 -0400 Subject: [PATCH 07/10] Fix issue importing MonacoEditor --- src/components/inspector/button/handler-event-property.js | 4 +--- src/components/inspector/code-editor.vue | 6 +++--- src/components/inspector/index.js | 1 + src/components/vue-form-builder.vue | 2 ++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/inspector/button/handler-event-property.js b/src/components/inspector/button/handler-event-property.js index df4d20ec9..17a4885cc 100644 --- a/src/components/inspector/button/handler-event-property.js +++ b/src/components/inspector/button/handler-event-property.js @@ -1,7 +1,5 @@ -import CodeEditor from '../code-editor.vue'; - export const handlerEventProperty = { - type: CodeEditor, + type: 'CodeEditor', field: 'handler', config: { label: 'Click Handler', diff --git a/src/components/inspector/code-editor.vue b/src/components/inspector/code-editor.vue index e14612531..ec7ca706b 100644 --- a/src/components/inspector/code-editor.vue +++ b/src/components/inspector/code-editor.vue @@ -44,9 +44,9 @@