@processmaker/screen-builder 2.90.0 → 2.91.0

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.
Files changed (34) hide show
  1. package/README.md +5 -4
  2. package/dist/vue-form-builder.css +1 -1
  3. package/dist/vue-form-builder.es.js +7868 -7451
  4. package/dist/vue-form-builder.es.js.map +1 -1
  5. package/dist/vue-form-builder.umd.js +74 -57
  6. package/dist/vue-form-builder.umd.js.map +1 -1
  7. package/package.json +3 -3
  8. package/src/DataProvider.js +28 -8
  9. package/src/assets/icons/Bypass.svg +5 -0
  10. package/src/assets/icons/Unbypass.svg +5 -0
  11. package/src/components/accordions.js +1 -0
  12. package/src/components/computed-properties.vue +211 -110
  13. package/src/components/index.js +2 -0
  14. package/src/components/renderer/form-list-table.vue +6 -1
  15. package/src/components/renderer/form-requests.vue +2 -2
  16. package/src/components/renderer/index.js +1 -0
  17. package/src/components/renderer/link-button.vue +30 -0
  18. package/src/components/sortable/Sortable.vue +95 -13
  19. package/src/components/sortable/sortable.scss +5 -0
  20. package/src/components/sortable/sortableList/SortableList.vue +103 -36
  21. package/src/components/sortable/sortableList/sortableList.scss +63 -22
  22. package/src/components/task.vue +256 -59
  23. package/src/components/vue-form-builder.vue +19 -10
  24. package/src/components/watchers-form.vue +4 -3
  25. package/src/components/watchers-list.vue +46 -100
  26. package/src/components/watchers-popup.vue +89 -16
  27. package/src/customLogs.js +26 -0
  28. package/src/form-builder-controls.js +42 -0
  29. package/src/main.js +26 -1
  30. package/src/mixins/computedFields.js +25 -7
  31. package/src/mixins/extensions/ComputedFields.js +26 -5
  32. package/src/mixins/extensions/Watchers.js +4 -0
  33. package/src/mixins/watchers.js +5 -2
  34. package/src/stories/Sortable.stories.js +58 -11
@@ -23,6 +23,7 @@
23
23
  <vue-form-renderer
24
24
  ref="renderer"
25
25
  v-model="requestData"
26
+ :class="{ loading: loadingTask || loadingListeners }"
26
27
  :config="screen.config"
27
28
  :computed="screen.computed"
28
29
  :custom-css="screen.custom_css"
@@ -120,6 +121,8 @@ export default {
120
121
  refreshScreen: 0,
121
122
  redirecting: null,
122
123
  loadingButton: false,
124
+ loadingTask: false,
125
+ loadingListeners: true,
123
126
  };
124
127
  },
125
128
  watch: {
@@ -129,12 +132,6 @@ export default {
129
132
  },
130
133
  },
131
134
 
