@processmaker/screen-builder 3.8.7 → 3.8.9

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.
@@ -0,0 +1,205 @@
1
+ <template>
2
+ <div v-if="stagesPerCase.length > 0">
3
+ <div class="pipeline-wrapper">
4
+ <div class="pipeline">
5
+ <div
6
+ v-for="(stage, index) in stagesPerCase"
7
+ :key="index"
8
+ class="stage-container"
9
+ >
10
+ <div
11
+ :class="['stage-block', getStageClass(stage.status)]"
12
+ :style="getClipPathStyle(index)"
13
+ >
14
+ <div class="stage-name">{{ stage.name }}</div>
15
+ <template v-if="validateDate(stage.completed_at)">
16
+ <div class="stage-date">{{ formatDate(stage.completed_at) }}</div>
17
+ </template>
18
+ <template v-else>
19
+ <div class="stage-status">{{ getStatusLabel(stage.status) }}</div>
20
+ </template>
21
+ </div>
22
+ <!-- Connector Arrow, not shown after last block -->
23
+ <div
24
+ v-if="index < stagesPerCase.length - 1"
25
+ :class="['arrow-connector', getStageClass(stage.status)]"
26
+ ></div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ <div v-else>
32
+ <div class="pipeline-wrapper">
33
+ <div class="pipeline">
34
+ <div class="stage-container">
35
+ <div class="stage-block pending">
36
+ <div class="stage-name">No stages available</div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <script>
45
+ import moment from "moment";
46
+ import { getUserDateFormat } from "@processmaker/vue-form-elements";
47
+
48
+ export default {
49
+ props: ["label", "event", "eventData"],
50
+ data() {
51
+ return {
52
+ caseNumber: null,
53
+ stagesPerCase: [],
54
+ stagesStatus: {
55
+ Done: "closed",
56
+ "In Progress": "active",
57
+ Pending: "pending"
58
+ }
59
+ };
60
+ },
61
+ mounted() {
62
+ this.caseNumber = window.ProcessMaker?.caseNumber;
63
+ this.getStageStatus(this.caseNumber);
64
+ },
65
+ methods: {
66
+ getStageStatus(caseNumber) {
67
+ let url_api = 'cases/stages_bar';
68
+ if (caseNumber) {
69
+ url_api += `/${caseNumber}`;
70
+ }
71
+ ProcessMaker.apiClient
72
+ .get(url_api)
73
+ .then((response) => {
74
+ this.stagesPerCase = response.data.stages_per_case;
75
+ })
76
+ .catch((error) => {
77
+ console.error("Error fetching data:", error);
78
+ });
79
+ },
80
+ validateDate(date) {
81
+ return date && date !== "null";
82
+ },
83
+ getStageClass(status) {
84
+ return this.stagesStatus[status] || "pending";
85
+ },
86
+ getStatusLabel(status) {
87
+ if (status === "Done") return "Completed";
88
+ return status || "Pending";
89
+ },
90
+ formatDate(date) {
91
+ if (!date || date === "null") return "";
92
+ return moment
93
+ .utc(date, [getUserDateFormat(), moment.ISO_8601], true)
94
+ .toISOString()
95
+ .split(RegExp("T[0-9]"))[0];
96
+ },
97
+ getClipPathStyle(index) {
98
+ if (index === 0) {
99
+ return {
100
+ clipPath: "none"
101
+ };
102
+ }
103
+ if (this.stagesPerCase.length > 1) {
104
+ return {
105
+ clipPath: "polygon(0 0, 100% 0, 100% 100%, 0px 100%, 20px 50%)"
106
+ };
107
+ }
108
+ return {
109
+ clipPath: "none"
110
+ };
111
+ }
112
+ }
113
+ };
114
+ </script>
115
+ <style scoped>
116
+ .pipeline-wrapper {
117
+ width: 100%;
118
+ overflow-x: hidden;
119
+ padding: 10px;
120
+ }
121
+ .pipeline {
122
+ display: flex;
123
+ align-items: stretch;
124
+ width: 100%;
125
+ }
126
+ .stage-container {
127
+ display: flex;
128
+ align-items: center;
129
+ flex: 1;
130
+ margin-left: -10px;
131
+ align-self: stretch;
132
+ }
133
+
134
+ .stage-block {
135
+ flex: 1;
136
+ height: 100px;
137
+ padding: 22px 8px 40px 44px;;
138
+ color: #333;
139
+ text-align: center;
140
+ position: relative;
141
+ display: flex;
142
+ flex-direction: column;
143
+ justify-content: center;
144
+ border-radius: 4px;
145
+ transition: background-color 0.3s ease;
146
+ box-sizing: border-box;
147
+ }
148
+ .stage-block.active {
149
+ background-color: #ffcc99;
150
+ }
151
+ .stage-block.closed {
152
+ background-color: #ffd9b3;
153
+ }
154
+ .stage-block.pending {
155
+ background-color: #f0f0f0;
156
+ color: #999;
157
+ }
158
+ .stage-name {
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ white-space: break-spaces;
162
+ font-size: 16px;
163
+ font-style: normal;
164
+ font-weight: 600;
165
+ line-height: 20px;
166
+ letter-spacing: -0.16px;
167
+ }
168
+ .stage-status,
169
+ .stage-date {
170
+ overflow: hidden;
171
+ text-overflow: ellipsis;
172
+ font-size: 12px;
173
+ font-style: normal;
174
+ font-weight: 500;
175
+ line-height: normal;
176
+ letter-spacing: -0.12px;
177
+ border-radius: 12px;
178
+ background: rgba(255, 255, 255, 0.75);
179
+ position: absolute;
180
+ right: 4.999px;
181
+ bottom: 12px;
182
+ display: flex;
183
+ padding: 2px 6px;
184
+ align-items: center;
185
+ gap: 10px;
186
+ }
187
+
188
+ .arrow-connector {
189
+ width: 0;
190
+ height: 0;
191
+ border-top: 50px solid transparent;
192
+ border-bottom: 50px solid transparent;
193
+ border-left: 20px solid;
194
+ flex-shrink: 0;
195
+ }
196
+ .arrow-connector.active {
197
+ color: #ffcc99;
198
+ }
199
+ .arrow-connector.closed {
200
+ color: #ffd9b3;
201
+ }
202
+ .arrow-connector.pending {
203
+ color: #f0f0f0;
204
+ }
205
+ </style>
@@ -10,7 +10,7 @@
10
10
  :disabled="showSpinner"
