@processmaker/screen-builder 3.0.2 → 3.0.4

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.0.2",
3
+ "version": "3.0.4",
4
4
  "scripts": {
5
5
  "dev": "VITE_COVERAGE=true vite",
6
6
  "build": "vite build",
@@ -57,7 +57,7 @@
57
57
  "@fortawesome/fontawesome-free": "^5.6.1",
58
58
  "@originjs/vite-plugin-commonjs": "^1.0.3",
59
59
  "@panter/vue-i18next": "^0.15.2",
60
- "@processmaker/vue-form-elements": "0.61.0",
60
+ "@processmaker/vue-form-elements": "0.61.2",
61
61
  "@processmaker/vue-multiselect": "2.3.0",
62
62
  "@storybook/addon-essentials": "^7.6.13",
63
63
  "@storybook/addon-interactions": "^7.6.13",
@@ -116,7 +116,7 @@
116
116
  },
117
117
  "peerDependencies": {
118
118
  "@panter/vue-i18next": "^0.15.0",
119
- "@processmaker/vue-form-elements": "0.61.0",
119
+ "@processmaker/vue-form-elements": "0.61.2",
120
120
  "i18next": "^15.0.8",
121
121
  "vue": "^2.6.12",
122
122
  "vuex": "^3.1.1"
package/src/App.vue CHANGED
@@ -114,6 +114,7 @@
114
114
  title="Default"
115
115
  :render-controls="displayBuilder"
116
116
  @change="updateConfig"
117
+ :screen-type="displayType"
117
118
  >
118
119
  <default-loading-spinner />
119
120
  </vue-form-builder>
@@ -448,6 +449,7 @@ export default {
448
449
  },
449
450
  showTemplatesPanel: false,
450
451
  sharedTemplatesData: null,
452
+ displayType: 'form'
451
453
  };
452
454
  },
453
455
  computed: {
@@ -24,9 +24,11 @@
24
24
  <hr class="card-divider" />
25
25
  <b-card-body class="p-1">
26
26
  <div class="template-details">
27
- <span class="template-name d-block pt-1">{{ template.name }}</span>
27
+ <span class="template-name d-block pt-1">{{
28
+ truncateText(template.name, 45)
29
+ }}</span>
28
30
  <span class="template-description d-block">{{
29
- truncateText(template.description, 100)
31
+ truncateText(template.description, 60)
30
32
  }}</span>
31
33
  </div>
32
34
  <b-collapse v-model="isApplyOptionsActive">
@@ -1,6 +1,11 @@
1
1
  <template>
2
2
  <div class="column-draggable" :selector="config.customCssSelector">
3
- <draggable style="min-height: 80px" :list="items" group="controls">
3
+ <draggable
4
+ style="min-height: 80px"
5
+ :list="items"
6
+ group="controls"
7
+ @change="onChange"
8
+ >
4
9
  <div
5
10
  v-for="(element, index) in items"
6
11
  :key="index"
@@ -27,8 +32,18 @@
27
32
  class="mr-2 ml-1"
28
33
  />
29
34
  {{ element.config.name || $t("Variable Name") }}
35
+ <b-badge
36
+ v-if="isInClipboard(items[index]) && screenType === 'form'"
37
+ data-cy="copied-badge"
38
+ class="m-2 custom-badge"
39
+ pill
40
+ >
41
+ <i class="far fa-check-circle"></i>
42
+ <span class="pl-2">{{ $t('Copied')}}</span>
43
+ </b-badge>
30
44
  <div class="ml-auto">
31
45
  <clipboard-button
46
+ v-if="screenType === 'form'"
32
47
  :index="index"
33
48
  :config="element.config"
34
49
  :isInClipboard="isInClipboard(items[index])"
@@ -74,6 +89,7 @@
74
89
  :config="element.config"
75
90
  @inspect="inspect"
76
91
  @update-state="$emit('update-state')"
92
+ :screen-type="screenType"
77
93
  />
78
94
  </div>
79
95
  </div>
@@ -91,8 +107,18 @@
91
107
  class="mr-2 ml-1"
92
108
  />
93
109
  {{ element.config.name || $t("Variable Name") }}
110
+ <b-badge
111
+ v-if="isInClipboard(items[index]) && screenType === 'form'"
112
+ data-cy="copied-badge"
113
+ class="m-2 custom-badge"
114
+ pill
115
+ >
116
+ <i class="far fa-check-circle"></i>
117
+ <span class="pl-2">{{ $t('Copied')}}</span>
118
+ </b-badge>
94
119
  <div class="ml-auto">
95
120
  <clipboard-button
121
+ v-if="screenType === 'form'"
96
122
  :index="index"
97
123
  :config="element.config"
98
124
  :isInClipboard="isInClipboard(items[index])"
@@ -128,6 +154,7 @@
128
154
  ]"
