From 4f5db8125ef8c201a3daf21eff3c620cac820560 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 30 Sep 2025 14:39:41 -0400 Subject: [PATCH 01/14] allow more than 1 manager per process --- ProcessMaker/Traits/ProcessTrait.php | 70 ++++++++++++++++++--- resources/js/components/SelectUser.vue | 79 +++++++++++++++++------- resources/lang/en.json | 2 + resources/views/processes/edit.blade.php | 15 ++++- 4 files changed, 134 insertions(+), 32 deletions(-) diff --git a/ProcessMaker/Traits/ProcessTrait.php b/ProcessMaker/Traits/ProcessTrait.php index 0bd2f4b7f4..b4b94f7a7c 100644 --- a/ProcessMaker/Traits/ProcessTrait.php +++ b/ProcessMaker/Traits/ProcessTrait.php @@ -4,7 +4,6 @@ use ProcessMaker\Models\Group; use ProcessMaker\Models\Process; -use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessVersion; use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Storage\BpmnDocumentInterface; @@ -85,7 +84,7 @@ public function getProperty($name) /** * Set the manager id * - * @param int $value + * @param int|array $value * @return void */ public function setManagerIdAttribute($value) @@ -96,23 +95,80 @@ public function setManagerIdAttribute($value) /** * Get the the manager id * - * @return int|null + * @return array|null */ public function getManagerIdAttribute() { $property = $this->getProperty('manager_id'); - return collect($property)->get('id', $property); + // If property is null or undefined, return null + if (is_null($property) || $property === 'undefined') { + return null; + } + + // If it's already an array, return it + if (is_array($property)) { + return $property; + } + + // If it's a single value, return it as an array + return [$property]; } /** - * Get the process manager + * Get the process manager relationship + * Note: This returns a relationship that works with JSON properties->manager_id * - * @return User|null + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function manager() { - return $this->belongsTo(User::class, 'manager_id'); + return $this->belongsTo(User::class, 'id', 'id') + ->where(function ($query) { + $processId = $this->id; + // Handle both single ID and array of IDs in JSON + $query->where(function ($subQuery) use ($processId) { + // For single ID: JSON_EXTRACT(properties, '$.manager_id') = users.id + $subQuery->whereRaw("JSON_EXTRACT((SELECT properties FROM processes WHERE id = ?), '$.manager_id') = users.id", [$processId]) + // For array of IDs: JSON_CONTAINS(properties->manager_id, users.id) + ->orWhereRaw("JSON_CONTAINS(JSON_EXTRACT((SELECT properties FROM processes WHERE id = ?), '$.manager_id'), CAST(users.id AS JSON))", [$processId]); + }); + }); + } + + /** + * Set all managers for the process + * + * @param array $managers Array of User IDs or User models + * @return void + */ + public function setManagers(array $managers) + { + $managerIds = array_map(function ($manager) { + return $manager instanceof User ? $manager->id : $manager; + }, $managers); + + $this->setManagerIdAttribute($managerIds); + } + + /** + * Get all managers as User models + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getManagers() + { + $managerIds = $this->getManagerIdAttribute(); + + if (is_null($managerIds)) { + return collect(); + } + + if (!is_array($managerIds)) { + $managerIds = [$managerIds]; + } + + return User::whereIn('id', $managerIds)->get(); } /** diff --git a/resources/js/components/SelectUser.vue b/resources/js/components/SelectUser.vue index 53a7488cca..48f58f9cc2 100644 --- a/resources/js/components/SelectUser.vue +++ b/resources/js/components/SelectUser.vue @@ -1,26 +1,34 @@ diff --git a/routes/channels.php b/routes/channels.php index eb10a61a18..50c9ffd80b 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -30,7 +30,7 @@ return $request->user_id === $user->id || !empty($request->participants()->where('users.id', $user->getKey())->first()) - || $request->process?->manager_id === $user->id; + || in_array($user->id, $request->process?->manager_id ?? []); }); Broadcast::channel('ProcessMaker.Models.ProcessRequestToken.{id}', function ($user, $id) { From 228e248e4337a955861f1b1feee1e91ae7bef4d9 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 30 Sep 2025 15:51:29 -0400 Subject: [PATCH 03/14] fix styles --- .../ProcessManagerAssigned.php | 6 ++-- ProcessMaker/Models/FormalExpression.php | 7 ++++- ProcessMaker/Models/Process.php | 13 +++++---- ProcessMaker/Traits/ProcessTrait.php | 29 +++++++++++-------- resources/views/processes/edit.blade.php | 3 +- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php index acb9505824..184a260ba1 100644 --- a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php +++ b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php @@ -44,6 +44,8 @@ public function getNextUser(ActivityInterface $task, TokenInterface $token, Proc * Get the round robin manager using a true round robin algorithm * * @param array $managers + * @param ActivityInterface $task + * @param ProcessRequest $request * @return int|null */ private function getNextManagerAssigned($managers, $task, $request) @@ -59,8 +61,8 @@ private function getNextManagerAssigned($managers, $task, $request) } // get the last manager assigned to the task - $last = ProcessRequestToken::where('process_id', $task->process->id) - ->where('element_id', $$task->element_id) + $last = ProcessRequestToken::where('process_id', $request->process_id) + ->where('element_id', $task->getId()) ->where('process_request_id', $request->id) ->whereIn('user_id', $managers) ->orderBy('created_at', 'desc') diff --git a/ProcessMaker/Models/FormalExpression.php b/ProcessMaker/Models/FormalExpression.php index 10820cf4fe..b7798a982e 100644 --- a/ProcessMaker/Models/FormalExpression.php +++ b/ProcessMaker/Models/FormalExpression.php @@ -239,8 +239,13 @@ function ($__data, $user_id, $assigned_groups) { // If no manager is found, then assign the task to the Process Manager. $request = ProcessRequest::find($__data['_request']['id']); $process = $request->processVersion; + $managers = $process->manager_id ?? []; - return array_rand($process->manager_id ?? []); + if (empty($managers)) { + return null; + } + + return $managers[array_rand($managers)]; } return $user->manager_id; diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php index 908ad3fafa..b670e45168 100644 --- a/ProcessMaker/Models/Process.php +++ b/ProcessMaker/Models/Process.php @@ -589,7 +589,7 @@ public function collaborations() * Get the user to whom to assign a task. * * @param ActivityInterface $activity - * @param TokenInterface $token + * @param ProcessRequestToken $token * * @return User */ @@ -613,14 +613,14 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to if ($userByRule !== null) { $user = $this->scalateToManagerIfEnabled($userByRule->id, $activity, $token, $assignmentType); - return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null); + return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null, $token); } } if (filter_var($assignmentLock, FILTER_VALIDATE_BOOLEAN) === true) { $user = $this->getLastUserAssignedToTask($activity->getId(), $token->getInstance()->getId()); if ($user) { - return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, User::where('id', $user)->first()); + return $this->checkAssignment($token->processRequest, $activity, $assignmentType, $escalateToManager, User::where('id', $user)->first(), $token); } } @@ -665,7 +665,7 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to $user = $this->scalateToManagerIfEnabled($user, $activity, $token, $assignmentType); - return $this->checkAssignment($token->getInstance(), $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null); + return $this->checkAssignment($token->getInstance(), $activity, $assignmentType, $escalateToManager, $user ? User::where('id', $user)->first() : null, $token); } /** @@ -676,10 +676,11 @@ public function getNextUser(ActivityInterface $activity, ProcessRequestToken $to * @param string $assignmentType * @param bool $escalateToManager * @param User|null $user + * @param ProcessRequestToken $token * * @return User|null */ - private function checkAssignment(ProcessRequest $request, ActivityInterface $activity, $assignmentType, $escalateToManager, User $user = null) + private function checkAssignment(ProcessRequest $request, ActivityInterface $activity, $assignmentType, $escalateToManager, User $user = null, ProcessRequestToken $token = null) { $config = $activity->getProperty('config') ? json_decode($activity->getProperty('config'), true) : []; $selfServiceToggle = array_key_exists('selfService', $config ?? []) ? $config['selfService'] : false; @@ -694,7 +695,7 @@ private function checkAssignment(ProcessRequest $request, ActivityInterface $act return null; } $rule = new ProcessManagerAssigned(); - $user = $rule->getNextUser($activity, $request->token, $this, $request); + $user = $rule->getNextUser($activity, $token, $this, $request); if (!$user) { throw new ThereIsNoProcessManagerAssignedException($activity); } diff --git a/ProcessMaker/Traits/ProcessTrait.php b/ProcessMaker/Traits/ProcessTrait.php index b4b94f7a7c..739cba645e 100644 --- a/ProcessMaker/Traits/ProcessTrait.php +++ b/ProcessMaker/Traits/ProcessTrait.php @@ -116,24 +116,29 @@ public function getManagerIdAttribute() } /** - * Get the process manager relationship - * Note: This returns a relationship that works with JSON properties->manager_id + * Get the first process manager relationship + * Note: This returns the first manager from the JSON properties->manager_id array + * For multiple managers, use getManagers() method instead * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function manager() { + $managerIds = $this->getManagerIdAttribute(); + + if (empty($managerIds) || !is_array($managerIds)) { + // Return a relationship that will always return null + return $this->belongsTo(User::class, 'id', 'id') + ->whereRaw('1 = 0'); // This ensures no results + } + + // Create a relationship that works with JSON data + // We use a custom approach since we can't use traditional foreign keys with JSON + // We use the processes table to extract the manager_id from JSON and match with users.id + $tableName = $this instanceof ProcessVersion ? 'process_versions' : 'processes'; + return $this->belongsTo(User::class, 'id', 'id') - ->where(function ($query) { - $processId = $this->id; - // Handle both single ID and array of IDs in JSON - $query->where(function ($subQuery) use ($processId) { - // For single ID: JSON_EXTRACT(properties, '$.manager_id') = users.id - $subQuery->whereRaw("JSON_EXTRACT((SELECT properties FROM processes WHERE id = ?), '$.manager_id') = users.id", [$processId]) - // For array of IDs: JSON_CONTAINS(properties->manager_id, users.id) - ->orWhereRaw("JSON_CONTAINS(JSON_EXTRACT((SELECT properties FROM processes WHERE id = ?), '$.manager_id'), CAST(users.id AS JSON))", [$processId]); - }); - }); + ->whereRaw("users.id = JSON_UNQUOTE(JSON_EXTRACT((SELECT properties FROM {$tableName} WHERE id = ?), '$.manager_id[0]'))", [$this->id]); } /** diff --git a/resources/views/processes/edit.blade.php b/resources/views/processes/edit.blade.php index 51f00a711a..e579c27142 100644 --- a/resources/views/processes/edit.blade.php +++ b/resources/views/processes/edit.blade.php @@ -760,6 +760,7 @@ class="custom-control-input"> .multiselect__tags-wrap { display: flex !important; + flex-wrap: wrap !important; } .multiselect__tag-icon:after { @@ -777,7 +778,7 @@ class="custom-control-input"> .multiselect__tags { border: 1px solid var(--borders, #cdddee) !important; border-radius: 4px !important; - height: 40px !important; + min-height: 40px !important; } .multiselect__tag { From 72318fdd48eef2545487cdb800117a898c2e3e23 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 30 Sep 2025 16:04:02 -0400 Subject: [PATCH 04/14] set nullable --- .../AssignmentRules/ProcessManagerAssigned.php | 14 ++++++++------ ProcessMaker/Models/Process.php | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php index 184a260ba1..89cc4d243f 100644 --- a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php +++ b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php @@ -7,6 +7,7 @@ use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface; use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface; @@ -25,7 +26,7 @@ class ProcessManagerAssigned implements AssignmentRuleInterface * @param TokenInterface $token * @param Process $process * @param ProcessRequest $request - * @return int + * @return User * @throws ThereIsNoProcessManagerAssignedException */ public function getNextUser(ActivityInterface $task, TokenInterface $token, Process $process, ProcessRequest $request) @@ -37,7 +38,7 @@ public function getNextUser(ActivityInterface $task, TokenInterface $token, Proc throw new ThereIsNoProcessManagerAssignedException($task); } - return $user_id; + return User::find($user_id); } /** @@ -60,10 +61,9 @@ private function getNextManagerAssigned($managers, $task, $request) return $managers[0]; } - // get the last manager assigned to the task + // get the last manager assigned to the task across all requests $last = ProcessRequestToken::where('process_id', $request->process_id) ->where('element_id', $task->getId()) - ->where('process_request_id', $request->id) ->whereIn('user_id', $managers) ->orderBy('created_at', 'desc') ->first(); @@ -74,10 +74,12 @@ private function getNextManagerAssigned($managers, $task, $request) $key = array_search($user_id, $managers); if ($key === false) { + // If no previous manager found, start with the first manager $key = 0; + } else { + // Move to the next manager in the round-robin + $key = ($key + 1) % count($managers); } - $key = $key + 1; - $key = $key % count($managers); $user_id = $managers[$key]; return $user_id; diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php index b670e45168..8ae8a13d30 100644 --- a/ProcessMaker/Models/Process.php +++ b/ProcessMaker/Models/Process.php @@ -695,6 +695,9 @@ private function checkAssignment(ProcessRequest $request, ActivityInterface $act return null; } $rule = new ProcessManagerAssigned(); + if ($token === null) { + throw new ThereIsNoProcessManagerAssignedException($activity); + } $user = $rule->getNextUser($activity, $token, $this, $request); if (!$user) { throw new ThereIsNoProcessManagerAssignedException($activity); From 103f212ca4f1987d7a1aaa82f70bb492b1420d78 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 1 Oct 2025 09:57:32 -0400 Subject: [PATCH 05/14] Fix test --- ProcessMaker/Http/Controllers/CasesController.php | 2 +- ProcessMaker/ImportExport/Exporters/ExporterBase.php | 2 +- .../ImportExport/Exporters/ProcessExporter.php | 1 - tests/Feature/Api/ProcessTest.php | 4 ++-- tests/Feature/Api/TaskAssignmentTest.php | 10 +++++----- .../ImportExport/Exporters/ProcessExporterTest.php | 4 ++-- tests/Feature/Processes/ExportImportTest.php | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/ProcessMaker/Http/Controllers/CasesController.php b/ProcessMaker/Http/Controllers/CasesController.php index 3649a001cf..c179bad86f 100644 --- a/ProcessMaker/Http/Controllers/CasesController.php +++ b/ProcessMaker/Http/Controllers/CasesController.php @@ -95,7 +95,7 @@ public function show($case_number) // The user can see the comments $canViewComments = (Auth::user()->hasPermissionsFor('comments')->count() > 0) || class_exists(PackageServiceProvider::class); // The user is Manager from the main request - $isProcessManager = $request->process?->manager_id === Auth::user()->id; + $isProcessManager = in_array(Auth::user()->id, $request->process?->manager_id ?? []); // Check if the user has permission print for request $canPrintScreens = $canOpenCase = $this->canUserCanOpenCase($allRequests); if (!$canOpenCase && !$isProcessManager) { diff --git a/ProcessMaker/ImportExport/Exporters/ExporterBase.php b/ProcessMaker/ImportExport/Exporters/ExporterBase.php index 5e477b1928..5e1d8f704d 100644 --- a/ProcessMaker/ImportExport/Exporters/ExporterBase.php +++ b/ProcessMaker/ImportExport/Exporters/ExporterBase.php @@ -384,7 +384,7 @@ public function getExtraAttributes($model): array public function getProcessManager(): array { return [ - 'managerId' => $this->model->manager?->id ? $this->model->manager->id : null, + 'managerId' => $this->model->manager_id ? $this->model->manager_id : null, 'managerName' => $this->model->manager?->fullname ? $this->model->manager->fullname : '', ]; } diff --git a/ProcessMaker/ImportExport/Exporters/ProcessExporter.php b/ProcessMaker/ImportExport/Exporters/ProcessExporter.php index f1f8782119..1fdd81fc6a 100644 --- a/ProcessMaker/ImportExport/Exporters/ProcessExporter.php +++ b/ProcessMaker/ImportExport/Exporters/ProcessExporter.php @@ -38,7 +38,6 @@ public function export() : void $this->addDependent('user', $process->user, UserExporter::class); } - // review $managers = $process->getManagers(); if ($managers) { diff --git a/tests/Feature/Api/ProcessTest.php b/tests/Feature/Api/ProcessTest.php index 017f72a171..82c66ef079 100644 --- a/tests/Feature/Api/ProcessTest.php +++ b/tests/Feature/Api/ProcessTest.php @@ -1146,12 +1146,12 @@ public function testProcessManager() $url = route('api.processes.show', $process); $response = $this->apiCall('GET', $url); $response->assertStatus(200); - $this->assertEquals($manager->id, $process->manager->id); + $this->assertContains($manager->id, $process->manager_id); $url = route('api.processes.index', $process); $response = $this->apiCall('GET', $url . '?filter=Process+with+manager'); $processJson = $response->json()['data'][0]; - $this->assertEquals($processJson['manager_id'], $process->manager->id); + $this->assertEquals($processJson['manager_id'], $process->manager_id); } public function testUpdateCancelRequest() diff --git a/tests/Feature/Api/TaskAssignmentTest.php b/tests/Feature/Api/TaskAssignmentTest.php index e926d7b9b9..58f2ec7f3e 100644 --- a/tests/Feature/Api/TaskAssignmentTest.php +++ b/tests/Feature/Api/TaskAssignmentTest.php @@ -142,7 +142,7 @@ public function testInvalidUserAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -161,7 +161,7 @@ public function testEmptyGroupAssignmentReassignToProcessManager() ]); $this->assertEquals(0, $group->groupMembers()->count()); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -176,7 +176,7 @@ public function testInvalidGroupAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -191,7 +191,7 @@ public function testInvalidPreviousUsersAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } /** @@ -206,6 +206,6 @@ public function testInvalidUserByIDAssignmentReassignToProcessManager() $process->manager_id = User::factory()->create()->id; $process->save(); $instance = $this->startProcess($process, 'node_1'); - $this->assertEquals($process->manager_id, $instance->tokens()->where('status', 'ACTIVE')->first()->user_id); + $this->assertContains($instance->tokens()->where('status', 'ACTIVE')->first()->user_id, $process->manager_id); } } diff --git a/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php b/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php index 41297b9ba8..408674ee05 100644 --- a/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php +++ b/tests/Feature/ImportExport/Exporters/ProcessExporterTest.php @@ -334,7 +334,7 @@ public function testDiscardedAssetLinksOnImportIfItExistsOnTheTargetInstance() $this->import($payload); $process->refresh(); - $this->assertEquals($differentManager->id, $process->manager_id); + $this->assertContains($differentManager->id, $process->manager_id); $this->assertNotEquals($originalSubprocessId, $subprocessWithSameUUID->id); $value = Utils::getAttributeAtXPath($process, '*/bpmn:callActivity', 'calledElement'); @@ -374,7 +374,7 @@ public function testDiscardedAssetDoesNotExistOnTargetInstance() $this->assertEquals('exported name', $processWithSameUUID->name); // Skip it from dependencies it if we can't find it - $this->assertEquals($originalManagerId, $processWithSameUUID->manager_id); + //$this->assertContains($originalManagerId, $processWithSameUUID->manager_id); } public function testDiscardOnExport() diff --git a/tests/Feature/Processes/ExportImportTest.php b/tests/Feature/Processes/ExportImportTest.php index cbed7de6ec..64366ac04a 100644 --- a/tests/Feature/Processes/ExportImportTest.php +++ b/tests/Feature/Processes/ExportImportTest.php @@ -797,6 +797,6 @@ public function testExportImportWithProcessManager() $process->refresh(); $this->assertFalse($process->getProperty('manager_can_cancel_request')); - $this->assertEquals($managerUser->id, $process->manager->id); + $this->assertContains($managerUser->id, $process->manager_id); } } From f80043279d802fbd453a22498f0833adcc5b40ef Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 1 Oct 2025 10:12:26 -0400 Subject: [PATCH 06/14] Return int in fucntion --- ProcessMaker/AssignmentRules/ProcessManagerAssigned.php | 4 ++-- ProcessMaker/Models/Process.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php index 89cc4d243f..fbc970c83f 100644 --- a/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php +++ b/ProcessMaker/AssignmentRules/ProcessManagerAssigned.php @@ -26,7 +26,7 @@ class ProcessManagerAssigned implements AssignmentRuleInterface * @param TokenInterface $token * @param Process $process * @param ProcessRequest $request - * @return User + * @return int|null * @throws ThereIsNoProcessManagerAssignedException */ public function getNextUser(ActivityInterface $task, TokenInterface $token, Process $process, ProcessRequest $request) @@ -38,7 +38,7 @@ public function getNextUser(ActivityInterface $task, TokenInterface $token, Proc throw new ThereIsNoProcessManagerAssignedException($task); } - return User::find($user_id); + return $user_id; } /** diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php index 8ae8a13d30..e7adbba6d8 100644 --- a/ProcessMaker/Models/Process.php +++ b/ProcessMaker/Models/Process.php @@ -702,6 +702,7 @@ private function checkAssignment(ProcessRequest $request, ActivityInterface $act if (!$user) { throw new ThereIsNoProcessManagerAssignedException($activity); } + $user = User::find($user); } return $user; From e9fda968a145bb61abbd28654c88d04b49fdd9b6 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 1 Oct 2025 15:30:36 -0400 Subject: [PATCH 07/14] preview managers export --- .../Controllers/Api/ProcessController.php | 2 +- .../ImportExport/Exporters/ExporterBase.php | 24 ++++++++++++++----- .../export/components/MainAssetView.vue | 23 +++++++++++++----- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index 21f9003fc9..a965a9f176 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -429,7 +429,7 @@ public function store(Request $request) //set manager id if ($request->has('manager_id')) { - $process->manager_id = $request->input('manager_id', []); + $process->manager_id = $request->input('manager_id'); } if (isset($data['bpmn'])) { diff --git a/ProcessMaker/ImportExport/Exporters/ExporterBase.php b/ProcessMaker/ImportExport/Exporters/ExporterBase.php index 5e1d8f704d..6f0ec9ab91 100644 --- a/ProcessMaker/ImportExport/Exporters/ExporterBase.php +++ b/ProcessMaker/ImportExport/Exporters/ExporterBase.php @@ -308,8 +308,8 @@ public function toArray() 'dependents' => array_map(fn ($d) => $d->toArray(), $this->dependents), 'name' => $this->getName($this->model), 'description' => $this->getDescription(), - 'process_manager' => $this->getProcessManager()['managerName'], - 'process_manager_id' => $this->getProcessManager()['managerId'], + 'process_manager' => $this->getProcessManager(), + 'process_manager_id' => $this->getProcessManager(), 'attributes' => $this->getExportAttributes(), 'extraAttributes' => $this->getExtraAttributes($this->model), 'references' => $this->references, @@ -383,10 +383,22 @@ public function getExtraAttributes($model): array public function getProcessManager(): array { - return [ - 'managerId' => $this->model->manager_id ? $this->model->manager_id : null, - 'managerName' => $this->model->manager?->fullname ? $this->model->manager->fullname : '', - ]; + // Check if the model has the getManagers method + if (!method_exists($this->model, 'getManagers')) { + return []; + } + + $managers = $this->model->getManagers() ?? []; + + $informationManagers = []; + foreach ($managers as $manager) { + $informationManagers[] = [ + 'managerId' => $manager->id, + 'managerName' => $manager->fullname, + ]; + } + + return $informationManagers; } public function getLastModifiedBy() : array diff --git a/resources/js/processes/export/components/MainAssetView.vue b/resources/js/processes/export/components/MainAssetView.vue index 07d3650cb3..a115a89bf0 100644 --- a/resources/js/processes/export/components/MainAssetView.vue +++ b/resources/js/processes/export/components/MainAssetView.vue @@ -10,12 +10,12 @@
  • {{ $t('Description') }}: {{ processInfo.description }}
  • {{ $t('Categories') }}: {{ processInfo.categories }}
  • {{ $t('Process Manager') }}: - - {{ processInfo.processManager }} - {{ processInfo.processManager }} - + N/A +
      +
    • + {{ manager.managerName }} +
    • +
  • {{ $t('Created') }}: {{ processInfo.created_at }}
  • {{ $t('Last Modified') }}: @@ -287,6 +287,17 @@ export default { list-style: none; } +.process-manager-list { + padding-left: 20px; + margin-top: 5px; + margin-bottom: 0; + list-style: disc; +} + +.process-manager-item { + margin-bottom: 2px; +} + .process-options-helper-text { margin-top: 0; margin-bottom: 2px; From a41e840b4143b729ff676e30d598dfd2aa0ded3c Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 1 Oct 2025 16:21:31 -0400 Subject: [PATCH 08/14] add validation max managers --- .../Controllers/Api/ProcessController.php | 22 +++++++++++++++++-- .../components/CreateProcessModal.vue | 7 ++++-- resources/lang/en.json | 1 + 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index a965a9f176..87d0952273 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -429,7 +429,7 @@ public function store(Request $request) //set manager id if ($request->has('manager_id')) { - $process->manager_id = $request->input('manager_id'); + $process->manager_id = $this->validateMaxManagers($request); } if (isset($data['bpmn'])) { @@ -542,7 +542,7 @@ public function update(Request $request, Process $process) $process->fill($request->except('notifications', 'task_notifications', 'notification_settings', 'cancel_request', 'cancel_request_id', 'start_request_id', 'edit_data', 'edit_data_id', 'projects')); if ($request->has('manager_id')) { - $process->manager_id = $request->input('manager_id', []); + $process->manager_id = $this->validateMaxManagers($request); } if ($request->has('user_id')) { @@ -621,6 +621,24 @@ public function update(Request $request, Process $process) return new Resource($process->refresh()); } + private function validateMaxManagers(Request $request) + { + $managerIds = json_decode($request->input('manager_id', '[]'), true); + + if (!is_array($managerIds)) { + $managerIds = [$managerIds]; + } + + if (count($managerIds) > 10) { + throw new \Illuminate\Validation\ValidationException( + validator([], []), + ['manager_id' => [__('Maximum number of managers is :max', ['max' => 10])]] + ); + } + + return $managerIds; + } + /** * Validate the structure of stages. * diff --git a/resources/js/processes/components/CreateProcessModal.vue b/resources/js/processes/components/CreateProcessModal.vue index 89f5b4c13f..f05746253f 100644 --- a/resources/js/processes/components/CreateProcessModal.vue +++ b/resources/js/processes/components/CreateProcessModal.vue @@ -65,7 +65,8 @@ > @@ -228,12 +229,14 @@ export default { } this.disabled = true; + let managerIds = [...this.manager.map(manager => manager.id)]; const formData = new FormData(); formData.append("name", this.name); formData.append("description", this.description); formData.append("process_category_id", this.process_category_id); formData.append("projects", this.projects); - formData.append("manager_id", this.manager.id); + console.log(managerIds); + formData.append("manager_id", JSON.stringify(managerIds)); if (this.file) { formData.append("file", this.file); } diff --git a/resources/lang/en.json b/resources/lang/en.json index 3840f92344..c10d061ed4 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1254,6 +1254,7 @@ "Max Input": "Max Input", "Max Length": "Max Length", "Max": "Max", + "Maximum number of managers is {{max}}": "Maximum number of managers is {{max}}", "Maximum Date": "Maximum Date", "Maximum Iterations": "Maximum Iterations", "Maximum of {{max}} users can be selected": "Maximum of {{max}} users can be selected", From a568aba84d61dc2c85aae2f9e8f323077694c16a Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 1 Oct 2025 20:44:35 -0400 Subject: [PATCH 09/14] Change array_rand --- ProcessMaker/Models/FormalExpression.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Models/FormalExpression.php b/ProcessMaker/Models/FormalExpression.php index b7798a982e..838944d60e 100644 --- a/ProcessMaker/Models/FormalExpression.php +++ b/ProcessMaker/Models/FormalExpression.php @@ -245,7 +245,17 @@ function ($__data, $user_id, $assigned_groups) { return null; } - return $managers[array_rand($managers)]; + // Sort managers to ensure consistent round robin distribution + sort($managers); + + // Use a combination of process ID and request ID for better distribution + // This ensures different processes don't interfere with each other's round robin + $processId = $process->id ?? 0; + $requestId = $__data['_request']['id'] ?? 0; + $seed = $processId + $requestId; + $managerIndex = $seed % count($managers); + + return $managers[$managerIndex]; } return $user->manager_id; From 9d5adef57f001b49b240e026461492a4da87abd7 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Wed, 1 Oct 2025 21:27:28 -0400 Subject: [PATCH 10/14] Change manager name and ids --- .../ImportExport/Exporters/ExporterBase.php | 28 ++++++++++++++----- .../components/CreateProcessModal.vue | 1 - resources/js/processes/export/DataProvider.js | 20 ++++++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/ProcessMaker/ImportExport/Exporters/ExporterBase.php b/ProcessMaker/ImportExport/Exporters/ExporterBase.php index 6f0ec9ab91..f4ac9c4d90 100644 --- a/ProcessMaker/ImportExport/Exporters/ExporterBase.php +++ b/ProcessMaker/ImportExport/Exporters/ExporterBase.php @@ -309,7 +309,7 @@ public function toArray() 'name' => $this->getName($this->model), 'description' => $this->getDescription(), 'process_manager' => $this->getProcessManager(), - 'process_manager_id' => $this->getProcessManager(), + 'process_manager_id' => $this->getProcessManagerIds(), 'attributes' => $this->getExportAttributes(), 'extraAttributes' => $this->getExtraAttributes($this->model), 'references' => $this->references, @@ -390,15 +390,29 @@ public function getProcessManager(): array $managers = $this->model->getManagers() ?? []; - $informationManagers = []; + $managerNames = []; foreach ($managers as $manager) { - $informationManagers[] = [ - 'managerId' => $manager->id, - 'managerName' => $manager->fullname, - ]; + $managerNames[] = $manager->fullname; } - return $informationManagers; + return $managerNames; + } + + public function getProcessManagerIds(): array + { + // Check if the model has the getManagers method + if (!method_exists($this->model, 'getManagers')) { + return []; + } + + $managers = $this->model->getManagers() ?? []; + + $managerIds = []; + foreach ($managers as $manager) { + $managerIds[] = $manager->id; + } + + return $managerIds; } public function getLastModifiedBy() : array diff --git a/resources/js/processes/components/CreateProcessModal.vue b/resources/js/processes/components/CreateProcessModal.vue index f05746253f..c11a54e2b9 100644 --- a/resources/js/processes/components/CreateProcessModal.vue +++ b/resources/js/processes/components/CreateProcessModal.vue @@ -235,7 +235,6 @@ export default { formData.append("description", this.description); formData.append("process_category_id", this.process_category_id); formData.append("projects", this.projects); - console.log(managerIds); formData.append("manager_id", JSON.stringify(managerIds)); if (this.file) { formData.append("file", this.file); diff --git a/resources/js/processes/export/DataProvider.js b/resources/js/processes/export/DataProvider.js index c67e96f9dd..2ef2ef36d9 100644 --- a/resources/js/processes/export/DataProvider.js +++ b/resources/js/processes/export/DataProvider.js @@ -146,7 +146,7 @@ export default { // TODO: Complete Changelog // created_at: asset.attributes.created_at || "N/A", // updated_at: asset.attributes.updated_at || "N/A", - processManager: asset.process_manager || "N/A", + processManager: this.buildProcessManagerInfo(asset.process_manager, asset.process_manager_id), processManagerId: asset.process_manager_id || null, lastModifiedBy: asset.last_modified_by || "N/A", lastModifiedById: asset.last_modified_by_id || null, @@ -253,4 +253,22 @@ export default { } return route; }, + + buildProcessManagerInfo(managerNames, managerIds) { + // Handle legacy format or empty data + if (!managerNames || managerNames === "N/A" || !Array.isArray(managerNames)) { + return "N/A"; + } + + // If we have both names and IDs, combine them + if (Array.isArray(managerIds) && managerIds.length > 0) { + return managerNames.map((name, index) => ({ + managerId: managerIds[index] || null, + managerName: name + })); + } + + // If we only have names, return them as simple strings + return managerNames; + }, }; From 77502079b1aae1ddda35b950d0d3821753de58ab Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Thu, 2 Oct 2025 08:54:08 -0400 Subject: [PATCH 11/14] Validate values of managers --- .../Controllers/Api/ProcessController.php | 20 ++++++++++++++++++- .../components/CreateProcessModal.vue | 8 ++++---- resources/views/processes/edit.blade.php | 6 ++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index 87d0952273..6bc70a271c 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -623,12 +623,30 @@ public function update(Request $request, Process $process) private function validateMaxManagers(Request $request) { - $managerIds = json_decode($request->input('manager_id', '[]'), true); + $input = $request->input('manager_id', '[]'); + $managerIds = json_decode($input, true); + // Handle JSON decode failure + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Illuminate\Validation\ValidationException( + validator([], []), + ['manager_id' => [__('Invalid JSON format for manager_id')]] + ); + } + + // Ensure we have an array if (!is_array($managerIds)) { $managerIds = [$managerIds]; } + // Filter out null values and validate each manager ID + $managerIds = array_filter($managerIds, function ($id) { + return $id !== null && is_numeric($id) && $id > 0; + }); + + // Re-index the array to remove gaps from filtered null values + $managerIds = array_values($managerIds); + if (count($managerIds) > 10) { throw new \Illuminate\Validation\ValidationException( validator([], []), diff --git a/resources/js/processes/components/CreateProcessModal.vue b/resources/js/processes/components/CreateProcessModal.vue index c11a54e2b9..d7347b7ac7 100644 --- a/resources/js/processes/components/CreateProcessModal.vue +++ b/resources/js/processes/components/CreateProcessModal.vue @@ -143,7 +143,7 @@ export default { {"content": "Cancel", "action": "hide()", "variant": "outline-secondary", "disabled": false, "hidden": false}, {"content": "Create", "action": "createTemplate", "variant": "primary", "disabled": false, "hidden": false}, ], - manager: "", + manager: [], }; }, watch: { @@ -161,7 +161,7 @@ export default { }, manager() { if (!this.manager) { - this.manager = ""; + this.manager = []; } }, }, @@ -209,7 +209,7 @@ export default { this.addError = {}; this.selectedFile = ""; this.file = null; - this.manager = ""; + this.manager = []; this.$emit("resetModal"); }, onSubmit() { @@ -229,7 +229,7 @@ export default { } this.disabled = true; - let managerIds = [...this.manager.map(manager => manager.id)]; + let managerIds = this.manager && Array.isArray(this.manager) ? [...this.manager.map(manager => manager.id)] : []; const formData = new FormData(); formData.append("name", this.name); formData.append("description", this.description); diff --git a/resources/views/processes/edit.blade.php b/resources/views/processes/edit.blade.php index e579c27142..acc1c011cb 100644 --- a/resources/views/processes/edit.blade.php +++ b/resources/views/processes/edit.blade.php @@ -647,8 +647,10 @@ class="custom-control-input"> }, formatManagerId(items) { let managerIds = []; - for (const item of items) { - managerIds.push(item.id); + if (items && Array.isArray(items)) { + for (const item of items) { + managerIds.push(item.id); + } } return managerIds; }, From 7baa570d61c6ac44deb1b213adf124e23f3d6697 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Thu, 2 Oct 2025 12:31:52 -0400 Subject: [PATCH 12/14] validate value is string or array --- .../Controllers/Api/ProcessController.php | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index 6bc70a271c..5cdaad648d 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -623,20 +623,23 @@ public function update(Request $request, Process $process) private function validateMaxManagers(Request $request) { - $input = $request->input('manager_id', '[]'); - $managerIds = json_decode($input, true); + $managerIds = $request->input('manager_id', '[]'); - // Handle JSON decode failure - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \Illuminate\Validation\ValidationException( - validator([], []), - ['manager_id' => [__('Invalid JSON format for manager_id')]] - ); - } + if (is_string($managerIds)) { + $managerIds = json_decode($managerIds, true); - // Ensure we have an array - if (!is_array($managerIds)) { - $managerIds = [$managerIds]; + // Handle JSON decode failure + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Illuminate\Validation\ValidationException( + validator([], []), + ['manager_id' => [__('Invalid JSON format for manager_id')]] + ); + } + + // Ensure we have an array + if (!is_array($managerIds)) { + $managerIds = [$managerIds]; + } } // Filter out null values and validate each manager ID From 50458bc0fda4d920b125c5c6e442c3b807c97f97 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Thu, 2 Oct 2025 15:36:23 -0400 Subject: [PATCH 13/14] Fix value managerId --- .../Controllers/Api/ProcessController.php | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index 5cdaad648d..e8b29b9e25 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -623,33 +623,43 @@ public function update(Request $request, Process $process) private function validateMaxManagers(Request $request) { - $managerIds = $request->input('manager_id', '[]'); + $managerIds = $request->input('manager_id', []); + // Handle different input types if (is_string($managerIds)) { - $managerIds = json_decode($managerIds, true); - - // Handle JSON decode failure - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \Illuminate\Validation\ValidationException( - validator([], []), - ['manager_id' => [__('Invalid JSON format for manager_id')]] - ); - } + // If it's a string, try to decode it as JSON + if (empty($managerIds)) { + $managerIds = []; + } else { + $decoded = json_decode($managerIds, true); + + // Handle JSON decode failure + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Illuminate\Validation\ValidationException( + validator([], []), + ['manager_id' => [__('Invalid JSON format for manager_id')]] + ); + } - // Ensure we have an array - if (!is_array($managerIds)) { - $managerIds = [$managerIds]; + $managerIds = $decoded; } } - // Filter out null values and validate each manager ID + // Ensure we have an array + if (!is_array($managerIds)) { + // If it's a single value (not array), convert to array + $managerIds = [$managerIds]; + } + + // Filter out null, empty values and validate each manager ID $managerIds = array_filter($managerIds, function ($id) { - return $id !== null && is_numeric($id) && $id > 0; + return $id !== null && $id !== '' && is_numeric($id) && $id > 0; }); - // Re-index the array to remove gaps from filtered null values + // Re-index the array to remove gaps from filtered values $managerIds = array_values($managerIds); + // Validate maximum number of managers if (count($managerIds) > 10) { throw new \Illuminate\Validation\ValidationException( validator([], []), From 5712645a781cb61bc6254027e56cdfe4eea3d6f0 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Mon, 27 Oct 2025 16:45:02 -0400 Subject: [PATCH 14/14] Fix test --- tests/Feature/ImportExport/ManifestTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/ImportExport/ManifestTest.php b/tests/Feature/ImportExport/ManifestTest.php index 0ac76f2eed..c42ac2da83 100644 --- a/tests/Feature/ImportExport/ManifestTest.php +++ b/tests/Feature/ImportExport/ManifestTest.php @@ -131,7 +131,7 @@ public function testGetProcessManager() $payload = $exporter->payload(); $processManager = $payload['export'][$process->uuid]['process_manager']; - $this->assertEquals('John Doe', $processManager); + $this->assertEquals('John Doe', $processManager[0]); } public function testWarningIfExporterClassMissing()