11
11
  >
12
12
  <b-spinner v-if="showSpinner" small></b-spinner>
13
- {{ showSpinner ? (!loadingLabel ? 'Loading...': loadingLabel) : label }}
13
+ {{ showSpinner ? (!loadingLabel ? "Loading..." : loadingLabel) : label }}
14
14
  </button>
15
15
  </div>
16
16
  </template>
@@ -19,17 +19,33 @@
19
19
  import Mustache from 'mustache';
20
20
  import { mapActions, mapState } from "vuex";
21
21
  import { getValidPath } from '@/mixins';
22
+ import Worker from "@/workers/worker.js?worker&inline";
23
+ import { findRootScreen } from "@/mixins/DataReference";
24
+ import { stringify } from 'flatted';
22
25
 
23
26
  export default {
24
27
  mixins: [getValidPath],
25
- props: ['variant', 'label', 'event', 'eventData', 'name', 'fieldValue', 'value', 'tooltip', 'transientData', 'loading', 'loadingLabel', 'handler'],
28
+ props: [
29
+ "variant",
30
+ "label",
31
+ "event",
32
+ "eventData",
33
+ "name",
34
+ "fieldValue",
35
+ "value",
36
+ "tooltip",
37
+ "transientData",
38
+ "loading",
39
+ "loadingLabel",
40
+ "handler"
41
+ ],
26
42
  data() {
27
43
  return {
28
44
  showSpinner: false
29
45
  };
30
46
  },
31
47
  computed: {
32
- ...mapState('globalErrorsModule', ['valid']),
48
+ ...mapState("globalErrorsModule", ["valid"]),
33
49
  classList() {
34
50
  let variant = this.variant || 'primary';
35
51
  return {
@@ -98,24 +114,50 @@ export default {
98
114
  });
99
115
  return;
100
116
  }
117
+ if (this.event === 'pageNavigate') {
118
+ // Run handler for page navigate
119
+ await this.runHandler();
120
+ }
101
121
  this.$emit(this.event, this.eventData);
102
122
  if (this.event === 'pageNavigate') {
103
123
  this.$emit('page-navigate', this.eventData);
104
124
  }
105
125
  },
106
- async runHandler() {
126
+ runHandler() {
107
127
  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
- }
128
+ return new Promise((resolve, reject) => {
129
+ try {
130
+ const rootScreen = findRootScreen(this);
131
+ const data = rootScreen.vdata;
132
+ const scope = this.transientData;
133
+
134
+ const worker = new Worker();
135
+ // Send the handler code to the worker
136
+ worker.postMessage({
137
+ fn: this.handler,
138
+ dataRefs: stringify({data, scope}),
139
+ });
140
+
141
+ // Listen for the result from the worker
142
+ worker.onmessage = (e) => {
143
+ if (e.data.error) {
144
+ reject(e.data.error);
145
+ } else if (e.data.result) {
146
+ // Update the data with the result
147
+ Object.keys(e.data.result).forEach(key => {
148
+ if (key === '_root') {
149
+ Object.assign(data, e.data.result[key]);
150
+ } else {
151
+ scope[key] = e.data.result[key];
152
+ }
153
+ });
154
+ resolve();
155
+ }
156
+ };
157
+ } catch (error) {
158
+ console.error("❌ There is an error in the button handler", error);
159
+ }
160
+ });
119
161
  }
120
162
  }