129
155
  :tabindex="element.config.interactive ? 0 : -1"
130
156
  :config="element.config"
157
+ :screen-type="screenType"
131
158
  @input="
132
159
  element.config.interactive
133
160
  ? (element.config.content = $event)
@@ -173,7 +200,7 @@ export default {
173
200
  ...renderer
174
201
  },
175
202
  mixins: [HasColorProperty, Clipboard],
176
- props: ["value", "name", "config", "selected", "validationErrors"],
203
+ props: ["value", "name", "config", "selected", "validationErrors", "screenType"],
177
204
  data() {
178
205
  return {
179
206
  items: [],
@@ -258,7 +285,13 @@ export default {
258
285
  this.$set(item.config.aiConfig, "progress", progress);
259
286
  }
260
287
  });
261
- }
288
+ },
289
+ /**
290
+ * Triggered when the draggable container is changed.
291
+ */
292
+ onChange(e) {
293
+ this.$emit("update-state");
294
+ },
262
295
  }
263
296
  };
264
297
  </script>
@@ -334,4 +367,12 @@ export default {
334
367
  box-shadow: 0 0 0 13px rgba(0, 0, 0, 0);
335
368
  }
336
369
  }
370
+ .custom-badge {
371
+ background-color: #D1F4D7 !important;
372
+ color: #06723A !important;
373
+ padding: 0.5rem 0.75rem;
374
+ border-radius: 8px;
375
+ font-weight: 500;
376
+ font-size: 14px;
377
+ }
337
378
  </style>
@@ -42,8 +42,18 @@
42
42
  class="mr-2 ml-1"
43
43
  />
44
44
  {{ element.config.name || $t("Variable Name") }}
45
+ <b-badge
46
+ v-if="isInClipboard(element) && screenType === 'form'"
47
+ data-cy="copied-badge"
48
+ class="m-2 custom-badge"
49
+ pill
50
+ >
51
+ <i class="far fa-check-circle"></i>
52
+ <span class="pl-2">{{ $t('Copied')}}</span>
53
+ </b-badge>
45
54
  <div class="ml-auto">
46
55
  <clipboard-button
56
+ v-if="screenType === 'form'"
47
57
  :index="index"
48
58
  :config="element.config"
49
59
  :isInClipboard="isInClipboard(element)"
@@ -86,6 +96,7 @@
86
96
  :selected="selected"
87
97
  :ai-element="element"
88
98
  :config="element.config"
99
+ :screen-type="screenType"
89
100
  @inspect="inspect"
90
101
  @update-state="$emit('update-state')"
91
102
  />
@@ -108,8 +119,18 @@
108
119
  class="mr-2 ml-1"
109
120
  />
110
121
  {{ element.config.name || $t("Variable Name") }}
122
+ <b-badge
123
+ v-if="isInClipboard(element) && screenType === 'form'"
124
+ data-cy="copied-badge"
125
+ class="m-2 custom-badge"
126
+ pill
127
+ >
128
+ <i class="far fa-check-circle"></i>
129
+ <span class="pl-2">{{ $t('Copied')}}</span>
130
+ </b-badge>
111
131
  <div class="ml-auto">
112
132
  <clipboard-button
