Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ProcessMaker/Http/Controllers/Api/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
11 changes: 10 additions & 1 deletion ProcessMaker/Http/Controllers/RequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand All @@ -145,7 +152,9 @@ public function show(ProcessRequest $request, Media $mediaItems)
'canPrintScreens',
'screenRequested',
'addons',
'isProcessManager'
'isProcessManager',
'eligibleRollbackTask',
'errorTask',
));
}

Expand Down
14 changes: 14 additions & 0 deletions ProcessMaker/Policies/ProcessRequestTokenPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
206 changes: 206 additions & 0 deletions ProcessMaker/RollbackProcessRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

namespace ProcessMaker;

use Illuminate\Support\Facades\Auth;
use ProcessMaker\Events\ActivityAssigned;
use ProcessMaker\Facades\WorkflowManager;
use ProcessMaker\Models\Comment;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Repositories\BpmnDocument;

class RollbackProcessRequest
{
/**
* Element types that can be rolled-back to
*
* @var array
*/
private $eligibleTypes = ['task', 'scriptTask', 'serviceTask'];

/**
* The current task (that is presumably failing or has some problem)\
*
* @var ProcessRequestToken
*/
private $currentTask;

/**
* A previously completed task that we want to rollback to.
*
* @var ProcessRequestToken
*/
private $rollbackToTask;

/**
* A copy of the rollbackToTask that will be the new active task.
*
* @var ProcessRequestToken
*/
private $newTask;

/**
* BPMN Definitions for the process
*
* @var ProcessMaker\Repositories\BpmnDocument
*/
private $processDefinitions;

/**
* Return the last task if its status was failing.
* We may need to update this logic later.
*
* @param ProcessRequest $processRequest
*
* @return ProcessRequestToken|null
*/
public function getErrorTask(ProcessRequest $processRequest) : ?ProcessRequestToken
{
$lastTask = $processRequest->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,
]),
]);
}
}
1 change: 1 addition & 0 deletions resources/js/components/Timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
24 changes: 24 additions & 0 deletions resources/views/requests/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,18 @@ class="d-inline-flex pull-left align-items-center" :input-data="requestBy" displ
</button>
</li>
@endif
@if ($eligibleRollbackTask)
@can('rollback', $errorTask)
<li class="list-group-item">
<h5>{{ __('Rollback Request') }}</h5>
<button id="retryRequestButton" type="button" class="btn btn-outline-info btn-block"
data-toggle="modal" @click="rollback({{ $errorTask->id }}, '{{ $eligibleRollbackTask->element_name }}')">
<i class="fas fa-undo"></i> {{ __('Rollback') }}
</button>
<small>{{ __('Rollback to task') }}: <b>{{ $eligibleRollbackTask->element_name }}</b> ({{ $eligibleRollbackTask->element_id }})</small>
</li>
@endcan
@endif
@if ($request->parentRequest)
<li class="list-group-item">
<h5>{{ __('Parent Request') }}</h5>
Expand Down Expand Up @@ -701,6 +713,18 @@ classStatusCard() {
apiRequest
);
},
rollback(errorTaskId, rollbackToName) {
ProcessMaker.confirmModal(
this.$t('Confirm'),
this.$t('Are you sure you want to rollback to the task @{{name}}? Warning! This request will continue as the current published process version.', { name: rollbackToName }),
'default',
() => {
ProcessMaker.apiClient.post(`tasks/${errorTaskId}/rollback`).then(response => {
location.reload();
});
}
)
},
getConfigurationComments() {
if (this.canViewComments) {
const commentsPackage = 'comment-editor' in Vue.options.components;
Expand Down
2 changes: 2 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
Route::get('tasks', [TaskController::class, 'index'])->name('tasks.index'); // Already filtered in controller
Route::get('tasks/{task}', [TaskController::class, 'show'])->name('tasks.show')->middleware('can:view,task');
Route::get('tasks/{task}/screens/{screen}', [TaskController::class, 'getScreen'])->name('tasks.get_screen')->middleware('can:viewScreen,task,screen');
Route::get('tasks/{task}/eligibleRollbackTask', [TaskController::class, 'eligibleRollbackTask'])->name('tasks.eligible_rollback_task')->middleware('can:rollback,task');
Route::post('tasks/{task}/rollback', [TaskController::class, 'rollbackTask'])->name('tasks.rollback_task')->middleware('can:rollback,task');

// Requests
Route::get('requests', [ProcessRequestController::class, 'index'])->name('requests.index'); // Already filtered in controller
Expand Down
Loading