121
163
  },
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="itemData">
4
+ <slot></slot>
5
+ </div>
6
+ <div v-else class="dynamic-panel-empty">
7
+ <div v-html="renderedEmptyMessage" class="text-muted"></div>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script>
13
+ import Mustache from 'mustache';
14
+
15
+ export default {
16
+ name: "FormDynamicPanel",
17
+ props: {
18
+ itemData: {
19
+ type: [Object, Array, String, Number, Boolean],
20
+ required: false,
21
+ default: null
22
+ },
23
+ emptyStateMessage: {
24
+ type: String,
25
+ required: false,
26
+ default: 'No data available for this dynamic panel'
27
+ },
28
+ validationData: {
29
+ type: Object,
30
+ required: false,
31
+ default: () => ({})
32
+ }
33
+ },
34
+ computed: {
35
+ hasData() {
36
+ return this.itemData !== null && this.itemData !== undefined;
37
+ },
38
+ renderedEmptyMessage() {
39
+ if (!this.emptyStateMessage) {
40
+ return this.$t('No data available for this dynamic panel');
41
+ }
42
+
43
+ try {
44
+ // Process Mustache placeholders
45
+ const processedMessage = Mustache.render(this.emptyStateMessage, this.validationData);
46
+ return processedMessage;
47
+ } catch (error) {
48
+ // If Mustache processing fails, return the original message
49
+ return this.emptyStateMessage;
50
+ }
51
+ }
52
+ }
53
+ };
54
+ </script>
55
+
56
+ <style scoped>
57
+
58
+ .dynamic-panel-empty {
59
+ text-align: center;
60
+ padding: 16px;
61
+ background-color: #f8f9fa;
62
+ border-radius: 4px;
63
+ }
64
+
65
+ .dynamic-panel-empty p {
66
+ margin-bottom: 8px;
67
+ }
68
+ </style>
@@ -21,3 +21,5 @@ export { default as FormTasks } from "./form-tasks.vue";
21
21
  export { default as LinkButton } from "./link-button.vue";
22
22
  export { default as FormCollectionRecordControl } from "./form-collection-record-control.vue";
23
23
  export { default as FormCollectionViewControl } from "./form-collection-view-control.vue";
24
+ export { default as FormDynamicPanel } from "./form-dynamic-panel.vue";
25
+ export { default as CaseProgressBar } from "./case-progress-bar.vue";
@@ -1,6 +1,9 @@
1
1
  <template>
2
- <div class="form-group">
3
- <a :class="classColor" :href="inputUrlLink"> {{ label }} </a>
2
+ <div class="form-group link-button">
3
+ <a :class="classColor" :href="inputUrlLink">
4
+ <i v-if="variantStyle === 'button'" class="fas fa-external-link-alt" />
5
+ {{ label }}
6
+ </a>
4
7
  </div>
5
8
  </template>
6
9
 
