@processmaker/screen-builder 3.8.3 → 3.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@processmaker/screen-builder",
3
- "version": "3.8.3",
3
+ "version": "3.8.5",
4
4
  "scripts": {
5
5
  "dev": "VITE_COVERAGE=true vite",
6
6
  "build": "vite build",
@@ -0,0 +1,9 @@
1
+ export const handlerEventProperty = {
2
+ type: 'CodeEditor',
3
+ field: 'handler',
4
+ config: {
5
+ label: 'Click Handler',
6
+ helper: 'The handler is a JavaScript function that will be executed when the button is clicked.',
7
+ dataFeature: 'i1177',
8
+ },
9
+ };
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <div>
3
+ <label :for="`${dataFeature}-code-input`">{{ $t(label) }} </label>
4
+ <div class="float-right buttons">
5
+ <b-button
6
+ :title="$t('Expand')"
7
+ variant="light"
8
+ size="sm"
9
+ :data-test="`${dataFeature}-expand-button`"
10
+ @click="expand"
11
+ ><i class="fas fa-expand"></i></b-button>
12
+ </div>
13
+ <MonacoEditor
14
+ :id="`${dataFeature}-code-input`"
15
+ v-model="configValue"
16
+ :options="smallMonacoOptions"
17
+ class="editor"
18
+ language="javascript"
19
+ :data-test="`${dataFeature}-code-input`"
20
+ @change="updateValue"
21
+ />
22
+ <small v-if="helper" class="form-text text-muted mt-2">{{ $t(helper) }}</small>
23
+ <b-modal
24
+ ref="codeEditorModal"
25
+ :title="$t(label)"
26
+ size="xl"
27
+ dialog-class="modal-dialog-fullscreen"
28
+ >
29
+ <MonacoEditor
30
+ v-model="configValue"
31
+ :options="largeMonacoOptions"
32
+ class="editor large-editor"
33
+ language="javascript"
34
+ :data-test="`${dataFeature}-code-input`"
35
+ @change="updateValue"
36
+ />
37
+ <template #modal-footer="{ ok }">
38
+ <b-button @click="ok" class="btn btn-secondary text-uppercase">
39
+ {{ $t('Close') }}
40
+ </b-button>
41
+ </template>
42
+ </b-modal>
43
+ </div>
44
+ </template>
45
+
46
+ <script>
47
+ import MonacoEditor from 'vue-monaco';
48
+
49
+ export default {
50
+ name: 'CodeEditor',
51
+ props: {
52
+ value: {
53
+ type: String,
54
+ required: false,
55
+ },
56
+ label: {
57
+ type: String,
58
+ default: '',
59
+ },
60
+ helper: {
61
+ type: String,
62
+ required: false,
63
+ },
64
+ dataFeature: {
65
+ type: String,
66
+ required: true,
67
+ },
68
+ },
69
+ components: {
70
+ MonacoEditor,
71
+ },
72
+ data() {
73
+ return {
74
+ configValue: this.value || '',
75
+ smallMonacoOptions: {
76
+ lineNumbers: 'off',
77
+ lineDecorationsWidth: 0,
78
+ lineNumbersMinChars: 0,
79
+ minimap: { enabled: false },
80
+ fixedOverflowWidgets: true,
81
+ automaticLayout: true,
82
+ renderLineHighlight: 'none',
83
+ overviewRulerLanes: 0,
84
+ },
85
+ largeMonacoOptions: {
86
+ lineNumbers: 'on',
87
+ lineDecorationsWidth: 0,
88
+ minimap: { enabled: false },
89
+ automaticLayout: true,
90
+ overviewRulerLanes: 0,
91
+ },
92
+ };
93
+ },
94
+ methods: {
95
+ updateValue() {
96
+ this.$emit('input', this.configValue);
97
+ },
98
+ expand() {
99
+ this.$refs.codeEditorModal.show();
100
+ },
101
+ },
102
+ watch: {
103
+ value(newVal) {
104
+ this.configValue = newVal;
105
+ },
106
+ },
107
+ };
108
+ </script>
109
+
110
+ <style scoped>
111
+ .buttons button {
112
+ min-width: 2.2em;
113
+ margin-left: 0.5em;
114
+ margin-bottom: 0.5em;
115
+ }
116
+
117
+ .editor {
118
+ width: 100%;
119
+ height: 10em;
120
+ border: 1px solid var(--gray);
121
+ overflow: hidden;
122
+ }
123
+
124
+ .large-editor {
125
+ height: 25em;
126
+ }
127
+ </style>
@@ -30,3 +30,4 @@ export { default as LoadingSubmitButton } from "./loading-submit-button.vue";
30
30
  export { default as LabelSubmitButton } from "./label-submit-button.vue";
31
31
  export { default as AnalyticsSelector } from "./analytics-selector.vue";
32
32
  export { default as EncryptedConfig } from "./encrypted-config.vue";
33
+ export { default as CodeEditor } from "./code-editor.vue";
@@ -22,7 +22,7 @@ import { getValidPath } from '@/mixins';
22
22
 
23
23
  export default {
24
24
  mixins: [getValidPath],
25
- props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel'],
25
+ props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel', 'handler'],
26
26
  data() {
27
27
  return {
28
28
  showSpinner: false
@@ -80,6 +80,8 @@ export default {
80
80
  const trueValue = this.fieldValue || '1';
81
81
  const value = (this.value == trueValue) ? null : trueValue;
82
82
  this.$emit('input', value);
83
+ // Run handler after setting the value
84
+ await this.runHandler();
83
85
  }
84
86
  if (this.event !== 'pageNavigate' && this.name) {
85
87
  this.setValue(this.$parent, this.name, this.fieldValue);
@@ -89,6 +91,8 @@ export default {
89
91
  this.showSpinner = true;
90
92
  }
91
93
  this.$emit('input', this.fieldValue);
94
+ // Run handler after setting the value
95
+ await this.runHandler();
92
96
  this.$nextTick(() => {
93
97
  this.$emit('submit', this.eventData, this.loading, this.buttonInfo);
94
98
  });
@@ -99,6 +103,21 @@ export default {
99
103
  this.$emit('page-navigate', this.eventData);
100
104
  }
101
105
  },
106
+ async runHandler() {
107
+ if (this.handler) {
108
+ try {
109
+ const data = this.getScreenDataReference(null, (screen, name, value) => {
110
+ // Enable the data reference to be updated by the handler
111
+ screen.$set(screen.vdata, name, value);
112
+ });
113
+ await new Function(['toRaw'], this.handler).apply(data, [(item) => {
114
+ return item[Symbol.for('__v_raw')];
115
+ }]);
116
+ } catch (error) {
117
+ console.error('❌ There is an error in the button handler', error);
118
+ }
119
+ }
120
+ }
102
121
  },
103
122
  };
104
123
  </script>
@@ -469,9 +469,9 @@ export default {
469
469
  this.currentPage = this.currentPage == 0 ? 1 : this.currentPage;
470
470
  }
471
471
  },
472
- // Watch for changes in the option input field
473
- "validationData.option": {
474
- handler(newValue) {
472
+ // Watch for changes in validationData to handle any Mustache variable changes
473
+ validationData: {
474
+ handler(newValue, oldValue) {
475
475
  if (this.source?.sourceOptions === "Collection" && this.source?.collectionFields?.pmql) {
476
476
  this.onCollectionChange(
477
477
  this.source?.collectionFields?.collectionId,
@@ -479,6 +479,7 @@ export default {
479
479
  );
480
480
  }
481
481
  },
482
+ deep: true,
482
483
  immediate: false
483
484
  }
484
485
  },
@@ -614,12 +615,9 @@ export default {
614
615
  // Get data from validationData
615
616
  const data = this.validationData || {};
616
617
 
617
- // Clean up the PMQL by removing unnecessary quotes around Mustache variables
618
- let processedPmql = pmql.replace(/"{{([^}]+)}}"/g, "{{$1}}");
619
-
620
- // Process Mustache variables
621
- processedPmql = Mustache.render(processedPmql, data);
622
-
618
+ // First, process all Mustache variables (both quoted and unquoted)
619
+ let processedPmql = Mustache.render(pmql, data);
620
+
623
621
  // Check if the processed PMQL has empty values
624
622
  if (this.hasEmptyValues(processedPmql)) {
625
623
  this.collectionData = [];
@@ -627,7 +625,11 @@ export default {
627
625
  }
628
626
 
629
627
  // Add quotes around string values in PMQL if they don't have them
630
- processedPmql = processedPmql.replace(/= ([^"'\s]+)/g, '= "$1"');
628
+ // This regex now properly handles values with spaces by looking for the end of the comparison
629
+ processedPmql = processedPmql.replace(
630
+ /= ([^"'\s][^"']*[^"'\s]|[^"'\s]+)(?=\s|$)/g,
631
+ '= "$1"'
632
+ );
631
633
 
632
634
  return processedPmql;
633
635
  } catch (error) {
@@ -562,6 +562,7 @@ import TabsBar from "./TabsBar.vue";
562
562
  import Sortable from './sortable/Sortable.vue';
563
563
  import ClipboardButton from './ClipboardButton.vue';
564
564
  import ScreenTemplates from './ScreenTemplates.vue';
565
+ import CodeEditor from "./inspector/code-editor.vue";
565
566
 
566
567
  // To include another language in the Validator with variable processmaker
567
568
  const globalObject = typeof window === "undefined" ? global : window;
@@ -633,6 +634,7 @@ export default {
633
634
  Sortable,
634
635
  ClipboardButton,
635
636
  ScreenTemplates,
637
+ CodeEditor,
636
638
  },
637
639
  mixins: [HasColorProperty, testing, Clipboard],
638
640
  props: {
@@ -13,6 +13,7 @@ import FormListTable from './components/renderer/form-list-table';
13
13
  import FormAnalyticsChart from "./components/renderer/form-analytics-chart";
14
14
  import FormCollectionRecordControl from './components/renderer/form-collection-record-control.vue';
15
15
  import FormCollectionViewControl from './components/renderer/form-collection-view-control.vue';
16
+ import { handlerEventProperty } from './components/inspector/button/handler-event-property';
16
17
  import {DataTypeProperty, DataFormatProperty, DataTypeDateTimeProperty} from './VariableDataTypeProperties';
17
18
  import {
18
19
  FormInput,
@@ -720,6 +721,7 @@ export default [
720
721
  name: null,
721
722
  fieldValue: null,
722
723
  tooltip: {},
724
+ handler: '',
723
725
  },
724
726
  inspector: [
725
727
  {
@@ -742,6 +744,7 @@ export default [
742
744
  },
743
745
  },
744
746
  buttonTypeEvent,
747
+ handlerEventProperty,
745
748
  LoadingSubmitButtonProperty,
746
749
  LabelSubmitButtonProperty,
747
750
  tooltipProperty,
@@ -28,6 +28,7 @@ function findScreenOwner(control, lastScreenContentIfNull = false) {
28
28
  * @return {object} proxy
29
29
  */
30
30
  function wrapScreenData(screen, customProperties = null, setter = null) {
31
+ const RAW = Symbol.for('__v_raw');
31
32
  const handler = {
32
33
  get: (target, name) => {
33
34
  if (customProperties && customProperties[name]) {
@@ -44,6 +45,9 @@ function wrapScreenData(screen, customProperties = null, setter = null) {
44
45
  }
45
46
  return undefined;
46
47
  }
48
+ if (name === RAW) {
49
+ return screen.vdata;
50
+ }
47
51
  // Check if vdata exists
48
52
  if (screen.vdata !== undefined && screen.vdata !== null) {
49
53
  return screen.vdata[name];
@@ -101,6 +101,10 @@ export default {
101
101
  properties[":disabled"] = isCalcProp || element.config.disabled;
102
102
  // Events
103
103
  properties['@submit'] = 'submitForm';
104
+ // Add handler event if Button
105
+ if(componentName === 'FormButton') {
106
+ properties[':handler'] = this.byRef(element.config.handler);
107
+ }
104
108
  },
105
109
  },
106
110
  mounted() {
@@ -0,0 +1,112 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ import { userEvent, expect, within } from "@storybook/test";
3
+ import "../bootstrap";
4
+ import CodeEditor from "../components/inspector/code-editor.vue";
5
+
6
+ export default {
7
+ title: "Components/CodeEditor",
8
+ component: CodeEditor,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ value: {
12
+ control: { type: 'text' },
13
+ description: 'The code value to display in the editor'
14
+ },
15
+ helper: {
16
+ control: { type: 'text' },
17
+ description: 'Helper text displayed below the editor'
18
+ },
19
+ dataFeature: {
20
+ control: { type: 'text' },
21
+ description: 'Data test attribute prefix for testing'
22
+ }
23
+ },
24
+ render: (args, { argTypes }) => ({
25
+ props: Object.keys(argTypes),
26
+ components: { CodeEditor },
27
+ template: '<code-editor v-bind="$props" v-model="inputValue" @input="handleInput" />',
28
+ data() {
29
+ return { inputValue: args.value };
30
+ },
31
+ methods: {
32
+ handleInput(value) {
33
+ this.inputValue = value;
34
+ }
35
+ },
36
+ watch: {
37
+ // Updates the value when the property changes in storybook controls
38
+ value(value) {
39
+ this.inputValue = value;
40
+ }
41
+ }
42
+ })
43
+ };
44
+
45
+ /**
46
+ * Stories of the component
47
+ */
48
+ // Preview the component with basic JavaScript code
49
+ export const Preview = {
50
+ args: {
51
+ label: "Click Handler",
52
+ helper: "Enter your JavaScript code here",
53
+ dataFeature: "code-editor",
54
+ value: "console.log('Hello, World!');\n\nfunction greet(name) {\n return `Hello, ${name}!`;\n}"
55
+ }
56
+ };
57
+
58
+ // Story with empty value
59
+ export const EmptyEditor = {
60
+ args: {
61
+ label: "Empty Editor",
62
+ helper: "Start typing your code...",
63
+ dataFeature: "code-editor-empty",
64
+ value: ""
65
+ },
66
+ play: async ({ canvasElement }) => {
67
+ const canvas = within(canvasElement);
68
+ await userEvent.type(canvas.getByRole('textbox'), 'console.log("Hello, World!");');
69
+ // Check if the code is displayed
70
+ expect(canvas.getByRole('textbox')).toHaveValue('console.log("Hello, World!");');
71
+ }
72
+ };
73
+
74
+ // Story with long code
75
+ export const LongCode = {
76
+ args: {
77
+ label: "Long Code Example",
78
+ helper: "This editor contains a longer piece of code",
79
+ dataFeature: "code-editor-long",
80
+ value: `// This is a longer code example
81
+ function processUserData(users) {
82
+ return users
83
+ .filter(user => user.active)
84
+ .map(user => ({
85
+ id: user.id,
86
+ name: user.name,
87
+ email: user.email,
88
+ role: user.role,
89
+ lastLogin: user.lastLogin,
90
+ permissions: user.permissions || []
91
+ }))
92
+ .sort((a, b) => a.name.localeCompare(b.name))
93
+ .reduce((acc, user) => {
94
+ if (!acc[user.role]) {
95
+ acc[user.role] = [];
96
+ }
97
+ acc[user.role].push(user);
98
+ return acc;
99
+ }, {});
100
+ }
101
+
102
+ // Example usage
103
+ const users = [
104
+ { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin', active: true, lastLogin: new Date() },
105
+ { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user', active: true, lastLogin: new Date() },
106
+ { id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'admin', active: false, lastLogin: new Date() }
107
+ ];
108
+
109
+ const processedUsers = processUserData(users);
110
+ console.log(processedUsers);`
111
+ }
112
+ };