diff --git a/.gitignore b/.gitignore
index 92a559950..6afca413a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,4 @@ yarn-error.log*
*.njsproj
*.sln
*.sw*
+storybook-static/
diff --git a/.storybook/main.js b/.storybook/main.js
index 16a711f67..319c12481 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -1,4 +1,7 @@
/** @type { import('@storybook/vue-vite').StorybookConfig } */
+const path = require('path');
+const { mergeConfig } = require('vite');
+
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
@@ -13,6 +16,15 @@ const config = {
},
docs: {
autodocs: "tag"
- }
+ },
+ viteFinal: async (config) => {
+ return mergeConfig(config, {
+ resolve: {
+ alias: {
+ 'vue-monaco': path.resolve(__dirname, '../node_modules/vue-monaco/dist/vue-monaco.js'),
+ },
+ },
+ });
+ },
};
export default config;
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..17a4885cc
--- /dev/null
+++ b/src/components/inspector/button/handler-event-property.js
@@ -0,0 +1,9 @@
+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..4ec1a66e4
--- /dev/null
+++ b/src/components/inspector/code-editor.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
{{ $t(helper) }}
+
+
+
+
+ {{ $t('Close') }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/inspector/index.js b/src/components/inspector/index.js
index 18fc3380c..3df8d995a 100644
--- a/src/components/inspector/index.js
+++ b/src/components/inspector/index.js
@@ -30,3 +30,4 @@ export { default as LoadingSubmitButton } from "./loading-submit-button.vue";
export { default as LabelSubmitButton } from "./label-submit-button.vue";
export { default as AnalyticsSelector } from "./analytics-selector.vue";
export { default as EncryptedConfig } from "./encrypted-config.vue";
+export { default as CodeEditor } from "./code-editor.vue";
diff --git a/src/components/renderer/form-button.vue b/src/components/renderer/form-button.vue
index c8471e581..aa3116351 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,21 @@ export default {
this.$emit('page-navigate', this.eventData);
}
},
+ async runHandler() {
+ if (this.handler) {
+ try {
+ const data = this.getScreenDataReference(null, (screen, name, value) => {
+ // Enable the data reference to be updated by the handler
+ screen.$set(screen.vdata, name, value);
+ });
+ await new Function(['toRaw'], this.handler).apply(data, [(item) => {
+ return item[Symbol.for('__v_raw')];
+ }]);
+ } catch (error) {
+ console.error('❌ There is an error in the button handler', error);
+ }
+ }
+ }
},
};
diff --git a/src/components/vue-form-builder.vue b/src/components/vue-form-builder.vue
index 1ec45a266..60702854f 100644
--- a/src/components/vue-form-builder.vue
+++ b/src/components/vue-form-builder.vue
@@ -562,6 +562,7 @@ import TabsBar from "./TabsBar.vue";
import Sortable from './sortable/Sortable.vue';
import ClipboardButton from './ClipboardButton.vue';
import ScreenTemplates from './ScreenTemplates.vue';
+import CodeEditor from "./inspector/code-editor.vue";
// To include another language in the Validator with variable processmaker
const globalObject = typeof window === "undefined" ? global : window;
@@ -633,6 +634,7 @@ export default {
Sortable,
ClipboardButton,
ScreenTemplates,
+ CodeEditor,
},
mixins: [HasColorProperty, testing, Clipboard],
props: {
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/DataReference.js b/src/mixins/DataReference.js
index c8749c41f..3641efaff 100644
--- a/src/mixins/DataReference.js
+++ b/src/mixins/DataReference.js
@@ -28,6 +28,7 @@ function findScreenOwner(control, lastScreenContentIfNull = false) {
* @return {object} proxy
*/
function wrapScreenData(screen, customProperties = null, setter = null) {
+ const RAW = Symbol.for('__v_raw');
const handler = {
get: (target, name) => {
if (customProperties && customProperties[name]) {
@@ -44,6 +45,9 @@ function wrapScreenData(screen, customProperties = null, setter = null) {
}
return undefined;
}
+ if (name === RAW) {
+ return screen.vdata;
+ }
// Check if vdata exists
if (screen.vdata !== undefined && screen.vdata !== null) {
return screen.vdata[name];
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() {
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
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"
+ );
+ });
+});