@@ -8,6 +11,7 @@
8
11
  export default {
9
12
  props: [
10
13
  "variant",
14
+ "variantStyle",
11
15
  "label",
12
16
  "event",
13
17
  "eventData",
@@ -18,7 +22,10 @@ export default {
18
22
  ],
19
23
  computed: {
20
24
  classColor() {
21
- return `text-${this.variant}`;
25
+ if (this.variantStyle === "link") {
26
+ return `text-${this.variant}`;
27
+ }
28
+ return `btn btn-${this.variant}`;
22
29
  }
23
30
  }
24
31
  };
@@ -1521,7 +1521,8 @@ export default {
1521
1521
 
1522
1522
  if (
1523
1523
  _.findIndex(control.inspector, keyNamePropertyToFind) !== -1 ||
1524
- control.component === "FormLoop"
1524
+ control.component === "FormLoop" ||
1525
+ control.component === "FormDynamicPanel"
1525
1526
  ) {
1526
1527
  [this.variables, copy.config.name] = this.generator.generate(
1527
1528
  this.config,
@@ -1531,7 +1532,9 @@ export default {
1531
1532
  copy.config.settings.varname = copy.config.name;
1532
1533
  }
1533
1534
  }
1534
-
1535
+ if (copy.component === "FormHtmlViewer") {
1536
+ copy.config.name = "form_html_viewer";
1537
+ }
1535
1538
  return copy;
1536
1539
  },
1537
1540
  initiateLanguageSupport() {
@@ -3,6 +3,7 @@ import FormAvatar from './components/renderer/form-avatar';
3
3
  import FormButton from './components/renderer/form-button';
4
4
  import FormMultiColumn from './components/renderer/form-multi-column';
5
5
  import FormLoop from './components/renderer/form-loop';
6
+ import FormDynamicPanel from './components/renderer/form-dynamic-panel.vue';
6
7
  import FormRecordList from './components/renderer/form-record-list';
7
8
  import FormImage from './components/renderer/form-image';
8
9
  import FormMaskedInput from './components/renderer/form-masked-input';
@@ -27,6 +28,7 @@ import {
27
28
  } from '@processmaker/vue-form-elements';
28
29
  import { dataSourceValues } from '@/components/inspector/data-source-types';
29
30
  import LinkButton from "./components/renderer/link-button.vue";
31
+ import CaseProgressBar from "./components/renderer/case-progress-bar.vue";
30
32
 
31
33
  import {
32
34
  bgcolorProperty,
@@ -50,6 +52,8 @@ import {
50
52
  bgcolorModern,
51
53
  bgcolorPropertyRecord,
52
54
  colorPropertyRecord,
55
+ linkVariantStyleProperty,
56
+ variantStyleProperty
53
57
  } from './form-control-common-properties';
54
58
 
55
59
  export default [
@@ -461,6 +465,43 @@ export default [
461
465
  ],
462
466
  },
463
467
  },
468
+ {
469
+ editorComponent: FormDynamicPanel,
470
+ editorBinding: 'FormDynamicPanel',
471
+ rendererComponent: FormDynamicPanel,
472
+ rendererBinding: 'FormDynamicPanel',
473
+ control: {
474
+ popoverContent: "Add a dynamic panel component",
475
+ order: 6.0,
476
+ group: 'Content Fields',
477
+ label: 'Dynamic Panel',
478
+ component: 'FormDynamicPanel',
479
+ 'editor-component': 'DynamicPanel',
480
+ 'editor-control': 'DynamicPanel',
481
+ container: true,
482
+ // Default items container
483
+ items: [],
484
+ config: {
485
+ name: '',
486
+ icon: 'fas fa-th-large',
487
+ settings: {
488
+ type: 'new',
489
+ varname: 'dynamic_panel',
490
+ indexName: '',
491
+ add: false,
492
+ emptyStateMessage: 'No data available for this dynamic panel',
493
+ },
494
+ },
495
+ inspector: [
496
+ {
497
+ type: 'DynamicPanelInspector',
498
+ field: 'settings',
499
+ config: {
500
+ },
501
+ },
502
+ ],
503
+ },
504
+ },
464
505
  {
465
506
  editorComponent: FormText,
466
507
  editorBinding: 'FormText',
@@ -1083,7 +1124,9 @@ export default [
1083
1124
  label: "New Link",
1084
1125
  icon: "fas fa-link",
1085
1126
  variant: "primary",
1127
+ variantStyle: "link",
1086
1128
  event: "link",
1129
+ value: "link"
1087
1130
  },
1088
1131
  inspector: [
1089
1132
  {
@@ -1091,7 +1134,7 @@ export default [
1091
1134
  field: 'label',
1092
1135
  config: {
1093
1136
  label: 'Label',
1094
- helper: 'The label describes the button\'s text',
1137
+ helper: 'The label describes the link\'s text',
1095
1138
  },
1096
1139
  },
1097
1140
  {
@@ -1100,9 +1143,10 @@ export default [
1100
1143
  config: {
1101
1144
  label: 'Link URL',
1102
1145
  helper: 'Type here the URL link. Mustache syntax is supported.',
1103
- },
1146
+ },
1104
1147
  },
1105
- buttonVariantStyleProperty
1148
+ variantStyleProperty,
1149
+ linkVariantStyleProperty
1106
1150
  ]
1107
1151
  }
1108
1152
  },
@@ -1213,5 +1257,30 @@ export default [
1213
1257
  }
1214
1258
  ],
1215
1259
  },
1260
+ },
1261
+ {
1262
+ editorComponent: CaseProgressBar,
1263
+ editorBinding: 'CaseProgressBar',
1264
+ rendererComponent: CaseProgressBar,
1265
+ rendererBinding: 'CaseProgressBar',
1266
+ control: {
1267
+ popoverContent: "Add a progress bar to show the status of a case",
1268
+ order: 7.0,
1269
+ group: 'Dashboards',
1270
+ label: 'Case Progress Bar',
1271
+ component: 'CaseProgressBar',
1272
+ 'editor-component': 'CaseProgressBar',
1273
+ 'editor-control': 'CaseProgressBar',
1274
+ config: {
1275
+ label: 'New Case Progress Bar',
1276
+ icon: 'fas fa-chart-bar',
1277
+ variant: 'primary',
1278
+ event: 'submit',
1279
+ name: null,
1280
+ fieldValue: null,
1281
+ tooltip: {},
1282
+ },
1283
+ inspector: [],
1284
+ }
1216
1285
  }