132
- initialTaskId: {
133
- handler() {
134
- this.taskId = this.initialTaskId;
135
- },
136
- },
137
-
138
135
  initialRequestId: {
139
136
  handler() {
140
137
  this.requestId = this.initialRequestId;
@@ -161,16 +158,6 @@ export default {
161
158
  },
162
159
  },
163
160
 
164
- taskId: {
165
- handler() {
166
- if (this.taskId) {
167
- if (!this.task || this.task.id !== this.taskId) {
168
- this.loadTask();
169
- }
170
- }
171
- },
172
- },
173
-
174
161
  requestId: {
175
162
  handler() {
176
163
  if (this.requestId) {
@@ -182,30 +169,6 @@ export default {
182
169
  },
183
170
  },
184
171
 
185
- task: {
186
- handler() {
187
- if (!this.screen) {
188
- // if no current screen show the interstitial screen if exists
189
- this.screen = this.task && this.task.interstitial_screen;
190
- }
191
- this.taskId = this.task.id;
192
- this.nodeId = this.task.element_id;
193
- this.listenForParentChanges();
194
- if (this.task.process_request.status === 'COMPLETED') {
195
- if (!this.taskPreview) {
196
- this.$emit('completed', this.task.process_request.id);
197
- }
198
- }
199
- if (this.taskPreview && this.task.status === "CLOSED") {
200
- this.task.interstitial_screen['_interstitial'] = false;
201
- if (!this.alwaysAllowEditing) {
202
- this.task.screen.config = this.disableForm(this.task.screen.config);
203
- }
204
- this.screen = this.task.screen;
205
- }
206
- }
207
- },
208
-
209
172
  value: {
210
173
  handler() {
211
174
  this.requestData = this.value;
@@ -295,7 +258,7 @@ export default {
295
258
  }
296
259
  },
297
260
  loadTask() {
298
- const url = `/${this.taskId}?include=data,user,draft,requestor,processRequest,component,screen,requestData,loopContext,bpmnTagName,interstitial,definition,nested,userRequestPermission`;
261
+ const url = `/${this.taskId}?include=data,user,draft,requestor,processRequest,component,screen,requestData,loopContext,bpmnTagName,interstitial,definition,nested,userRequestPermission,elementDestination`;
299
262
  // For Vocabularies
300
263
  if (window.ProcessMaker && window.ProcessMaker.packages && window.ProcessMaker.packages.includes('package-vocabularies')) {
301
264
  window.ProcessMaker.VocabulariesSchemaUrl = `vocabularies/task_schema/${this.taskId}`;
@@ -306,6 +269,7 @@ export default {
306
269
  .getTasks(url)
307
270
  .then((response) => {
308
271
  this.task = response.data;
272
+ this.linkTask();
309
273
  this.checkTaskStatus();
310
274
  if (
311
275
  window.PM4ConfigOverrides
@@ -319,9 +283,28 @@ export default {
319
283
  })
320
284
  .catch(() => {
321
285
  this.hasErrors = true;
286
+ })
287
+ .finally(() => {
288
+ this.loadingTask = false;
322
289
  });
323
290
  });
324
291
  },
292
+ linkTask() {
293
+ this.nodeId = this.task.element_id;
294
+ this.listenForParentChanges();
295
+ if (this.task.process_request.status === 'COMPLETED') {
296
+ if (!this.taskPreview) {
297
+ this.$emit('completed', this.task.process_request.id);
298
+ }
299
+ }
300
+ if (this.taskPreview && this.task.status === "CLOSED") {
301
+ this.task.interstitial_screen['_interstitial'] = false;
302
+ if (!this.alwaysAllowEditing) {
303
+ this.task.screen.config = this.disableForm(this.task.screen.config);
304
+ }
305
+ this.screen = this.task.screen;
306
+ }
307
+ },
325
308
  prepareTask() {
326
309
  // If the immediate task status is completed and we are waiting with a loading button,
327
310
  // do not reset the screen because that would stop displaying the loading spinner
@@ -380,28 +363,107 @@ export default {
380
363
  }
381
364
  });
382
365
  },
366
+ /**
367
+ * Closes the current task and performs necessary actions based on task properties.
368
+ * @param {string|null} parentRequestId - The parent request ID.
369
+ */
383
370
  closeTask(parentRequestId = null) {
384
371
  if (this.hasErrors) {
385
- this.$emit('error', this.requestId);
386
- return;
372
+ this.emitError();
387
373
  }
388
374
 
389
- if (this.task.process_request.status === 'COMPLETED') {
390
- this.loadNextAssignedTask(parentRequestId);
391
- } else if (this.loadingButton) {
375
+ if (this.shouldLoadNextTask()) {
392
376
  this.loadNextAssignedTask(parentRequestId);
393
377
  } else if (this.task.allow_interstitial) {
394
- this.task.interstitial_screen['_interstitial'] = true;
395
- this.screen = this.task.interstitial_screen;
396
- this.loadNextAssignedTask(parentRequestId);
378
+ this.showInterstitial(parentRequestId);
397
379
  } else if (!this.taskPreview) {
398
- this.$emit('closed', this.task.id);
380
+ this.emitClosedEvent();
381
+ }
382
+ },
383
+
384
+ /**
385
+ * Checks if the next task should be loaded.
386
+ * @returns {boolean} - True if the next task should be loaded, otherwise false.
387
+ */
388
+ shouldLoadNextTask() {
389
+ return (
390
+ this.task.process_request.status === "COMPLETED" || this.loadingButton
391
+ );
392
+ },
393
+ /**
394
+ * Shows the interstitial screen and loads the next assigned task.
395
+ * @param {string|null} parentRequestId - The parent request ID.
396
+ */
397
+ showInterstitial(parentRequestId) {
398
+ // Show the interstitial screen
399
+ this.task.interstitial_screen['_interstitial'] = true;
400
+ this.screen = this.task.interstitial_screen;
401
+
402
+ // Load the next assigned task
403
+ this.loadNextAssignedTask(parentRequestId);
404
+ },
405
+ /**
406
+ * Emits an error event.
407
+ */
408
+ emitError() {
409
+ this.$emit('error', this.requestId);
410
+ },
411
+ /**
412
+ * Emits a closed event.
413
+ */
414
+ async emitClosedEvent() {
415
+ this.$emit("closed", this.task.id, await this.getDestinationUrl());
416
+ },
417
+ /**
418
+ * Retrieves the destination URL for the closed event.
419
+ * @returns {string|null} - The destination URL.
420
+ */
421
+ // eslint-disable-next-line consistent-return
422
+ async getDestinationUrl() {
423
+ const { elementDestination } = this.task || {};
424
+
425
+ if (!elementDestination) {
426
+ return null;
427
+ }
428
+
429
+ if (elementDestination.type === "taskSource") {
430
+ try {
431
+ const params = {
432
+ processRequestId: this.requestId,
433
+ status: "ACTIVE",
434
+ userId: this.userId,
435
+ };
436
+
437
+ const response = await this.retryApiCall(() => this.getTasks(params));
438
+
439
+ const firstTask = response.data.data[0];
440
+ if (firstTask?.user_id === this.userId) {
441
+ return `/tasks/${firstTask.id}/edit`;
442
+ }
443
+
444
+ return document.referrer || null;
445
+ } catch (error) {
446
+ console.error("Error in getDestinationUrl:", error);
447
+ return null;
448
+ }
449
+ }
450
+
451
+ const elementDestinationUrl = elementDestination.value;
452
+ if (elementDestinationUrl) {
453
+ return elementDestinationUrl;
399
454
  }
455
+
456
+ const sessionStorageUrl = sessionStorage.getItem("elementDestinationURL");
457
+ return sessionStorageUrl || null;
400
458
  },
459
+
401
460
  loadNextAssignedTask(requestId = null) {
402
461
  if (!requestId) {
403
462
  requestId = this.requestId;
404
463
  }
464
+ if (!this.userId) {
465
+ return;
466
+ }
405
467
  const timestamp = !window.Cypress ? `&t=${Date.now()}` : "";
406
468
  const url = `?user_id=${this.userId}&status=ACTIVE&process_request_id=${requestId}&include_sub_tasks=1${timestamp}`;
407
469
  return this.$dataProvider
@@ -421,6 +483,7 @@ export default {
421
483
  this.emitIfTaskCompleted(requestId);
422
484
  }
423
485
  this.taskId = task.id;
486
+ this.loadTask();
424
487
  this.nodeId = task.element_id;
425
488
  } else if (this.parentRequest && ['COMPLETED', 'CLOSED'].includes(this.task.process_request.status)) {
426
489
  this.$emit('completed', this.getAllowedRequestId());
@@ -481,14 +544,139 @@ export default {
481
544
  activityAssigned() {
482
545
  // This may no longer be needed
483
546
  },
484
- processCompleted() {
547
+ processCompleted(data = null) {
485
548
  let requestId;
486
549
  if (this.parentRequest) {
487
550
  requestId = this.getAllowedRequestId();
488
551
  this.$emit('completed', requestId);
489
552
  }
490
553
  if (requestId !== this.requestId) {
491
- this.$emit('completed', this.requestId);
554
+ this.processCompletedRedirect(data, this.userId, this.requestId);
555
+ }
556
+ },
557
+ /**
558
+ * Makes an API call with retry logic.
559
+ * @param {Function} apiCall - The API call to be made.
560
+ * @param {number} retries - The number of retry attempts.
561
+ * @param {number} delay - The delay between retries in milliseconds.
562
+ * @returns {Promise} - The response from the API call.
563
+ */
564
+ // eslint-disable-next-line consistent-return
565
+ async retryApiCall(apiCall, retries = 3, delay = 1000) {
566
+ for (let attempt = 0; attempt < retries; attempt++) {
567
+ try {
568
+ // eslint-disable-next-line no-await-in-loop
569
+ const response = await apiCall();
570
+ return response;
571
+ } catch (error) {
572
+ if (attempt === retries - 1) {
573
+ throw error;
574
+ }
575
+ // eslint-disable-next-line no-await-in-loop
576
+ await new Promise((resolve) => {
577
+ setTimeout(resolve, delay);
578
+ });
579
+ }
580
+ }
581
+ },
582
+ /**
583
+ * Gets the next request by posting to the specified endpoint.
584
+ * @param {string} processId - The process ID.
585
+ * @param {string} startEvent - The start event.
586
+ * @returns {Promise} - The response from the API call.
587
+ */
588
+ getNextRequest(processId, startEvent) {
589
+ return window.ProcessMaker.apiClient.post(
590
+ `/process_events/${processId}?event=${startEvent}`,
591
+ {}
592
+ );
593
+ },
594
+
595
+ /**
596
+ * Gets the tasks for the specified process request ID.
597
+ * @param {object} params - The query params.
598
+ * @returns {Promise} - The response from the API call.
599
+ */
600
+
601
+ getTasks(params) {
602
+ const queryParams = {
603
+ user_id: this.userId,
604
+ process_request_id: params.processRequestId,
605
+ page: params.page,
606
+ per_page: params.perPage,
607
+ status: params.status
608
+ };
609
+
610
+ const queryString = Object.entries(queryParams)
611
+ .filter(([, value]) => value !== undefined && value !== null)
612
+ .map(
613
+ ([key, value]) =>
614
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
615
+ )
616
+ .join("&");
617
+
618
+ return window.ProcessMaker.apiClient.get(`tasks?${queryString}`);
619
+ },
620
+ /**
621
+ * Parses a JSON string and returns the result.
622
+ * @param {string} jsonString - The JSON string to parse.
623
+ * @returns {object|null} - The parsed object or null if parsing fails.
624
+ */
625
+ parseJsonSafely(jsonString) {
626
+ try {
627
+ return JSON.parse(jsonString);
628
+ } catch (error) {
629
+ console.error("Invalid JSON string:", error);
630
+ return null;
631
+ }
632
+ },
633
+ /**
634
+ * Handles the process completion and redirects the user based on the task assignment.
635
+ * @param {object} data - The data object containing endEventDestination.
636
+ * @param {string} userId - The ID of the current user.
637
+ * @param {string} requestId - The ID of the current request.
638
+ */
639
+ async processCompletedRedirect(data, userId, requestId) {
640
+ try {
641
+ // Verify if is not anotherProcess type
642
+ if (data.endEventDestination.type !== "anotherProcess") {
643
+ this.$emit(
644
+ "completed",
645
+ this.requestId,
646
+ data?.endEventDestination.value
647
+ );
648
+ return;
649
+ }
650
+ // Parse endEventDestination from the provided data
651
+ const endEventDestination = this.parseJsonSafely(
652
+ data.endEventDestination.value
653
+ );
654
+ // Get the next request using retry logic
655
+ const nextRequest = await this.retryApiCall(() =>
656
+ this.getNextRequest(
657
+ endEventDestination.processId,
658
+ endEventDestination.startEvent
659
+ )
660
+ );
661
+
662
+ const params = {
663
+ processRequestId: nextRequest.data.id,
664
+ status: "ACTIVE",
665
+ page: 1,
666
+ perPage: 1
667
+ };
668
+ // Get the tasks for the next request using retry logic
669
+ const response = await this.retryApiCall(() => this.getTasks(params));
670
+ // Handle the first task from the response
671
+ const firstTask = response.data.data[0];
672
+ if (firstTask && firstTask.user_id === userId) {
673
+ this.$emit("completed", requestId, `/tasks/${firstTask.id}/edit`);
674
+ } else {
675
+ this.$emit("completed", requestId);
676
+ }
677
+ } catch (error) {
678
+ console.error("Error processing completed redirect:", error);
679
+ this.$emit("completed", requestId);
492
680
  }
493
681
  },
494
682
  getAllowedRequestId() {
@@ -498,14 +686,19 @@ export default {
498
686
  return allowed ? this.parentRequest : this.requestId
499
687
  },
500
688
  processUpdated: _.debounce(function(data) {
501
- if (data.event === 'ACTIVITY_ACTIVATED') {
689
+ if (
690
+ data.event === "ACTIVITY_ACTIVATED"
691
+ && data.elementType === 'task'
692
+ ) {
693
+ this.taskId = data.tokenId;
502
694
  this.reload();
503
695
  }
504
696
  if (data.event === 'ACTIVITY_EXCEPTION') {
505
697
  this.$emit('error', this.requestId);
506
698
  }
507
- }, 300),
699
+ }, 100),
508
700
  initSocketListeners() {
701
+ this.loadingListeners = false;
509
702
  this.addSocketListener(
510
703
  `completed-${this.requestId}`,
511
704
  `ProcessMaker.Models.ProcessRequest.${this.requestId}`,
@@ -514,7 +707,6 @@ export default {
514
707
  this.processCompleted(data);
515
708
  }
516
709
  );
517
-
518
710
  this.addSocketListener(
519
711
  `updated-${this.requestId}`,
520
712
  `ProcessMaker.Models.ProcessRequest.${this.requestId}`,
@@ -547,14 +739,12 @@ export default {
547
739
  '.ProcessUpdated',
548
740
  (data) => {
549
741
  if (
550
- ['ACTIVITY_ACTIVATED'].includes(data.event) &&
551
- !this.existsEventMessage(`${data.event}-${this.userId}-${this.taskId}`)
742
+ ['ACTIVITY_ACTIVATED'].includes(data.event)
552
743
  ) {
553
744
  this.closeTask(this.parentRequest);
554
745
  }
555
746
  if (
556
- ["ACTIVITY_COMPLETED"].includes(data.event) &&
557
- !this.existsEventMessage(`${data.event}-${this.userId}-${this.taskId}`)
747
+ ["ACTIVITY_COMPLETED"].includes(data.event)
558
748
  ) {
559
749
  if (this.task.process_request.status === 'COMPLETED') {
560
750
  this.processCompleted();
@@ -612,9 +802,16 @@ export default {
612
802
  this.nodeId = this.initialNodeId;
613
803
  this.requestData = this.value;
614
804
  this.loopContext = this.initialLoopContext;
805
+ this.loadTask();
615
806
  },
616
807
  destroyed() {
617
808
  this.unsubscribeSocketListeners();
618
809
  },
619
810
  };
620
811
  </script>
812
+
813
+ <style scoped>
814
+ .loading {
815
+ pointer-events: none;
816
+ }
817
+ </style>
@@ -368,18 +368,20 @@
368
368
  :ok-title="$t('DONE')"
369
369
  ok-only
370
370
  ok-variant="secondary"
371
- header-class = "modal-header-custom"
371
+ header-class="modal-header-custom"
372
372
  >
373
- <template #modal-title>
374
- <h5 class="modal-title">{{ $t('Edit Pages') }}</h5>
375
- <span class="modal-subtitle">{{ $t('Change pages order and name') }}</span>
376
- </template>
377
- <template #modal-header-close="{ close }">
378
- <button type="button" aria-label="Close" class="close" @click="close()">×</button>
379
- </template>
373
+ <template #modal-title>
374
+ <h5 class="modal-title">{{ $t('Edit Pages') }}</h5>
375
+ <span class="modal-subtitle">{{ $t('Change pages order and name') }}</span>
376
+ </template>
377
+ <template #modal-header-close="{ close }">
378
+ <button type="button" aria-label="Close" class="close">×</button>
379
+ </template>
380
+
380
381
  <Sortable
382
+ :fields="fields"
381
383
  :items="config"
382
- filter-key="name"
384
+ :search-properties="searchProperties"
383
385
  @item-edit="() => {}"
384
386
  @item-delete="confirmDelete"
385
387
  @add-page="$bvModal.show('addPageModal')"
@@ -604,6 +606,12 @@ export default {
604
606
  editPageIndex: null,
605
607
  editPageName: "",
606
608
  originalPageName: null,
609
+ fields: [
610
+ {
611
+ label: this.$t("Name"),
612
+ key: "name",
613
+ },
614
+ ],
607
615
  config,
608
616
  confirmMessage: "",
609
617
  pageDelete: 0,
@@ -621,7 +629,8 @@ export default {
621
629
  editorContentKey: 0,
622
630
  cancelledJobs: [],
623
631
  collapse: {},
624
- groupOrder: {}
632
+ groupOrder: {},
633
+ searchProperties: ['name'],
625
634
  };
626
635
  },
627
636
  computed: {
@@ -341,14 +341,14 @@
341
341
 
342
342
  <div class="d-flex justify-content-end mt-3">
343
343
  <button
344
- class="btn btn-outline-secondary"
344
+ class="btn btn-outline-secondary text-uppercase"
345
345
  data-cy="watchers-button-cancel"
346
346
  @click.stop="displayTableList"
347
347
  >
348
348
  {{ $t("Cancel") }}
349
349
  </button>
350
350
  <button
351
- class="btn btn-secondary ml-3"
351
+ class="btn btn-secondary ml-3 text-uppercase"
352
352
  data-cy="watchers-button-save"
353
353
  @click="validateDataAndSave"
354
354
  >
@@ -701,6 +701,7 @@ export default {
701
701
 
702
702
  if (!this.config.uid) {
703
703
  this.config.uid = _.uniqueId(new Date().getTime());
704
+ this.config.byPass = false;
704
705
  }
705
706
 
706
707
  this.save();
@@ -743,7 +744,7 @@ export default {
743
744
  }
744
745
 
745
746
  .editor {
746
- height: 8.5em;
747
+ height: 375px;
747
748
  z-index: 0;
748
749
  }
749
750