diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index 2d556c62f3..668d28cc44 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Http\Controllers\Api; use Carbon\Carbon; +use Facades\ProcessMaker\RollbackProcessRequest; use Illuminate\Database\QueryException; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -417,4 +418,22 @@ public function getScreen(Request $request, ProcessRequestToken $task, Screen $s // Authorized in policy return new ApiResource($screen->versionFor($task->processRequest)); } + + public function eligibleRollbackTask(Request $request, ProcessRequestToken $task) + { + $eligibleTask = RollbackProcessRequest::eligibleRollbackTask($task); + if (!$eligibleTask) { + return ['message' => __('Task can not be rolled back')]; + } + + return new Resource($eligibleTask); + } + + public function rollbackTask(Request $request, ProcessRequestToken $task) + { + $processDefinitions = $task->process->getDefinitions(); + $newTask = RollbackProcessRequest::rollback($task, $processDefinitions); + + return new Resource($newTask); + } } diff --git a/ProcessMaker/Http/Controllers/RequestController.php b/ProcessMaker/Http/Controllers/RequestController.php index 5bdbc2c01e..1a0f7277e8 100644 --- a/ProcessMaker/Http/Controllers/RequestController.php +++ b/ProcessMaker/Http/Controllers/RequestController.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Http\Controllers; +use Facades\ProcessMaker\RollbackProcessRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; @@ -134,6 +135,12 @@ public function show(ProcessRequest $request, Media $mediaItems) $isProcessManager = $request->process?->manager_id === Auth::user()->id; + $eligibleRollbackTask = null; + $errorTask = RollbackProcessRequest::getErrorTask($request); + if ($errorTask) { + $eligibleRollbackTask = RollbackProcessRequest::eligibleRollbackTask($errorTask); + } + return view('requests.show', compact( 'request', 'files', @@ -145,7 +152,9 @@ public function show(ProcessRequest $request, Media $mediaItems) 'canPrintScreens', 'screenRequested', 'addons', - 'isProcessManager' + 'isProcessManager', + 'eligibleRollbackTask', + 'errorTask', )); } diff --git a/ProcessMaker/Policies/ProcessRequestTokenPolicy.php b/ProcessMaker/Policies/ProcessRequestTokenPolicy.php index 092de5c7dc..43893e3bd6 100644 --- a/ProcessMaker/Policies/ProcessRequestTokenPolicy.php +++ b/ProcessMaker/Policies/ProcessRequestTokenPolicy.php @@ -88,4 +88,18 @@ public function viewScreen(User $user, ProcessRequestToken $task, Screen $screen return true; } + + /** + * Determine if a user can rollback the process request. + * + * @param \ProcessMaker\Models\User $user + * @param \ProcessMaker\Models\ProcessRequestToken $processRequestToken + * + * @return bool + */ + public function rollback(User $user, ProcessRequestToken $task) + { + // For now, only the process manager can rollback the request + return $user->id === $task->process->managerId; + } } diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php new file mode 100644 index 0000000000..a0ac68b9dc --- /dev/null +++ b/ProcessMaker/RollbackProcessRequest.php @@ -0,0 +1,206 @@ +tokens()->orderBy('id', 'desc')->first(); + + if (!$lastTask) { + return null; + } + + if ($lastTask->status === 'FAILING') { + return $lastTask; + } + + // Allow for gateway tasks + if ( + $lastTask->element_type === 'gateway' && + $lastTask->status === 'CLOSED' && + $processRequest->status === 'ERROR' + ) { + return $lastTask; + } + + return null; + } + + /** + * Find an element in the request that can be rolled-back to. + * Return null if none are eligible. + * + * @return ProcessRequestToken|null + */ + public function eligibleRollbackTask(ProcessRequestToken $currentTask) : ?ProcessRequestToken + { + $processRequest = $currentTask->processRequest; + + return $processRequest->tokens() + ->where('status', 'CLOSED') + ->where('id', '<', $currentTask->id) + ->where('element_id', '!=', $currentTask->element_id) + ->whereIn('element_type', $this->eligibleTypes) + ->orderBy('id', 'desc') + ->first(); + } + + /** + * Rollback a token to the previous token and reset the request status. + * + * @param ProcessRequestToken $task + * @return ProcessRequestToken + */ + public function rollback( + ProcessRequestToken $currentTask, + BpmnDocument $processDefinitions + ) : ProcessRequestToken { + $this->currentTask = $currentTask; + $this->processDefinitions = $processDefinitions; + $this->rollbackToTask = $this->eligibleRollbackTask($currentTask); + if (!$this->rollbackToTask) { + throw new \Exception('No eligible rollback task found'); + } + + switch ($this->rollbackToTask->element_type) { + case 'scriptTask': + $this->rollbackToScriptTask(); + break; + case 'serviceTask': + $this->rollbackToServiceTask(); + break; + case 'task': + $this->rollbackToTask(); + break; + } + + $processRequest = $this->newTask->processRequest; + $processRequest->status = 'ACTIVE'; + $processRequest->process_version_id = $processRequest->process->getLatestVersion()->id; + $processRequest->saveOrFail(); + + $currentTask->status = 'CLOSED'; + $currentTask->saveOrFail(); + + return $this->newTask; + } + + private function rollbackToTask() + { + $this->copyTask(); + + if ($this->newTask->user_id) { + $this->newTask->sendActivityActivatedNotifications(); + } + event(new ActivityAssigned($this->newTask)); + + $this->addComment(); + } + + private function rollbackToScriptTask() + { + $this->copyTask(); + + $bpmnTask = $this->processDefinitions->getEvent($this->newTask->element_id); + + WorkflowManager::runScripTask($bpmnTask, $this->newTask); + + $this->addComment(); + } + + private function rollbackToServiceTask() + { + $this->copyTask(); + + $bpmnTask = $this->processDefinitions->getEvent($this->newTask->element_id); + + WorkflowManager::runServiceTask($bpmnTask, $this->newTask); + + $this->addComment(); + } + + private function copyTask() + { + $newTask = $this->rollbackToTask->replicate(); + $newTask->uuid = null; + $newTask->status = 'ACTIVE'; + $newTask->saveOrFail(); + $this->newTask = $newTask; + } + + /** + * Add a comment that the task was rolled back. + * + * @return void + */ + private function addComment() : void + { + $user = Auth::user(); + $userName = $user ? $user->fullname : __('The System'); + Comment::create([ + 'type' => 'LOG', + 'user_id' => $user ? $user->id : null, + 'commentable_type' => ProcessRequest::class, + 'commentable_id' => $this->currentTask->process_request_id, + 'subject' => 'Rollback', + 'body' => __(':user rolled back :failed_task_name to :new_task_name', [ + 'user' => $userName, + 'failed_task_name' => $this->currentTask->element_name, + 'new_task_name' => $this->newTask->element_name, + ]), + ]); + } +} diff --git a/resources/js/components/Timeline.vue b/resources/js/components/Timeline.vue index b92ff7d09b..2451ac3595 100644 --- a/resources/js/components/Timeline.vue +++ b/resources/js/components/Timeline.vue @@ -30,6 +30,7 @@ const SubjectIcons = { 'Task Complete': 'far fa-square', 'Gateway': 'far fa-square fa-rotate-45', + 'Rollback': 'fa fa-undo', }; export default { props: ["commentable_id", diff --git a/resources/lang/en.json b/resources/lang/en.json index a6578f83ab..f1d628028b 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1773,5 +1773,11 @@ "Seconds to wait before retrying. Leave empty to use script default. Set to 0 for no retry wait time. This setting is only used when running a script task in a process.": "Seconds to wait before retrying. Leave empty to use script default. Set to 0 for no retry wait time. This setting is only used when running a script task in a process.", "Number of times to retry. Leave empty to use script default. Set to 0 for no retry attempts. This setting is only used when running a script task in a process.": "Number of times to retry. Leave empty to use script default. Set to 0 for no retry attempts. This setting is only used when running a script task in a process.", "View Request": "View Request", - "Execution Error": "Execution Error" + "Execution Error": "Execution Error", + "Task can not be rolled back": "Task can not be rolled back", + ":user rolled back :failed_task_name to :new_task_name": ":user rolled back :failed_task_name to :new_task_name", + "Rollback Request": "Rollback Request", + "Rollback": "Rollback", + "Rollback to task" : "Rollback to task", + "Are you sure you want to rollback to the task @{{name}}? Warning! This request will continue as the current published process version.": "Are you sure you want to rollback to the task @{{name}}? Warning! This request will continue as the current published process version." } diff --git a/resources/views/requests/show.blade.php b/resources/views/requests/show.blade.php index 196a4456cf..ada5a7fd0a 100644 --- a/resources/views/requests/show.blade.php +++ b/resources/views/requests/show.blade.php @@ -313,6 +313,18 @@ class="d-inline-flex pull-left align-items-center" :input-data="requestBy" displ @endif + @if ($eligibleRollbackTask) + @can('rollback', $errorTask) +