1217
1286
  ];
@@ -284,53 +284,76 @@ export const toggleStyleProperty = {
284
284
  },
285
285
  };
286
286
 
287
+ const optionsColor = [
288
+ {
289
+ value: "primary",
290
+ content: "Primary"
291
+ },
292
+ {
293
+ value: "secondary",
294
+ content: "Secondary"
295
+ },
296
+ {
297
+ value: "success",
298
+ content: "Success"
299
+ },
300
+ {
301
+ value: "danger",
302
+ content: "Danger"
303
+ },
304
+ {
305
+ value: "warning",
306
+ content: "Warning"
307
+ },
308
+ {
309
+ value: "info",
310
+ content: "Info"
311
+ },
312
+ {
313
+ value: "light",
314
+ content: "Light"
315
+ },
316
+ {
317
+ value: "dark",
318
+ content: "Dark"
319
+ },
320
+ {
321
+ value: "link",
322
+ content: "Link"
323
+ }
324
+ ];
325
+
287
326
  export const buttonVariantStyleProperty = {
288
- type: 'FormMultiselect',
289
- field: 'variant',
327
+ type: "FormMultiselect",
328
+ field: "variant",
290
329
  config: {
291
- label: 'Button Variant Style',
292
- helper: 'The variant determines the appearance of the button',
293
- options: [
294
- {
295
- value: 'primary',
296
- content: 'Primary',
297
- },
298
- {
299
- value: 'secondary',
300
- content: 'Secondary',
301
- },
302
- {
303
- value: 'success',
304
- content: 'Success',
305
- },
306
- {
307
- value: 'danger',
308
- content: 'Danger',
309
- },
310
- {
311
- value: 'warning',
312
- content: 'Warning',
313
- },
314
- {
315
- value: 'info',
316
- content: 'Info',
317
- },
318
- {
319
- value: 'light',
320
- content: 'Light',
321
- },
330
+ label: "Button Variant Style",
331
+ helper: "The variant determines the appearance of the button",
332
+ options: optionsColor
333
+ }
334
+ };
322
335
 
323
- {
324
- value: 'dark',
325
- content: 'Dark',
326
- },
336
+ export const linkVariantStyleProperty = {
337
+ type: "FormMultiselect",
338
+ field: "variant",
339
+ config: {
340
+ label: "Link Variant Style",
341
+ helper: "The variant determines the appearance of the link",
342
+ options: optionsColor
343
+ }
344
+ };
327
345
 
328
- {
329
- value: 'link',
330
- content: 'Link',
331
- },
332
- ],
333
- },
346
+ export const variantStyleProperty = {
347
+ type: "FormMultiselect",
348
+ field: "variantStyle",
349
+ config: {
350
+ label: "Variant Style",
351
+ helper: "The variant determines the appearance of the link",
352
+ options: [
353
+ { value: "link", content: "Link" },
354
+ { value: "button", content: "Button" }
355
+ ]
356
+ }
334
357
  };
335
358
 
336
359
  export const defaultValueProperty = {