133
+ v-if="screenType === 'form'"
113
134
  :index="index"
114
135
  :config="element.config"
115
136
  :isInClipboard="isInClipboard(element)"
@@ -145,6 +166,7 @@
145
166
  ]"
146
167
  :tabindex="element.config.interactive ? 0 : -1"
147
168
  :config="element.config"
169
+ :screen-type="screenType"
148
170
  @input="
149
171
  element.config.interactive
150
172
  ? (element.config.content = $event)
@@ -194,7 +216,7 @@ export default {
194
216
  ...renderer
195
217
  },
196
218
  mixins: [HasColorProperty, Clipboard],
197
- props: ["value", "name", "config", "selected", "validationErrors"],
219
+ props: ["value", "name", "config", "selected", "validationErrors", "screenType"],
198
220
  data() {
199
221
  return {
200
222
  items: [],
@@ -375,4 +397,12 @@ export default {
375
397
  box-shadow: 0 0 0 13px rgba(0, 0, 0, 0);
376
398
  }
377
399
  }
400
+ .custom-badge {
401
+ background-color: #D1F4D7 !important;
402
+ color: #06723A !important;
403
+ padding: 0.5rem 0.75rem;
404
+ border-radius: 8px;
405
+ font-weight: 500;
406
+ font-size: 14px;
407
+ }
378
408
  </style>
@@ -194,6 +194,9 @@
194
194
  },
195
195
  deep: true
196
196
  },
197
+ dataSelectionOptions() {
198
+ this.singleField = null;
199
+ }
197
200
  },
198
201
  };
199
202
  </script>
@@ -36,6 +36,9 @@
36
36
  ],
37
37
  };
38
38
  },
39
+ mounted () {
40
+ this.callBuilder(this.designerOptions);
41
+ },
39
42
  computed: {
40
43
  options() {
41
44
  return Object.fromEntries(
@@ -56,10 +59,15 @@
56
59
  options: {
57
60
  handler() {
58
61
  this.$emit("input", this.options);
59
- this.$root.$emit("style-mode", this.options.designerOptions);
62
+ this.callBuilder(this.options.designerOptions);
60
63
  },
61
64
  deep: true
62
65
  },
63
66
  },
67
+ methods: {
68
+ callBuilder(option) {
69
+ this.$root.$emit("style-mode", option);
70
+ }
71
+ }
64
72
  };
65
73
  </script>
@@ -1,13 +1,18 @@
1
1
  <template>
2
2
  <div>
3
3
  <div>
4
- <label for="collection">{{ $t("Collection") }}</label>
5
- <b-form-select
6
- id="collection"
7
- v-model="collectionId"
8
- :options="collections"
9
- data-cy="inspector-collection"
10
- />
4
+ <label for="collection">{{ $t("Collection Name") }}</label>
5
+ <b-form-group>
6
+ <b-form-select
7
+ id="collection"
8
+ v-model="collectionId"
9
+ :options="collections"
10
+ data-cy="inspector-collection"
11
+ />
12
+ <b-form-text class="mt-2">
13
+ {{ $t("Collection Record Control is not available for Anonymous Web Entry") }}
14
+ </b-form-text>
15
+ </b-form-group>
11
16
  </div>
12
17
  <div v-if="collectionId > 0" class="screen-link mt-2">
13
18
  <a
@@ -13,7 +13,7 @@
13
13
  <div v-if="settings.encrypted">
14
14
  <select-user-group
15
15
  :key="componentKey"
16
- :label="$t('Users/Groups to View')"
16
+ :label="$t('Users/Groups to View Encrypted Fields')"
17
17
  v-model="settings.assignments"
18
18
  :multiple="true"
19
19
  @input="emitChanges"
@@ -158,6 +158,10 @@ export default {
158
158
  this.removeDefaultClasses();
159
159
  },
160
160
  mounted() {
161
+ if (this.value) {
162
+ this.fetchFiles();
163
+ }
164
+
161
165
  this.$root.$on('set-upload-data-name',
162
166
  (recordList, index, id) => this.listenRecordList(recordList, index, id));
163
167
 
@@ -597,6 +601,10 @@ export default {
597
601
  }
598
602
 
599
603
  if (displayMessage.length > 0) {
604
+ const data = JSON.parse(displayMessage);
605
+ if (data.message) {
606
+ displayMessage = data.message;
607
+ }
600
608
  window.ProcessMaker.alert(`${this.$t('File Upload Error:')} ${displayMessage}`, 'danger');
601
609
  }
602
610
 
@@ -699,7 +707,29 @@ export default {
699
707
  },
700
708
  cfSkipFileUpload() {
701
709
  this.$emit('cf-skip-file-upload');
702
- }
710
+ },
711
+ async fetchFiles() {
712
+ const fileIds = Array.isArray(this.value) ? this.value : [this.value];
713
+
714
+ const fetchPromises = fileIds.map(async (file) => {
715
+ const id = file?.file ?? file;
716
+ const endpoint = `files/${id}`;
717
+ try {
718
+ const response = await ProcessMaker.apiClient.get(endpoint);
719
+ if (response?.data) {
720
+ const fileExists = this.files.some(existingFile => existingFile.id === response.data.id);
721
+ // Check if the file already exists in the list before adding it.
722
+ if (!fileExists) {
723
+ this.files.push(response.data);
724
+ }
725
+ }
726
+ } catch (error) {
727
+ console.error(`Failed to fetch file ${id}`, error);
728
+ }
729
+ });
730
+
731
+ return await Promise.all(fetchPromises);
732
+ },
703
733
  },
