From ddc09863b304105f116b9bcac8b5090224b5d281 Mon Sep 17 00:00:00 2001 From: CarliPinell Date: Thu, 5 Dec 2024 15:42:14 -0400 Subject: [PATCH 01/51] Fix sort in script executor --- .../Http/Controllers/Api/ScriptExecutorController.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php index e625ac01d8..db4d8794fe 100644 --- a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php +++ b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php @@ -59,7 +59,15 @@ public function index(Request $request) { $this->checkAuth($request); - return new ApiCollection(ScriptExecutor::nonSystem()->get()); + $query = ScriptExecutor::nonSystem(); + + if ($request->has('order_by')) { + $order_by = $request->input('order_by'); + $order_direction = $request->input('order_direction', 'ASC'); + $query->orderBy($order_by, $order_direction); + } + + return new ApiCollection($query->get()); } /** From c6ba4e604c335ea6bfbbc81da82316fe724c8032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Bascop=C3=A9?= Date: Tue, 24 Dec 2024 12:15:37 -0400 Subject: [PATCH 02/51] changed the navbar-icon when expended for mobile --- resources/js/app-layout.js | 4 ++++ resources/views/layouts/navbar.blade.php | 29 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/resources/js/app-layout.js b/resources/js/app-layout.js index c56cf4152d..abf887d29b 100644 --- a/resources/js/app-layout.js +++ b/resources/js/app-layout.js @@ -141,6 +141,7 @@ window.ProcessMaker.navbar = new Vue({ taskTitle: "", isMobile: false, isMobileDevice: window.ProcessMaker.mobileApp, + isNavbarExpanded: false, }; }, watch: { @@ -214,6 +215,9 @@ window.ProcessMaker.navbar = new Vue({ onResize() { this.isMobile = window.innerWidth < 992; }, + toggleNavbar() { + this.isNavbarExpanded = !this.isNavbarExpanded; + }, }, }); diff --git a/resources/views/layouts/navbar.blade.php b/resources/views/layouts/navbar.blade.php index 5ce2791f16..e03ba03ed5 100644 --- a/resources/views/layouts/navbar.blade.php +++ b/resources/views/layouts/navbar.blade.php @@ -5,7 +5,13 @@ $loginLogo = \ProcessMaker\Models\Setting::getLogin(); @endphp - + + +
@@ -168,6 +174,27 @@ class="ml-2"
@endsection 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 13/51] 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 15/51] 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 16/51] 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 17/51] 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 18/51] 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 19/51] 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 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 25/51] 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 c18338913cb1138321e58b87250777e8ec9913f6 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Fri, 3 Oct 2025 14:21:34 -0400 Subject: [PATCH 26/51] Add IsManager middleware for enhanced access control --- ProcessMaker/Http/Kernel.php | 1 + ProcessMaker/Http/Middleware/IsManager.php | 195 +++++++++++++++++++++ routes/api.php | 4 +- 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 ProcessMaker/Http/Middleware/IsManager.php diff --git a/ProcessMaker/Http/Kernel.php b/ProcessMaker/Http/Kernel.php index f8a5888430..66cf5f2144 100644 --- a/ProcessMaker/Http/Kernel.php +++ b/ProcessMaker/Http/Kernel.php @@ -91,6 +91,7 @@ class Kernel extends HttpKernel 'session_kill' => Middleware\SessionControlKill::class, 'no-cache' => Middleware\NoCache::class, 'admin' => Middleware\IsAdmin::class, + 'manager' => Middleware\IsManager::class, 'etag' => Middleware\Etag\HandleEtag::class, 'file_size_check' => Middleware\FileSizeCheck::class, ]; diff --git a/ProcessMaker/Http/Middleware/IsManager.php b/ProcessMaker/Http/Middleware/IsManager.php new file mode 100644 index 0000000000..638072f2be --- /dev/null +++ b/ProcessMaker/Http/Middleware/IsManager.php @@ -0,0 +1,195 @@ +user(); + + if (!$user) { + return abort(401, 'Unauthenticated'); + } + + // if user is administrator, allow access + if ($user->is_administrator) { + return $next($request); + } + + if (!Cache::get("user_{$user->id}_manager")) { + // if user is not manager, continue + return $next($request); + } + + // get the required permissions for this specific URL + $requiredPermissions = $this->getRequiredPermissionsForRequest($request); + + if (empty($requiredPermissions)) { + // if no required permissions, continue + return $next($request); + } + + // simulate that the user has all the necessary permissions for this request + $this->simulateRequiredPermissionsForRequest($user, $requiredPermissions); + + try { + // process the request - the internal endpoints will handle the permission validation + $response = $next($request); + + // clean up the simulated permissions after processing the request + $this->cleanupSimulatedPermission($user); + + return $response; + } catch (\Exception $e) { + // make sure to clean up the simulated permissions even if there is an exception + $this->cleanupSimulatedPermission($user); + throw $e; + } + } + + /** + * Simula que el usuario tiene los permisos requeridos solo para esta solicitud + */ + private function simulateRequiredPermissionsForRequest($user, array $requiredPermissions) + { + try { + // get the current permissions of the user + $currentPermissions = $user->loadPermissions(); + + // filter only the permissions that the user does not have + $permissionsToAdd = array_diff($requiredPermissions, $currentPermissions); + + if (empty($permissionsToAdd)) { + return; + } + + // simulate the permissions by adding them temporarily to the cache of permissions + $cacheKey = "user_{$user->id}_permissions"; + $simulatedPermissions = array_merge($currentPermissions, $permissionsToAdd); + + // save in cache temporarily (only for this request) + // use a very short time to expire quickly if not cleaned manually + Cache::put($cacheKey, $simulatedPermissions, 5); // 5 segundos como fallback + } catch (\Exception $e) { + Log::error('IsManager middleware - Error simulating permissions: ' . $e->getMessage()); + } + } + + /** + * clean up the simulated permissions from the cache after processing the request + */ + private function cleanupSimulatedPermission($user) + { + try { + $cacheKey = "user_{$user->id}_permissions"; + + // delete the cache to force the reload of real permissions + Cache::forget($cacheKey); + } catch (\Exception $e) { + Log::error('IsManager middleware - Error cleaning up simulated permissions: ' . $e->getMessage()); + } + } + + /** + * get the required permissions for the current URL + */ + private function getRequiredPermissionsForRequest(Request $request): array + { + $permissions = []; + + try { + $url = $request->fullUrl(); + $path = $request->path(); + $method = $request->method(); + + // first, get permissions from middlewares of the route + $middlewarePermissions = $this->getPermissionsFromMiddlewares($request); + $permissions = array_merge($permissions, $middlewarePermissions); + + // then, get permissions based on URL patterns + $urlPermissions = $this->getPermissionsFromUrlPatterns($url, $path, $method); + $permissions = array_merge($permissions, $urlPermissions); + } catch (\Exception $e) { + Log::error('IsManager middleware - Error getting required permissions: ' . $e->getMessage()); + } + + return array_unique($permissions); + } + + /** + * get permissions from the middlewares of the route + */ + private function getPermissionsFromMiddlewares(Request $request): array + { + $permissions = []; + + try { + // get all the middlewares of the route + $middlewares = $request->route()->middleware(); + + // filter only the middlewares that contain 'can:' + $permissionMiddlewares = array_filter($middlewares, function ($middleware) { + return str_contains($middleware, 'can:'); + }); + + // extract the permissions from each middleware + foreach ($permissionMiddlewares as $middleware) { + // format: "can:permission" or "can:permission,model" + if (preg_match('/can:([^,]+)/', $middleware, $matches)) { + $permissions[] = $matches[1]; + } + } + } catch (\Exception $e) { + Log::error('IsManager middleware - Error getting permissions from middlewares: ' . $e->getMessage()); + } + + return $permissions; + } + + /** + * get permissions based on URL patterns + */ + private function getPermissionsFromUrlPatterns(string $url, string $path, string $method): array + { + $permissions = []; + + // for now we only support GET methods + if ($method !== 'GET') { + return $permissions; + } + + try { + // URL patterns and their corresponding permissions + $urlPatterns = [ + // patterns for users + '/api\/.*\/users(\?.*)?$/' => 'view-users', + + // patterns for saved searches + '/api\/.*\/saved-searches\/columns(\?.*)?$/' => 'view-saved-searches-columns', + ]; + + // check each pattern + foreach ($urlPatterns as $pattern => $permission) { + if (preg_match($pattern, $url)) { + $permissions[] = $permission; + } + } + } catch (\Exception $e) { + Log::error('IsManager middleware - Error getting permissions from URL patterns: ' . $e->getMessage()); + } + + return $permissions; + } +} diff --git a/routes/api.php b/routes/api.php index 62fa62cde1..053de82bcc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -45,7 +45,7 @@ use ProcessMaker\Http\Controllers\Auth\TwoFactorAuthController; use ProcessMaker\Http\Controllers\TestStatusController; -Route::middleware('auth:api', 'setlocale', 'bindings', 'sanitize')->prefix('api/1.0')->name('api.')->group(function () { +Route::middleware('auth:api', 'setlocale', 'bindings', 'sanitize', 'manager')->prefix('api/1.0')->name('api.')->group(function () { // Users Route::get('users', [UserController::class, 'index'])->name('users.index'); // Permissions handled in the controller Route::get('users/{user}', [UserController::class, 'show'])->name('users.show'); // Permissions handled in the controller @@ -448,6 +448,6 @@ }); // Slack Connector Validation - Route::post('connector-slack/validate-token', [\ProcessMaker\Packages\Connectors\Slack\Controllers\SlackController::class, 'validateToken'])->name('connector-slack.validate-token'); + Route::post('connector-slack/validate-token', [ProcessMaker\Packages\Connectors\Slack\Controllers\SlackController::class, 'validateToken'])->name('connector-slack.validate-token'); }); Route::post('devlink/bundle-updated/{bundle}/{token}', [DevLinkController::class, 'bundleUpdated'])->name('devlink.bundle-updated'); From af97cf6fdd071ba5136a8ffcd6cfbd7a7b4190a5 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Fri, 3 Oct 2025 14:24:28 -0400 Subject: [PATCH 27/51] Refactor TaskController to improve user filtering and caching logic --- .../Http/Controllers/Api/TaskController.php | 15 ++++++++++----- .../Traits/TaskControllerIndexMethods.php | 9 ++++++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index cd4cb5fd1c..ee80c79975 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -146,15 +146,15 @@ public function index(Request $request, $getTotal = false, User $user = null) $this->applyAdvancedFilter($query, $request); - $this->applyForCurrentUser($query, $user); - - // Apply filter overdue - $query->overdue($request->input('overdue')); - if ($request->input('processesIManage') === 'true') { $this->applyProcessManager($query, $user); + } else { + $this->applyForCurrentUser($query, $user); } + // Apply filter overdue + $query->overdue($request->input('overdue')); + // If only the total is being requested (by a Saved Search), send it now if ($getTotal === true) { return $query->count(); @@ -168,6 +168,11 @@ public function index(Request $request, $getTotal = false, User $user = null) $response = $this->applyUserFilter($response, $request, $user); + if ($response->total() > 0 && $request->input('processesIManage') === 'true') { + // enable user manager in cache + $this->enableUserManager($user); + } + $inOverdueQuery = ProcessRequestToken::query() ->whereIn('id', $response->pluck('id')) ->where('due_at', '<', Carbon::now()); diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 0461c94ec1..849eaae2ef 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -6,6 +6,7 @@ use DB; use Illuminate\Database\QueryException; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; use ProcessMaker\Filters\Filter; use ProcessMaker\Managers\DataManager; @@ -344,8 +345,14 @@ public function applyProcessManager($query, $user) }); } + private function enableUserManager($user) + { + // enable user in cache + Cache::put("user_{$user->id}_manager", true); + } + /** - * Get the ID of the default saved search for tasks. + * Get the ID of the default saved search for tasks * * @return int|null */ From ae2a94e66f266b5b46eb863ccdcd57af5fdf3f4c Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 8 Oct 2025 09:39:15 -0700 Subject: [PATCH 28/51] Fix error when dependent screen is missing --- .../ImportExport/Exporters/ScreenExporter.php | 5 ++-- .../Exporters/ScreenExporterTest.php | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/ImportExport/Exporters/ScreenExporter.php b/ProcessMaker/ImportExport/Exporters/ScreenExporter.php index 03370ed26d..119803feae 100644 --- a/ProcessMaker/ImportExport/Exporters/ScreenExporter.php +++ b/ProcessMaker/ImportExport/Exporters/ScreenExporter.php @@ -97,11 +97,12 @@ private function getNestedScreens() : array $screenFinder = new ScreensInScreen(); foreach ($screenFinder->referencesToExport($this->model, [], null, false) as $screen) { try { - $screen = Screen::find($screen[1]); + $screenId = $screen[1]; + $screen = Screen::find($screenId); if ($screen) { $screens[] = $screen; } else { - \Log::debug("NestedScreen screenId: $screen[1] not exists"); + \Log::debug("NestedScreen screenId: $screenId not exists"); } } catch (ModelNotFoundException $error) { \Log::error($error->getMessage()); diff --git a/tests/Feature/ImportExport/Exporters/ScreenExporterTest.php b/tests/Feature/ImportExport/Exporters/ScreenExporterTest.php index c34a8c42b5..899145b98c 100644 --- a/tests/Feature/ImportExport/Exporters/ScreenExporterTest.php +++ b/tests/Feature/ImportExport/Exporters/ScreenExporterTest.php @@ -330,4 +330,28 @@ public function testAttemptToAddMultipleInterstials() $newInterstitial = Screen::where('title', 'Default Interstitial 2')->firstOrFail(); $this->assertNull($newInterstitial->key); } + + public function testExportScreenWithMissingDependentScreen() + { + $screen = Screen::factory()->create([ + 'title' => 'Screen with missing dependent screen', + 'key' => 'screen', + 'config' => [ + ['items' => [ + ['component' => 'FormNestedScreen', 'config' => ['screen' => 9999999999]], + ]], + ], + ]); + + $exporter = new Exporter(); + $exporter->exportScreen($screen); + $payload = $exporter->payload(); + + $screens = Arr::where($payload['export'], function ($value) { + return $value['type'] === 'Screen'; + }); + + $this->assertCount(1, $screens); + $this->assertEquals($screen->uuid, Arr::first($screens)['attributes']['uuid']); + } } From ce25e787e965803a434fab4dbdcfa2d17bb659c9 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 14 Oct 2025 17:54:58 -0500 Subject: [PATCH 29/51] Implement SAML email edit restriction alert in user profile updates --- resources/lang/en.json | 1 + resources/views/admin/users/edit.blade.php | 11 +++++++++-- resources/views/profile/edit.blade.php | 10 ++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index afbf41e7cd..03ce641c60 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -773,6 +773,7 @@ "Element is not connected": "Element is not connected", "Element": "Element", "Email Address": "Email Address", + "Email address for users created via SAML synchronization cannot be edited manually.": "Email address for users created via SAML synchronization cannot be edited manually.", "Email": "Email", "Embed Media": "Embed Media", "Embed URL": "Embed URL", diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index c6ec688672..7a276fce0c 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -478,9 +478,16 @@ } return true }, + profileUpdate($event) { - if(this.emailHasChanged && !this.ssoUser) { - $('#validateModal').modal('show'); + if (this.emailHasChanged) { + if (this.ssoUser) { + let message = 'Email address for users created via SAML synchronization cannot be edited manually.'; + ProcessMaker.alert(this.$t($message), 'warning'); + return; + } else { + $('#validateModal').modal('show'); + } } else { this.saveProfileChanges(); } diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index 3ddc8b4de2..85fa5e51a6 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -192,8 +192,14 @@ modalVueInstance.$refs.updateAvatarModal.show(); }, profileUpdate() { - if(this.emailHasChanged && !this.ssoUser) { - $('#validateModal').modal('show'); + if(this.emailHasChanged) { + if (this.ssoUser) { + let message = 'Email address for users created via SAML synchronization cannot be edited manually.'; + ProcessMaker.alert(this.$t($message), 'warning'); + return; + } else { + $('#validateModal').modal('show'); + } } else { this.saveProfileChanges(); } From f98013b5e4faaede6bf7c4a7d17f2396b5483216 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Tue, 14 Oct 2025 18:08:29 -0500 Subject: [PATCH 30/51] Fix alert message translation for SAML email edit restriction in user profile and admin user edit views --- resources/views/admin/users/edit.blade.php | 2 +- resources/views/profile/edit.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index 7a276fce0c..180739726b 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -483,7 +483,7 @@ if (this.emailHasChanged) { if (this.ssoUser) { let message = 'Email address for users created via SAML synchronization cannot be edited manually.'; - ProcessMaker.alert(this.$t($message), 'warning'); + ProcessMaker.alert(this.$t(message), 'warning'); return; } else { $('#validateModal').modal('show'); diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index 85fa5e51a6..d7d24757a2 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -195,7 +195,7 @@ if(this.emailHasChanged) { if (this.ssoUser) { let message = 'Email address for users created via SAML synchronization cannot be edited manually.'; - ProcessMaker.alert(this.$t($message), 'warning'); + ProcessMaker.alert(this.$t(message), 'warning'); return; } else { $('#validateModal').modal('show'); From a08445fc566afdacb247a610330a6a32526a93ca Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Mon, 20 Oct 2025 11:32:00 -0400 Subject: [PATCH 31/51] Add middleware hide server headers --- ProcessMaker/Http/Kernel.php | 1 + .../Http/Middleware/HideServerHeaders.php | 91 +++++++++++++++++++ config/app.php | 3 + 3 files changed, 95 insertions(+) create mode 100644 ProcessMaker/Http/Middleware/HideServerHeaders.php diff --git a/ProcessMaker/Http/Kernel.php b/ProcessMaker/Http/Kernel.php index f8a5888430..991672f4b0 100644 --- a/ProcessMaker/Http/Kernel.php +++ b/ProcessMaker/Http/Kernel.php @@ -27,6 +27,7 @@ class Kernel extends HttpKernel ServerTimingMiddleware::class, Middleware\FileSizeCheck::class, Middleware\AddTenantHeaders::class, + Middleware\HideServerHeaders::class, ]; /** diff --git a/ProcessMaker/Http/Middleware/HideServerHeaders.php b/ProcessMaker/Http/Middleware/HideServerHeaders.php new file mode 100644 index 0000000000..c46b3a8a77 --- /dev/null +++ b/ProcessMaker/Http/Middleware/HideServerHeaders.php @@ -0,0 +1,91 @@ +shouldHideHeaders()) { + // Remove all server-revealing headers + foreach ($this->headersToRemove as $header) { + $response->headers->remove($header); + } + + // Set a generic server header to avoid revealing the absence + $response->headers->set('Server', 'Web Server'); + } + + return $response; + } + + /** + * Determine if headers should be hidden based on environment + * + * @return bool + */ + private function shouldHideHeaders(): bool + { + // Hide headers in production or when explicitly configured + return app()->environment('production') || + config('app.hide_server_headers', false); + } +} diff --git a/config/app.php b/config/app.php index 71d8eb15c2..4106d36af8 100644 --- a/config/app.php +++ b/config/app.php @@ -41,6 +41,9 @@ // The timeout length for API calls, in milliseconds (0 for no timeout) 'api_timeout' => env('API_TIMEOUT', 5000), + // Hide server headers for security (prevents information disclosure) + 'hide_server_headers' => env('HIDE_SERVER_HEADERS', true), + // Disables PHP execution in the storage directory // TODO Is this config value still used anywhere? :) 'disable_php_upload_execution' => env('DISABLE_PHP_UPLOAD_EXECUTION', 0), From a2a6b7e9f3e54cada45e4fa8437c677e236cadaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:35:53 -0300 Subject: [PATCH 32/51] Add name attribute in task resource --- ProcessMaker/Http/Resources/V1_1/TaskResource.php | 1 + 1 file changed, 1 insertion(+) diff --git a/ProcessMaker/Http/Resources/V1_1/TaskResource.php b/ProcessMaker/Http/Resources/V1_1/TaskResource.php index cec32ced7e..26755cd5f5 100644 --- a/ProcessMaker/Http/Resources/V1_1/TaskResource.php +++ b/ProcessMaker/Http/Resources/V1_1/TaskResource.php @@ -107,6 +107,7 @@ class TaskResource extends ApiResource 'case_number', 'callable_id', 'process_version_id', + 'name', ], 'draft' => ['id', 'task_id', 'data'], 'screen' => ['id', 'config'], From 3edc98e89bc9d2775f2ab396faf6738750c2dfe8 Mon Sep 17 00:00:00 2001 From: "Marco A. Nina Mena" Date: Mon, 20 Oct 2025 15:39:14 -0400 Subject: [PATCH 33/51] Add Name Server --- ProcessMaker/Http/Middleware/HideServerHeaders.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ProcessMaker/Http/Middleware/HideServerHeaders.php b/ProcessMaker/Http/Middleware/HideServerHeaders.php index c46b3a8a77..31a9258648 100644 --- a/ProcessMaker/Http/Middleware/HideServerHeaders.php +++ b/ProcessMaker/Http/Middleware/HideServerHeaders.php @@ -41,8 +41,6 @@ class HideServerHeaders 'X-Cache-Status', 'X-Served-From', 'X-Content-Source', - 'X-Request-ID', - 'X-Request-Id', // PHP specific headers 'X-PHP-Version', @@ -71,7 +69,7 @@ public function handle(Request $request, Closure $next): Response } // Set a generic server header to avoid revealing the absence - $response->headers->set('Server', 'Web Server'); + $response->headers->set('Server', 'ProcessMaker Server'); } return $response; From adb49a743984bb201cead00690e534eae3b67122 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 21 Oct 2025 08:07:34 -0700 Subject: [PATCH 34/51] Add queue metrics for prometheus --- ProcessMaker/Console/Kernel.php | 2 ++ ProcessMaker/Services/MetricsService.php | 36 ++++++++++++++++++++++-- routes/web.php | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index d5f52d15e1..06b62297d6 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -88,6 +88,8 @@ protected function schedule(Schedule $schedule) $schedule->command('metrics:clear')->cron("*/{$clearInterval} * * * *"); break; } + + $schedule->command('horizon:snapshot')->everyFiveMinutes(); } /** diff --git a/ProcessMaker/Services/MetricsService.php b/ProcessMaker/Services/MetricsService.php index 20ae9b8862..4ae8492bca 100644 --- a/ProcessMaker/Services/MetricsService.php +++ b/ProcessMaker/Services/MetricsService.php @@ -3,6 +3,9 @@ namespace ProcessMaker\Services; use Exception; +use Laravel\Horizon\Contracts\JobRepository; +use Laravel\Horizon\Contracts\MetricsRepository; +use Laravel\Horizon\Contracts\WorkloadRepository; use ProcessMaker\Facades\Metrics; use Prometheus\CollectorRegistry; use Prometheus\Counter; @@ -65,7 +68,7 @@ public function getCollectionRegistry(): CollectorRegistry * @param array $labels The labels of the counter. * @return Counter The registered or retrieved counter. */ - public function counter(string $name, string $help = null, array $labels = []): Counter + public function counter(string $name, string|null $help = null, array $labels = []): Counter { $help = $help ?? $name; @@ -85,7 +88,7 @@ public function counter(string $name, string $help = null, array $labels = []): * @param array $labels The labels of the gauge. * @return Gauge The registered or retrieved gauge. */ - public function gauge(string $name, string $help = null, array $labels = []): Gauge + public function gauge(string $name, string|null $help = null, array $labels = []): Gauge { $help = $help ?? $name; @@ -201,7 +204,9 @@ public function addSystemLabels(array $labels) // Add system labels $labels['app_version'] = $this->getApplicationVersion(); $labels['app_name'] = config('app.name'); - $labels['app_custom_label'] = config('app.prometheus_custom_label'); + if (config('app.custom_label')) { + $labels['app_custom_label'] = config('app.custom_label'); + } return $labels; } @@ -223,4 +228,29 @@ private function getApplicationVersion() return $composer_json_path->version ?? '4.0.0'; } + + /** + * These are collected every time the /metrics route is accessed. + * + * @return void + */ + public function collectRealTimeMetrics(): void + { + $metricsRepository = app(MetricsRepository::class); + $jobsRepository = app(JobRepository::class); + $workloadRepository = app(WorkloadRepository::class); + + $this->gauge('horizon_jobs_per_minute', 'Jobs processed per minute')->set($metricsRepository->jobsProcessedPerMinute()); + $this->gauge('horizon_failed_jobs_per_hour', 'Failed jobs per hour')->set($jobsRepository->countRecentlyFailed()); + + foreach ($workloadRepository->get() as $workload) { + $name = $workload['name']; + foreach (['length', 'wait', 'processes'] as $type) { + $this->gauge( + 'horizon_workload_' . $name . '_' . $type, + 'Workload ' . $name . ' ' . $type + )->set($workload[$type]); + } + } + } } diff --git a/routes/web.php b/routes/web.php index 2039cb5ee7..e3d1a06219 100644 --- a/routes/web.php +++ b/routes/web.php @@ -257,6 +257,8 @@ // Metrics Route Route::get('/metrics', function () { + Metrics::collectRealTimeMetrics(); + return response(Metrics::renderMetrics(), 200, [ 'Content-Type' => 'text/plain; version=0.0.4', ]); From 02310ed04a5cc7f7881d8d8b42ef07f1fcf2accd Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 21 Oct 2025 08:19:04 -0700 Subject: [PATCH 35/51] Add note --- ProcessMaker/Console/Kernel.php | 1 + 1 file changed, 1 insertion(+) diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 06b62297d6..1d6ee38a81 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -89,6 +89,7 @@ protected function schedule(Schedule $schedule) break; } + // 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics $schedule->command('horizon:snapshot')->everyFiveMinutes(); } From ccadccac88b2f15b2f91d2e1744e0e8f5ff05990 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 21 Oct 2025 08:22:24 -0700 Subject: [PATCH 36/51] Fix config key --- ProcessMaker/Services/MetricsService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/Services/MetricsService.php b/ProcessMaker/Services/MetricsService.php index 4ae8492bca..b2196881be 100644 --- a/ProcessMaker/Services/MetricsService.php +++ b/ProcessMaker/Services/MetricsService.php @@ -204,8 +204,8 @@ public function addSystemLabels(array $labels) // Add system labels $labels['app_version'] = $this->getApplicationVersion(); $labels['app_name'] = config('app.name'); - if (config('app.custom_label')) { - $labels['app_custom_label'] = config('app.custom_label'); + if (config('app.prometheus_custom_label')) { + $labels['app_custom_label'] = config('app.prometheus_custom_label'); } return $labels; From 21808982dcfc681213ec697faa2107b9aff87c9a Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 22 Oct 2025 10:41:45 -0700 Subject: [PATCH 37/51] Handle cache prefixing in bootstrapper --- ProcessMaker/Multitenancy/TenantBootstrapper.php | 2 ++ config/multitenancy.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index 44897c4763..a7c09e7482 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -34,6 +34,7 @@ class TenantBootstrapper 'REDIS_PREFIX', 'CACHE_SETTING_PREFIX', 'SCRIPT_MICROSERVICE_CALLBACK', + 'CACHE_PREFIX', ]; public function bootstrap(Application $app) @@ -86,6 +87,7 @@ private function setTenantEnvironmentVariables($tenantData) $this->set('APP_KEY', $this->decrypt($config['app.key'])); $this->set('DB_DATABASE', $tenantData['database']); $this->set('DB_USERNAME', $tenantData['username'] ?? $this->getOriginalValue('DB_USERNAME')); + $this->set('CACHE_PREFIX', $this->getOriginalValue('CACHE_PREFIX') . 'tenant_' . $tenantData['id'] . ':'); $encryptedPassword = $tenantData['password']; $password = null; diff --git a/config/multitenancy.php b/config/multitenancy.php index b1a5aa5e2f..7c56e74d8f 100644 --- a/config/multitenancy.php +++ b/config/multitenancy.php @@ -34,7 +34,6 @@ */ 'switch_tenant_tasks' => [ SwitchTenant::class, - Spatie\Multitenancy\Tasks\PrefixCacheTask::class, ], /* From 10779c365825af4296ef1b670950667d935990db Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 22 Oct 2025 10:42:29 -0700 Subject: [PATCH 38/51] Make metrics tenant aware --- .../Exception/MultitenancyAccessedLandlord.php | 10 ++++++++++ ProcessMaker/Services/MetricsService.php | 13 ++++++++++--- routes/web.php | 4 +++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/ProcessMaker/Exception/MultitenancyAccessedLandlord.php b/ProcessMaker/Exception/MultitenancyAccessedLandlord.php index d2bb0654ed..cfeb78fd3b 100644 --- a/ProcessMaker/Exception/MultitenancyAccessedLandlord.php +++ b/ProcessMaker/Exception/MultitenancyAccessedLandlord.php @@ -5,11 +5,21 @@ use Exception; use Illuminate\Http\Request; use Illuminate\Http\Response; +use ProcessMaker\Facades\Metrics; class MultitenancyAccessedLandlord extends Exception { public function render(Request $request): Response { + // If we're trying to access the /metrics route, collect landlord metrics and render them + if ($request->path() === 'metrics') { + Metrics::collectQueueMetrics(); + + return response(Metrics::renderMetrics(), 200, [ + 'Content-Type' => 'text/plain; version=0.0.4', + ]); + } + return response()->view('multitenancy.landlord-landing-page'); } diff --git a/ProcessMaker/Services/MetricsService.php b/ProcessMaker/Services/MetricsService.php index b2196881be..824f12b6cc 100644 --- a/ProcessMaker/Services/MetricsService.php +++ b/ProcessMaker/Services/MetricsService.php @@ -7,12 +7,14 @@ use Laravel\Horizon\Contracts\MetricsRepository; use Laravel\Horizon\Contracts\WorkloadRepository; use ProcessMaker\Facades\Metrics; +use ProcessMaker\Multitenancy\Tenant; use Prometheus\CollectorRegistry; use Prometheus\Counter; use Prometheus\Gauge; use Prometheus\Histogram; use Prometheus\RenderTextFormat; -use Prometheus\Storage\Redis; +use Prometheus\Storage\Redis as PrometheusRedis; +use Redis; use RuntimeException; class MetricsService @@ -42,7 +44,12 @@ public function __construct(private $adapter = null) try { // Set up Redis as the adapter if none is provided if ($adapter === null) { - $adapter = Redis::fromExistingConnection(app('redis')->client()); + $redis = app('redis')->client(); + $adapter = PrometheusRedis::fromExistingConnection($redis); + if (app()->has(Tenant::BOOTSTRAPPED_TENANT)) { + $tenantInfo = app(Tenant::BOOTSTRAPPED_TENANT); + $adapter->setPrefix('tenant_' . $tenantInfo['id'] . ':PROMETHEUS_'); + } } $this->collectionRegistry = new CollectorRegistry($adapter); } catch (Exception $e) { @@ -234,7 +241,7 @@ private function getApplicationVersion() * * @return void */ - public function collectRealTimeMetrics(): void + public function collectQueueMetrics(): void { $metricsRepository = app(MetricsRepository::class); $jobsRepository = app(JobRepository::class); diff --git a/routes/web.php b/routes/web.php index e3d1a06219..30f1cdaa35 100644 --- a/routes/web.php +++ b/routes/web.php @@ -257,7 +257,9 @@ // Metrics Route Route::get('/metrics', function () { - Metrics::collectRealTimeMetrics(); + if (!config('app.multitenancy')) { + Metrics::collectQueueMetrics(); + } return response(Metrics::renderMetrics(), 200, [ 'Content-Type' => 'text/plain; version=0.0.4', From 0f0dd737b7b740cb4ccdeadd9b48873922c6aefe Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Thu, 23 Oct 2025 07:50:43 -0700 Subject: [PATCH 39/51] Fix schedule:run cron error --- ProcessMaker/Jobs/ErrorHandling.php | 2 +- ProcessMaker/Multitenancy/SwitchTenant.php | 2 +- .../Multitenancy/TenantBootstrapper.php | 18 ++++-------------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ProcessMaker/Jobs/ErrorHandling.php b/ProcessMaker/Jobs/ErrorHandling.php index ea0049a742..db880d3767 100644 --- a/ProcessMaker/Jobs/ErrorHandling.php +++ b/ProcessMaker/Jobs/ErrorHandling.php @@ -213,7 +213,7 @@ public static function convertResponseToException($result) if (str_starts_with($result['message'], 'Command exceeded timeout of')) { throw new ScriptTimeoutException($result['message']); } - throw new ScriptException($result['message']); + throw new ScriptException(json_encode($result, JSON_PRETTY_PRINT)); } } } diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php index 1902a0b8e5..c549e54418 100644 --- a/ProcessMaker/Multitenancy/SwitchTenant.php +++ b/ProcessMaker/Multitenancy/SwitchTenant.php @@ -70,7 +70,7 @@ private function overrideConfigs(Application $app, IsTenant $tenant) if (!isset($tenant->config['app.docker_host_url'])) { // There is no specific override in the tenant's config so set it to the app url - $newConfig['app.docker_host_url'] = config('app.url'); + $newConfig['app.docker_host_url'] = config('app.docker_host_url', config('app.url')); } // Set config from the entry in the tenants table diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index a7c09e7482..2ed8a9b07d 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Multitenancy; +use Dotenv\Dotenv; use Illuminate\Encryption\Encrypter; use Illuminate\Http\Request; use Illuminate\Support\Env; @@ -44,8 +45,6 @@ public function bootstrap(Application $app) } $this->app = $app; - self::saveLandlordValues($app); - $tenantData = null; // Try to find tenant by ID first if TENANT env var is set @@ -101,21 +100,12 @@ private function setTenantEnvironmentVariables($tenantData) $this->set('LOG_PATH', $this->app->storagePath('logs/processmaker.log')); } - public static function saveLandlordValues($app) + private function getOriginalValue($key) { - if ($app->has('landlordValues')) { - self::$landlordValues = $app->make('landlordValues'); - - return; + if (self::$landlordValues === []) { + self::$landlordValues = Dotenv::parse(file_get_contents(base_path('.env'))); } - foreach (self::$landlordKeysToSave as $key) { - self::$landlordValues[$key] = $_SERVER[$key] ?? ''; - } - } - - private function getOriginalValue($key) - { if (!isset(self::$landlordValues[$key])) { return ''; } From a56fdfebce1fa1bad39ca40a0d988fb823bb8561 Mon Sep 17 00:00:00 2001 From: ProcessMaker Bot <206180840+processmaker-bot@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:51:55 +0000 Subject: [PATCH 40/51] Version 4.15.9-RC1 --- composer.json | 4 ++-- composer.lock | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 2cf524b23f..b41f364ccf 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "processmaker/processmaker", - "version": "4.15.9+beta-3", + "version": "4.15.9-RC1", "description": "BPM PHP Software", "keywords": [ "php bpm processmaker" @@ -106,7 +106,7 @@ "Gmail" ], "processmaker": { - "build": "0d8546ea", + "build": "ada2bb9d", "cicd-enabled": true, "custom": { "package-ellucian-ethos": "1.19.7", diff --git a/composer.lock b/composer.lock index 1b8e57f38e..ef9a02a386 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8a7464b4b1cc0bc462ed44e24dc5e340", + "content-hash": "af82795be562eb46445c292c914557e4", "packages": [ { "name": "aws/aws-crt-php", diff --git a/package-lock.json b/package-lock.json index 0fb601b87a..b1e79e6a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.9-RC1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.9-RC1", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index b8b32e5e62..e800260bff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.9-RC1", "description": "ProcessMaker 4", "author": "DevOps ", "license": "ISC", From 7e6bcac3dd81bcfb45bab4062b226d30315e0cd3 Mon Sep 17 00:00:00 2001 From: ProcessMaker Bot <206180840+processmaker-bot@users.noreply.github.com> Date: Thu, 23 Oct 2025 19:46:35 +0000 Subject: [PATCH 41/51] Version 4.15.9 --- composer.json | 4 ++-- composer.lock | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index b41f364ccf..3d2ac7b7cf 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "processmaker/processmaker", - "version": "4.15.9-RC1", + "version": "4.15.9", "description": "BPM PHP Software", "keywords": [ "php bpm processmaker" @@ -106,7 +106,7 @@ "Gmail" ], "processmaker": { - "build": "ada2bb9d", + "build": "1c5ce226", "cicd-enabled": true, "custom": { "package-ellucian-ethos": "1.19.7", diff --git a/composer.lock b/composer.lock index ef9a02a386..0879d99a8b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "af82795be562eb46445c292c914557e4", + "content-hash": "bad55f1c553cbb7659c60c10759fe5b1", "packages": [ { "name": "aws/aws-crt-php", diff --git a/package-lock.json b/package-lock.json index b1e79e6a08..0fb601b87a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@processmaker/processmaker", - "version": "4.15.9-RC1", + "version": "4.15.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@processmaker/processmaker", - "version": "4.15.9-RC1", + "version": "4.15.9", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index e800260bff..b8b32e5e62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@processmaker/processmaker", - "version": "4.15.9-RC1", + "version": "4.15.9", "description": "ProcessMaker 4", "author": "DevOps ", "license": "ISC", From 0832e484d6ce88d36c95ee7092501ef5d5bd85f1 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Thu, 23 Oct 2025 14:06:36 -0700 Subject: [PATCH 42/51] Fix multitenancy settings cache --- ProcessMaker/Multitenancy/SwitchTenant.php | 5 ----- ProcessMaker/Multitenancy/TenantBootstrapper.php | 5 ++++- ProcessMaker/Providers/TenantQueueServiceProvider.php | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php index c549e54418..59b7c3ac5b 100644 --- a/ProcessMaker/Multitenancy/SwitchTenant.php +++ b/ProcessMaker/Multitenancy/SwitchTenant.php @@ -55,11 +55,6 @@ private function overrideConfigs(Application $app, IsTenant $tenant) 'app.instance' => config('app.instance') . '_' . $tenant->id, ]; - if (!isset($tenant->config['cache.stores.cache_settings.prefix'])) { - $newConfig['cache.stores.cache_settings.prefix'] = - 'tenant_id_' . $tenant->id . ':' . $tenant->getOriginalValue('CACHE_SETTING_PREFIX'); - } - if (!isset($tenant->config['script-runner-microservice.callback'])) { $newConfig['script-runner-microservice.callback'] = str_replace( $tenant->getOriginalValue('APP_URL'), diff --git a/ProcessMaker/Multitenancy/TenantBootstrapper.php b/ProcessMaker/Multitenancy/TenantBootstrapper.php index 2ed8a9b07d..dc71c3f0d0 100644 --- a/ProcessMaker/Multitenancy/TenantBootstrapper.php +++ b/ProcessMaker/Multitenancy/TenantBootstrapper.php @@ -86,7 +86,10 @@ private function setTenantEnvironmentVariables($tenantData) $this->set('APP_KEY', $this->decrypt($config['app.key'])); $this->set('DB_DATABASE', $tenantData['database']); $this->set('DB_USERNAME', $tenantData['username'] ?? $this->getOriginalValue('DB_USERNAME')); - $this->set('CACHE_PREFIX', $this->getOriginalValue('CACHE_PREFIX') . 'tenant_' . $tenantData['id'] . ':'); + + // Do not set REDIS_PREFIX because it is used by the queue (not tenant specific) + $this->set('CACHE_PREFIX', 'tenant_' . $tenantData['id'] . ':' . $this->getOriginalValue('CACHE_PREFIX')); + $this->set('CACHE_SETTING_PREFIX', 'tenant_' . $tenantData['id'] . ':' . $this->getOriginalValue('CACHE_SETTING_PREFIX')); $encryptedPassword = $tenantData['password']; $password = null; diff --git a/ProcessMaker/Providers/TenantQueueServiceProvider.php b/ProcessMaker/Providers/TenantQueueServiceProvider.php index 544650b22f..b603718e2d 100644 --- a/ProcessMaker/Providers/TenantQueueServiceProvider.php +++ b/ProcessMaker/Providers/TenantQueueServiceProvider.php @@ -286,7 +286,7 @@ protected function updateTenantJobLists(string $tenantId, string $jobId, string } else { // If job is already in the list, move it to the front (most recent) Redis::pipeline(function ($pipe) use ($listKey, $jobId) { - $pipe->lrem($listKey, 0, $jobId); + $pipe->lrem($listKey, $jobId, 0); $pipe->lpush($listKey, $jobId); $pipe->expire($listKey, 86400); // Expire in 24 hours }); From 929a573daea6fb1ccb07847df2f1cdd7e9eae595 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Fri, 24 Oct 2025 11:14:13 -0700 Subject: [PATCH 43/51] Refactor tenant queue authorization --- .../Admin/TenantQueueController.php | 67 ++++++++----------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php b/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php index 008a3495fd..2d8198434c 100644 --- a/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php +++ b/ProcessMaker/Http/Controllers/Admin/TenantQueueController.php @@ -14,36 +14,12 @@ class TenantQueueController extends Controller { - /** - * Constructor to check if tenant tracking is enabled. - */ - public function __construct() - { - // Check if tenant job tracking is enabled - $enabled = TenantQueueServiceProvider::enabled(); - - if (!$enabled) { - if (!app()->runningInConsole()) { - abort(404, 'Tenant queue tracking is disabled'); - } - } - - // If the route binding has a tenant id, check if the user is allowed to access the tenant queue - if ($id = (int) request()->route('tenantId')) { - if (!TenantQueueServiceProvider::allowAllTenats() && $id !== app('currentTenant')?->id) { - throw new AuthorizationException(); - } - } - } - /** * Show the tenant jobs dashboard. */ public function index() { - if (!Auth::user()->is_administrator) { - throw new AuthorizationException(); - } + $this->checkPermissions(); return view('admin.tenant-queues.index'); } @@ -53,9 +29,7 @@ public function index() */ public function getTenants(): JsonResponse { - if (!Auth::user()->is_administrator) { - throw new AuthorizationException(); - } + $this->checkPermissions(); $tenantsWithJobs = TenantQueueServiceProvider::getTenantsWithJobs(); @@ -87,9 +61,7 @@ public function getTenants(): JsonResponse */ public function getTenantJobs(Request $request, string $tenantId): JsonResponse { - if (!Auth::user()->is_administrator) { - throw new AuthorizationException(); - } + $this->checkPermissions(); $status = $request->get('status'); $limit = min((int) $request->get('limit', 50), 100); // Max 100 jobs @@ -125,9 +97,7 @@ public function getTenantStats(string $tenantId): JsonResponse */ public function getOverallStats(): JsonResponse { - if (!Auth::user()->is_administrator) { - throw new AuthorizationException(); - } + $this->checkPermissions(); $tenantsWithJobs = TenantQueueServiceProvider::getTenantsWithJobs(); @@ -163,9 +133,7 @@ public function getOverallStats(): JsonResponse */ public function getJobDetails(string $tenantId, string $jobId): JsonResponse { - if (!Auth::user()->is_administrator) { - throw new AuthorizationException(); - } + $this->checkPermissions(); $tenantKey = "tenant_jobs:{$tenantId}:{$jobId}"; $jobData = Redis::hgetall($tenantKey); @@ -199,9 +167,7 @@ public function getJobDetails(string $tenantId, string $jobId): JsonResponse */ public function clearTenantJobs(string $tenantId): JsonResponse { - if (!Auth::user()->is_administrator) { - throw new AuthorizationException(); - } + $this->checkPermissions(); try { $pattern = "tenant_jobs:{$tenantId}:*"; @@ -228,4 +194,25 @@ public function clearTenantJobs(string $tenantId): JsonResponse return response()->json(['error' => 'Failed to clear tenant job data'], 500); } } + + private function checkPermissions(): void + { + // Check if tenant job tracking is enabled + $enabled = TenantQueueServiceProvider::enabled(); + + if (!$enabled) { + throw new AuthorizationException('Tenant queue tracking is disabled'); + } + + if (!Auth::user()->is_administrator) { + throw new AuthorizationException(); + } + + // If the route binding has a tenant id, check if the user is allowed to access the tenant queue + if ($id = (int) request()->route('tenantId')) { + if (!TenantQueueServiceProvider::allowAllTenats() && $id !== app('currentTenant')?->id) { + throw new AuthorizationException(); + } + } + } } From 612afc4b0e877f9d339ae036e70f29257a983395 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Fri, 24 Oct 2025 17:10:01 -0700 Subject: [PATCH 44/51] Fix docker host url --- ProcessMaker/Multitenancy/SwitchTenant.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProcessMaker/Multitenancy/SwitchTenant.php b/ProcessMaker/Multitenancy/SwitchTenant.php index 59b7c3ac5b..eb5e319881 100644 --- a/ProcessMaker/Multitenancy/SwitchTenant.php +++ b/ProcessMaker/Multitenancy/SwitchTenant.php @@ -65,7 +65,7 @@ private function overrideConfigs(Application $app, IsTenant $tenant) if (!isset($tenant->config['app.docker_host_url'])) { // There is no specific override in the tenant's config so set it to the app url - $newConfig['app.docker_host_url'] = config('app.docker_host_url', config('app.url')); + $newConfig['app.docker_host_url'] = config('app.url'); } // Set config from the entry in the tenants table From 5f26d05dd4e53cda6bd25d0cae4185483e667558 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Mon, 27 Oct 2025 11:22:11 -0700 Subject: [PATCH 45/51] Bump build --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3d2ac7b7cf..29b7417b2c 100644 --- a/composer.json +++ b/composer.json @@ -106,7 +106,7 @@ "Gmail" ], "processmaker": { - "build": "1c5ce226", + "build": "2b742d29", "cicd-enabled": true, "custom": { "package-ellucian-ethos": "1.19.7", 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 46/51] 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() From db0f9a4071df077de423d37108b3c1b40d1336e6 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Mon, 27 Oct 2025 18:54:01 -0700 Subject: [PATCH 47/51] Update dependencies --- composer.json | 22 +++--- package-lock.json | 198 +++++++++++++++++++++++++--------------------- package.json | 6 +- 3 files changed, 124 insertions(+), 102 deletions(-) diff --git a/composer.json b/composer.json index 29b7417b2c..90948517db 100644 --- a/composer.json +++ b/composer.json @@ -145,33 +145,33 @@ "enterprise": { "connector-docusign": "1.11.0", "connector-idp": "1.14.0", - "connector-pdf-print": "1.23.0", - "connector-send-email": "1.32.8", + "connector-pdf-print": "1.23.1", + "connector-send-email": "1.32.9", "connector-slack": "1.9.3", "docker-executor-node-ssr": "1.7.2", "package-ab-testing": "1.4.0", - "package-actions-by-email": "1.22.6", - "package-advanced-user-manager": "1.13.0", - "package-ai": "1.16.9", + "package-actions-by-email": "1.22.7", + "package-advanced-user-manager": "1.13.1", + "package-ai": "1.16.10", "package-analytics-reporting": "1.11.1", "package-auth": "1.24.10", "package-collections": "2.27.0", - "package-comments": "1.16.0", + "package-comments": "1.16.1", "package-conversational-forms": "1.15.0", - "package-data-sources": "1.34.2", + "package-data-sources": "1.34.3", "package-decision-engine": "1.16.1", "package-dynamic-ui": "1.28.2", - "package-email-start-event": "1.0.7", - "package-files": "1.23.0", + "package-email-start-event": "1.0.8", + "package-files": "1.23.1", "package-googleplaces": "1.12.0", "package-photo-video": "1.6.1", - "package-pm-blocks": "1.12.3", + "package-pm-blocks": "1.12.4", "package-process-documenter": "1.12.0", "package-process-optimization": "1.10.0", "package-product-analytics": "1.5.11", "package-projects": "1.12.5", "package-rpa": "1.1.1", - "package-savedsearch": "1.43.4", + "package-savedsearch": "1.43.5", "package-slideshow": "1.4.3", "package-signature": "1.15.2", "package-testing": "1.8.1", diff --git a/package-lock.json b/package-lock.json index 0fb601b87a..8404fe3b7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,10 +20,10 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/vue-fontawesome": "^0.1.9", "@panter/vue-i18next": "^0.15.2", - "@processmaker/modeler": "1.69.17", + "@processmaker/modeler": "1.69.18", "@processmaker/processmaker-bpmn-moddle": "0.16.0", - "@processmaker/screen-builder": "3.8.13", - "@processmaker/vue-form-elements": "0.65.4", + "@processmaker/screen-builder": "3.8.14", + "@processmaker/vue-form-elements": "0.65.5", "@processmaker/vue-multiselect": "2.3.0", "@tinymce/tinymce-vue": "2.0.0", "axios": "^0.27.2", @@ -3800,9 +3800,9 @@ "license": "MIT" }, "node_modules/@processmaker/modeler": { - "version": "1.69.17", - "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.17.tgz", - "integrity": "sha512-X6Ux9yLgt2myers1X6eZhWJwpfZM9Ud2xVWX2tfgnzvwg2t+LL96PxWa+Uo7j9/bdaPgHxKwund0qWK9b4zqLA==", + "version": "1.69.18", + "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.18.tgz", + "integrity": "sha512-kFknN+LOzY8TbUDEPncZBVKLjNk+BU29veT91mSn5/k42YsXn8T7yfw/M0myAIwK+RCe5ctWsDL1MmmBD4u3QQ==", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.12.1", "@fortawesome/fontawesome-free": "^5.11.2", @@ -3810,8 +3810,8 @@ "@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/vue-fontawesome": "^0.1.8", - "@processmaker/screen-builder": "3.8.13", - "@processmaker/vue-form-elements": "0.65.4", + "@processmaker/screen-builder": "3.8.14", + "@processmaker/vue-form-elements": "0.65.5", "@processmaker/vue-multiselect": "2.3.0", "bootstrap": "^4.3.1", "bootstrap-vue": "^2.0.4", @@ -3963,9 +3963,9 @@ } }, "node_modules/@processmaker/screen-builder": { - "version": "3.8.13", - "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.13.tgz", - "integrity": "sha512-NALS0n+V5zY57tyJF+8mC2XVXkrVW2veejbhHrHAZK8AVj778DIOBkiw2COkCHvuDKg/FrE/MpXB/H0RSAJPbA==", + "version": "3.8.14", + "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.14.tgz", + "integrity": "sha512-imcHkMMNO8VO5Gb2MRtaUUw+W8VtUarQlkQjivLXBXudpvvLrwt6Tqsi7Fl7Ug8Z68QOjJmjhU5LBzC34GLCTg==", "dependencies": { "@chantouchsek/validatorjs": "1.2.3", "@storybook/addon-docs": "^7.6.13", @@ -3990,7 +3990,7 @@ }, "peerDependencies": { "@panter/vue-i18next": "^0.15.0", - "@processmaker/vue-form-elements": "0.65.4", + "@processmaker/vue-form-elements": "0.65.5", "i18next": "^15.0.8", "vue": "^2.6.12", "vuex": "^3.1.1" @@ -4032,9 +4032,9 @@ "license": "MIT" }, "node_modules/@processmaker/vue-form-elements": { - "version": "0.65.4", - "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.4.tgz", - "integrity": "sha512-xSPWnc3yBM4aHbfrfMWsN0o6FTsBh0v2Gimequ5x1W64n54DfAjJtDxLef9wDU5HIE41DFZiQuiJByjMuVJ1lw==", + "version": "0.65.5", + "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.5.tgz", + "integrity": "sha512-nIsyRYblLAkviqCwyOAQg8/ZH1+P+tkZ6c/9hmM++dV9G6kyWgb/i6c2vvHSnIrzK1t1CTio+hx764BXpb4dBQ==", "license": "MIT", "dependencies": { "@chantouchsek/validatorjs": "1.2.3", @@ -6163,9 +6163,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.3.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -6236,9 +6236,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -8548,9 +8548,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8561,13 +8561,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -10784,9 +10784,10 @@ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -12175,6 +12176,15 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12192,17 +12202,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -13660,13 +13670,14 @@ "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -15455,15 +15466,20 @@ "license": "MIT" }, "node_modules/markdown-to-jsx": { - "version": "7.7.13", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.13.tgz", - "integrity": "sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==", + "version": "7.7.17", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.17.tgz", + "integrity": "sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==", "license": "MIT", "engines": { "node": ">= 10" }, "peerDependencies": { "react": ">= 0.14.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } } }, "node_modules/material-colors": { @@ -25994,9 +26010,9 @@ "dev": true }, "@processmaker/modeler": { - "version": "1.69.17", - "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.17.tgz", - "integrity": "sha512-X6Ux9yLgt2myers1X6eZhWJwpfZM9Ud2xVWX2tfgnzvwg2t+LL96PxWa+Uo7j9/bdaPgHxKwund0qWK9b4zqLA==", + "version": "1.69.18", + "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.18.tgz", + "integrity": "sha512-kFknN+LOzY8TbUDEPncZBVKLjNk+BU29veT91mSn5/k42YsXn8T7yfw/M0myAIwK+RCe5ctWsDL1MmmBD4u3QQ==", "requires": { "@babel/plugin-proposal-private-methods": "^7.12.1", "@fortawesome/fontawesome-free": "^5.11.2", @@ -26004,8 +26020,8 @@ "@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/vue-fontawesome": "^0.1.8", - "@processmaker/screen-builder": "3.8.13", - "@processmaker/vue-form-elements": "0.65.4", + "@processmaker/screen-builder": "3.8.14", + "@processmaker/vue-form-elements": "0.65.5", "@processmaker/vue-multiselect": "2.3.0", "bootstrap": "^4.3.1", "bootstrap-vue": "^2.0.4", @@ -26109,9 +26125,9 @@ } }, "@processmaker/screen-builder": { - "version": "3.8.13", - "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.13.tgz", - "integrity": "sha512-NALS0n+V5zY57tyJF+8mC2XVXkrVW2veejbhHrHAZK8AVj778DIOBkiw2COkCHvuDKg/FrE/MpXB/H0RSAJPbA==", + "version": "3.8.14", + "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.14.tgz", + "integrity": "sha512-imcHkMMNO8VO5Gb2MRtaUUw+W8VtUarQlkQjivLXBXudpvvLrwt6Tqsi7Fl7Ug8Z68QOjJmjhU5LBzC34GLCTg==", "requires": { "@chantouchsek/validatorjs": "1.2.3", "@storybook/addon-docs": "^7.6.13", @@ -26160,9 +26176,9 @@ } }, "@processmaker/vue-form-elements": { - "version": "0.65.4", - "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.4.tgz", - "integrity": "sha512-xSPWnc3yBM4aHbfrfMWsN0o6FTsBh0v2Gimequ5x1W64n54DfAjJtDxLef9wDU5HIE41DFZiQuiJByjMuVJ1lw==", + "version": "0.65.5", + "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.5.tgz", + "integrity": "sha512-nIsyRYblLAkviqCwyOAQg8/ZH1+P+tkZ6c/9hmM++dV9G6kyWgb/i6c2vvHSnIrzK1t1CTio+hx764BXpb4dBQ==", "requires": { "@chantouchsek/validatorjs": "1.2.3", "@tinymce/tinymce-vue": "2.0.0", @@ -27512,9 +27528,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.3.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -27583,9 +27599,9 @@ } }, "@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", "requires": { "@types/yargs-parser": "*" } @@ -29373,21 +29389,21 @@ } }, "call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "requires": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "callsites": { @@ -31082,9 +31098,9 @@ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "requires": { "es-errors": "^1.3.0" } @@ -32118,6 +32134,11 @@ } } }, + "generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -32129,16 +32150,16 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -33201,12 +33222,13 @@ "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" }, "is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "requires": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } @@ -34521,9 +34543,9 @@ "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==" }, "markdown-to-jsx": { - "version": "7.7.13", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.13.tgz", - "integrity": "sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==", + "version": "7.7.17", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.17.tgz", + "integrity": "sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==", "requires": {} }, "material-colors": { diff --git a/package.json b/package.json index b8b32e5e62..ee867d6387 100644 --- a/package.json +++ b/package.json @@ -60,10 +60,10 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/vue-fontawesome": "^0.1.9", "@panter/vue-i18next": "^0.15.2", - "@processmaker/modeler": "1.69.17", + "@processmaker/modeler": "1.69.18", "@processmaker/processmaker-bpmn-moddle": "0.16.0", - "@processmaker/screen-builder": "3.8.13", - "@processmaker/vue-form-elements": "0.65.4", + "@processmaker/screen-builder": "3.8.14", + "@processmaker/vue-form-elements": "0.65.5", "@processmaker/vue-multiselect": "2.3.0", "@tinymce/tinymce-vue": "2.0.0", "axios": "^0.27.2", From ca0c63bbc9fc87f29014408cc83a59f108e9eec8 Mon Sep 17 00:00:00 2001 From: ProcessMaker Bot <206180840+processmaker-bot@users.noreply.github.com> Date: Tue, 28 Oct 2025 01:55:24 +0000 Subject: [PATCH 48/51] Version 4.15.10+beta-1 --- composer.json | 4 ++-- composer.lock | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 90948517db..3143d577fb 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.10+beta-1", "description": "BPM PHP Software", "keywords": [ "php bpm processmaker" @@ -106,7 +106,7 @@ "Gmail" ], "processmaker": { - "build": "2b742d29", + "build": "62c218fb", "cicd-enabled": true, "custom": { "package-ellucian-ethos": "1.19.7", diff --git a/composer.lock b/composer.lock index 0879d99a8b..dcd9abff5c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bad55f1c553cbb7659c60c10759fe5b1", + "content-hash": "9a050f3cbb7fdd2484dfd8cfddcdbc44", "packages": [ { "name": "aws/aws-crt-php", diff --git a/package-lock.json b/package-lock.json index 8404fe3b7c..435ecc985a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.10", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index ee867d6387..30b9d0f763 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@processmaker/processmaker", - "version": "4.15.9", + "version": "4.15.10", "description": "ProcessMaker 4", "author": "DevOps ", "license": "ISC", From 4f6b3f8678c4a7298ee2153bbfa41580f2e7757f Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Wed, 29 Oct 2025 14:04:46 -0600 Subject: [PATCH 49/51] fix(SmartExtract): adjust non-system task filtering based on HITL configuration --- ProcessMaker/Traits/TaskControllerIndexMethods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 146e5e4c1e..05e993464c 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -155,6 +155,7 @@ private function excludeNonVisibleTasks($query, $request) { $nonSystem = filter_var($request->input('non_system'), FILTER_VALIDATE_BOOLEAN); $allTasks = filter_var($request->input('all_tasks'), FILTER_VALIDATE_BOOLEAN); + $hitlEnabled = filter_var(config('smart-extract.hitl_enabled'), FILTER_VALIDATE_BOOLEAN); $query->when(!$allTasks, function ($query) { $query->where(function ($query) { $query->where('element_type', '=', 'task'); @@ -164,7 +165,7 @@ private function excludeNonVisibleTasks($query, $request) }); }); }) - ->when($nonSystem, function ($query) { + ->when($nonSystem && !$hitlEnabled, function ($query) { $query->nonSystem(); }); } From bf91d32508f9b0b2982143d4b92107b7657781ad Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Wed, 29 Oct 2025 14:49:28 -0600 Subject: [PATCH 50/51] refine non-system task filtering based on HITL configuration --- .../Traits/TaskControllerIndexMethods.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 05e993464c..c653e0c48c 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -165,8 +165,20 @@ private function excludeNonVisibleTasks($query, $request) }); }); }) - ->when($nonSystem && !$hitlEnabled, function ($query) { - $query->nonSystem(); + ->when($nonSystem, function ($query) use ($hitlEnabled) { + if (!$hitlEnabled) { + $query->nonSystem(); + + return; + } + + $query->where(function ($query) { + $query->nonSystem(); + $query->orWhere(function ($query) { + $query->where('element_type', '=', 'task'); + $query->where('element_name', '=', 'Manual Document Review'); + }); + }); }); } From 461c42dda2bacf6ba287186a5118f628c12feca9 Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Wed, 29 Oct 2025 15:02:16 -0600 Subject: [PATCH 51/51] add mergeHitlCaseNumber method to handle case number retrieval from parent request --- ProcessMaker/Http/Resources/Task.php | 26 ++++++++++++++++++++- resources/js/tasks/components/TasksList.vue | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ProcessMaker/Http/Resources/Task.php b/ProcessMaker/Http/Resources/Task.php index cde9101604..fe19ce85b5 100644 --- a/ProcessMaker/Http/Resources/Task.php +++ b/ProcessMaker/Http/Resources/Task.php @@ -74,6 +74,8 @@ public function toArray($request) $this->addAssignableUsers($array, $include); + $this->mergeHitlCaseNumber($array); + return $array; } @@ -114,6 +116,28 @@ private function addAssignableUsers(&$array, $include) } } + private function mergeHitlCaseNumber(array &$array): void + { + if (!config('smart-extract.hitl_enabled')) { + return; + } + + if (!empty(data_get($array, 'process_request.case_number'))) { + return; + } + + $this->processRequest->loadMissing('parentRequest'); + $parentCaseNumber = $this->processRequest->parentRequest?->case_number; + if (!$parentCaseNumber) { + return; + } + + data_set($array, 'process_request.case_number', $parentCaseNumber); + if (empty($array['case_number'])) { + $array['case_number'] = $parentCaseNumber; + } + } + /** * Add the active users to the list of assigned users * @@ -131,7 +155,7 @@ private function addActiveAssignedUsers(array $users, array $assignedUsers) ->whereNotIn('status', Process::NOT_ASSIGNABLE_USER_STATUS) ->whereIn('id', $chunk) ->pluck('id')->toArray(); - $assignedUsers = array_merge($assignedUsers,$activeUsers); + $assignedUsers = array_merge($assignedUsers, $activeUsers); } return $assignedUsers; diff --git a/resources/js/tasks/components/TasksList.vue b/resources/js/tasks/components/TasksList.vue index 340b746566..aae5f56de2 100644 --- a/resources/js/tasks/components/TasksList.vue +++ b/resources/js/tasks/components/TasksList.vue @@ -496,7 +496,7 @@ export default { return ` - # ${processRequest.case_number || record.case_number} + # ${processRequest.case_number || record.case_number || ""} `; }, formatCaseTitle(processRequest, record) {