704
734
  };
705
735
  </script>
@@ -16,6 +16,7 @@
16
16
  <script>
17
17
  import VueFormRenderer from "../vue-form-renderer.vue";
18
18
  import CollectionRecordsList from "../inspector/collection-records-list.vue";
19
+ import _ from 'lodash';
19
20
 
20
21
  const globalObject = typeof window === "undefined" ? global : window;
21
22
 
@@ -179,12 +180,12 @@ export default {
179
180
  this.selDisplayMode === "View" ? viewScreen : editScreen;
180
181
 
181
182
  this.loadScreen(this.screenCollectionId);
182
-
183
+
183
184
  //This section validates if Collection has draft data
184
185
  if(this.taskDraft?.draft?.data == null || this.taskDraft.draft.data === '') {
185
186
  this.localData = respData;
186
187
  }else{
187
- this.localData = this.taskDraft.draft.data;
188
+ this.localData = _.merge({}, respData, this.taskDraft.draft.data);
188
189
  }
189
190
 
190
191
  })
@@ -212,9 +213,9 @@ export default {
212
213
  },
213
214
  record(record) {
214
215
  this.hasMustache = false;
215
- if (record && !isNaN(record) && record > 0 && this.collection) {
216
+ if (record && !isNaN(record) && record > 0 && this.collection.collectionId) {
216
217
  this.selRecordId = record;
217
- this.loadRecordCollection(this.selCollectionId, record, this.collectionmode);
218
+ this.loadRecordCollection(this.collection.collectionId, record, this.selDisplayMode);
218
219
  } else {
219
220
  if (this.isMustache(record)) {
220
221
  this.callbackRecord();
@@ -206,9 +206,9 @@ export default {
206
206
  },
207
207
  record(record) {
208
208
  this.hasMustache = false;
209
- if (record && !isNaN(record) && record > 0 && this.collection) {
209
+ if (record && !isNaN(record) && record > 0 && this.collection.collectionId) {
210
210
  this.selRecordId = record;
211
- this.loadRecordCollection(this.selCollectionId, record, this.collectionmode);
211
+ this.loadRecordCollection(this.collection.collectionId, record, this.collectionmode);
212
212
  } else {
213
213
  if (this.isMustache(record)) {
214
214
  this.callbackRecord();
@@ -41,7 +41,7 @@
41
41
  <input
42
42
  v-else
43
43
  v-model="localValue"
44
- v-bind="componentConfig"
44
+ v-bind="componentConfigEncField"
45
45
  v-uni-id="name"
46
46
  :name="name"
47
47
  class="form-control"
@@ -231,6 +231,7 @@ export default {
231
231
  labelBtn: '',
232
232
  errorEncryptedField: '',
233
233
  concealExecuted: false,
234
+ componentConfigEncField: null,
234
235
  };
235
236
  },
236
237
  computed: {
@@ -289,17 +290,18 @@ export default {
289
290
  * "encrypted" attribute is enabled
290
291
  */
291
292
  if (this.encryptedConfig?.encrypted) {
293
+ this.componentConfigEncField = JSON.parse(JSON.stringify(this.componentConfig));
292
294
  if (uuidValidate(this.localValue)) {
293
295
  this.inputType = "password";
294
296
  this.iconBtn = "fas fa-eye";
295
297
  this.labelBtn = this.$t("Reveal");
296
298
  this.concealExecuted = true;
297
- this.componentConfig.readonly = true;
299
+ this.componentConfigEncField.readonly = true;
298
300
  } else {
299
301
  this.inputType = "text";
300
302
  this.iconBtn = "fas fa-eye-slash";
301
303
  this.labelBtn = this.$t("Conceal");
302
- this.componentConfig.readonly = false;
304
+ this.componentConfigEncField.readonly = this.componentConfig.readonly;
303
305
  }
304
306
  } else {
305
307
  this.inputType = this.dataType;
@@ -381,7 +383,7 @@ export default {
381
383
  this.iconBtn = "fas fa-eye";
382
384
  this.labelBtn = this.$t("Reveal");
383
385
  this.concealExecuted = true;
384
- this.componentConfig.readonly = true;
386
+ this.componentConfigEncField.readonly = true;
385
387
  this.errorEncryptedField = "";
386
388
 
387
389
  // Assign uuid from encrypted data
@@ -392,7 +394,7 @@ export default {
392
394
  this.inputType = this.dataType;
393
395
  this.iconBtn = "fas fa-eye-slash";
394
396
  this.labelBtn = this.$t("Conceal");
395
- this.componentConfig.readonly = false;
397
+ this.componentConfigEncField.readonly = this.componentConfig.readonly;
396
398
 
397
399
  // Assign value decrypted
398
400
  this.localValue = decryptedValue;
@@ -107,6 +107,7 @@ export default {
107
107
  alwaysAllowEditing: { type: Boolean, default: false },
108
108
  disableInterstitial: { type: Boolean, default: false },
109
109
  waitLoadingListeners: { type: Boolean, default: false },
110
+ isWebEntry: { type: Boolean, default: false },
110
111
  },
111
112
  data() {
112
113
  return {
@@ -677,55 +678,97 @@ export default {
677
678
  return null;
678
679
  }
679
680
  },
681
+
680
682
  /**
681
- * Handles the process completion and redirects the user based on the task assignment.
682
- * @param {object} data - The data object containing endEventDestination.
683
- * @param {string} userId - The ID of the current user.
684
- * @param {string} requestId - The ID of the current request.
683
+ * Handles redirection upon process completion, considering destination type and user task validation.
684
+ * @async
685
+ * @param {Object} data - Contains information about the end event destination.
686
+ * @param {number} userId - ID of the current user.
687
+ * @param {number} requestId - ID of the request to complete.
688
+ * @returns {Promise<void>}
685
689
  */
686
690
  async processCompletedRedirect(data, userId, requestId) {
691
+ // Emit completion event if accessed through web entry.
692
+ if (this.isWebEntry) {
693
+ this.$emit("completed", requestId);
694
+ return;
695
+ }
696
+
687
697
  try {
688
- // Verify if is not anotherProcess type
689
- if (data.endEventDestination.type !== "anotherProcess") {
690
- if (data?.endEventDestination.value) {
691
- window.location.href = data?.endEventDestination.value;
692
- } else {
693
- window.location.href = `/requests/${this.requestId}`;
694
- }
698
+ const destinationUrl = this.resolveDestinationUrl(data);
699
+ if (destinationUrl) {
700
+ window.location.href = destinationUrl;
695
701
  return;
696
702
  }
697
- // Parse endEventDestination from the provided data
698
- const endEventDestination = this.parseJsonSafely(
699
- data.endEventDestination.value
700
- );
701
- // Get the next request using retry logic
702
- const nextRequest = await this.retryApiCall(() =>
703
- this.getNextRequest(
704
- endEventDestination.processId,
705
- endEventDestination.startEvent
706
- )
707
- );
708
-
709
- const params = {
710
- processRequestId: nextRequest.data.id,
711
- status: "ACTIVE",
712
- page: 1,
713
- perPage: 1
714
- };
715
- // Get the tasks for the next request using retry logic
716
- const response = await this.retryApiCall(() => this.getTasks(params));
717
- // Handle the first task from the response
718
- const firstTask = response.data.data[0];
719
- if (firstTask && firstTask.user_id === userId) {
720
- this.redirectToTask(firstTask.id);
721
- } else {
722
- this.redirectToRequest(requestId);
723
- }
703
+
704
+ // Proceed to handle redirection to the next request if applicable.
705
+ await this.handleNextRequestRedirection(data, userId, requestId);
724
706
  } catch (error) {
725
707
  console.error("Error processing completed redirect:", error);
726
708
  this.$emit("completed", requestId);
727
709
  }
728
710
  },
711
+
712
+ /**
713
+ * Resolves the URL to redirect to if the end event is not another process.
714
+ * @param {Object} data - Contains the end event destination data.
715
+ * @returns {string|null} - The URL for redirection, or null if proceeding to another process.
716
+ */
717
+ resolveDestinationUrl(data) {
718
+ if (data.endEventDestination.type !== "anotherProcess") {
719
+ return data.endEventDestination.value || `/requests/${this.requestId}`;
720
+ }
721
+ return null;
722
+ },
723
+
724
+ /**
725
+ * Handles redirection logic to the next request's task or fallback to the request itself.
726
+ * @async
727
+ * @param {Object} data - Contains the end event destination.
728
+ * @param {number} userId - ID of the current user.
729
+ * @param {number} requestId - ID of the request to complete.
730
+ * @returns {Promise<void>}
731
+ */
732
+ async handleNextRequestRedirection(data, userId, requestId) {
733
+ const nextRequest = await this.fetchNextRequest(data.endEventDestination);
734
+ const firstTask = await this.fetchFirstTask(nextRequest);
735
+
736
+ if (firstTask?.user_id === userId) {
737
+ this.redirectToTask(firstTask.id);
738
+ } else {
739
+ this.redirectToRequest(requestId);
740
+ }
741
+ },
742
+
743
+ /**
744
+ * Fetch the next request using retry logic.
745
+ * @async
746
+ * @param {Object} endEventDestination - The parsed end event destination object.
747
+ * @returns {Promise<Object>} - The next request data.
748
+ */
749
+ async fetchNextRequest(endEventDestination) {
750
+ const destinationData = this.parseJsonSafely(endEventDestination.value);
751
+ return await this.retryApiCall(() =>
752
+ this.getNextRequest(destinationData.processId, destinationData.startEvent)
753
+ );
754
+ },
755
+
756
+ /**
757
+ * Fetch the first task from the next request using retry logic.
758
+ * @async
759
+ * @param {Object} nextRequest - The next request object.
760
+ * @returns {Promise<Object|null>} - The first task data, or null if no tasks found.
761
+ */
762
+ async fetchFirstTask(nextRequest) {
763
+ const params = {
764
+ processRequestId: nextRequest.data.id,
765
+ status: "ACTIVE",
766
+ page: 1,
767
+ perPage: 1
768
+ };
769
+ const response = await this.retryApiCall(() => this.getTasks(params));
770
+ return response.data.data[0] || null;
771
+ },
729
772
  getAllowedRequestId() {
730
773
  const permissions = this.task.user_request_permission || [];
731
774
  const permission = permissions.find(item => item.process_request_id === this.parentRequest)