From 5d6710dabaa08f55bb43e9bddd0f15fe731188c2 Mon Sep 17 00:00:00 2001 From: Teisha McRae Date: Tue, 31 Mar 2026 12:08:15 -0400 Subject: [PATCH 1/4] Skip status clause in PMQL if advanced filter set When an advanced_filter includes a Status condition, remove any status clauses from the incoming PMQL to avoid duplicate or conflicting status filters. Adds helper methods advancedFilterHasStatus() and removeStatusFromPmql(), and returns early if PMQL becomes empty after removal. The removal uses regex to strip status predicates joined by AND and trims the resulting PMQL. --- .../Traits/TaskControllerIndexMethods.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index be27fa8804..2ecafe85a8 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -295,6 +295,14 @@ private function applyPmql($query, $request, $user) { $pmql = $request->input('pmql', ''); if (!empty($pmql)) { + if ($this->advancedFilterHasStatus($request)) { + $pmql = $this->removeStatusFromPmql($pmql); + } + + if (empty($pmql)) { + return; + } + try { $query->pmql($pmql, null, $user); } catch (QueryException $e) { @@ -305,6 +313,36 @@ private function applyPmql($query, $request, $user) } } + private function advancedFilterHasStatus($request): bool + { + $advancedFilter = $request->input('advanced_filter', ''); + if (empty($advancedFilter)) { + return false; + } + + $filterArray = is_string($advancedFilter) ? json_decode($advancedFilter, true) : $advancedFilter; + if (!is_array($filterArray)) { + return false; + } + + foreach ($filterArray as $filter) { + if (isset($filter['subject']['type']) && $filter['subject']['type'] === 'Status') { + return true; + } + } + + return false; + } + + private function removeStatusFromPmql(string $pmql): string + { + $pmql = preg_replace('/\s+AND\s+\(status\s*=\s*"[^"]*"\)/i', '', $pmql); + $pmql = preg_replace('/\(status\s*=\s*"[^"]*"\)\s+AND\s+/i', '', $pmql); + $pmql = preg_replace('/\(status\s*=\s*"[^"]*"\)/i', '', $pmql); + + return trim($pmql); + } + private function applyAdvancedFilter($query, $request) { if ($advancedFilter = $request->input('advanced_filter', '')) { From 45413cf671caa4b2de51a237422a877c4deb0d6c Mon Sep 17 00:00:00 2001 From: Teisha McRae Date: Tue, 31 Mar 2026 13:38:15 -0400 Subject: [PATCH 2/4] Add tests for advanced vs PMQL status filter Add two feature tests to TasksTest that verify status filter precedence for the tasks API. One test (testAdvancedStatusFilterOverridesPmqlStatus) ensures an advanced Status filter overrides a conflicting pmql status clause; the other (testPmqlStatusPreservedWhenNoAdvancedStatusFilter) ensures the pmql status clause is respected when no advanced Status filter is provided. Both tests create an admin user and two ProcessRequestToken tasks (ACTIVE and CLOSED) and assert the returned task IDs match the expected filtering behavior. --- tests/Feature/Api/TasksTest.php | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/Feature/Api/TasksTest.php b/tests/Feature/Api/TasksTest.php index 573b705d63..53386e1e91 100644 --- a/tests/Feature/Api/TasksTest.php +++ b/tests/Feature/Api/TasksTest.php @@ -830,6 +830,75 @@ public function testAdvancedFilterByProcessRequestName() $this->assertEquals($hitTask->id, $json['data'][0]['id']); } + public function testAdvancedStatusFilterOverridesPmqlStatus() + { + $user = User::factory()->create(['is_administrator' => true]); + + $activeTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'user_id' => $user->id, + 'is_self_service' => 0, + ]); + + $completedTask = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'element_type' => 'task', + 'user_id' => $user->id, + 'is_self_service' => 0, + ]); + + $statusFilter = json_encode([ + [ + 'subject' => ['type' => 'Status'], + 'operator' => '=', + 'value' => 'Completed', + ], + ]); + + $response = $this->actingAs($user, 'api')->get(route('api.tasks.index', [ + 'pmql' => '(user_id = ' . $user->id . ') AND (status = "In Progress")', + 'advanced_filter' => $statusFilter, + ])); + + $response->assertStatus(200); + $data = $response->json('data'); + $returnedIds = collect($data)->pluck('id')->toArray(); + + $this->assertContains($completedTask->id, $returnedIds); + $this->assertNotContains($activeTask->id, $returnedIds); + } + + public function testPmqlStatusPreservedWhenNoAdvancedStatusFilter() + { + $user = User::factory()->create(['is_administrator' => true]); + + $activeTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'user_id' => $user->id, + 'is_self_service' => 0, + ]); + + $completedTask = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'element_type' => 'task', + 'user_id' => $user->id, + 'is_self_service' => 0, + ]); + + $response = $this->actingAs($user, 'api')->get(route('api.tasks.index', [ + 'pmql' => '(user_id = ' . $user->id . ') AND (status = "In Progress")', + ])); + + $response->assertStatus(200); + $data = $response->json('data'); + $returnedIds = collect($data)->pluck('id')->toArray(); + + $this->assertContains($activeTask->id, $returnedIds); + $this->assertNotContains($completedTask->id, $returnedIds); + } + public function testGetScreenFields() { $this->be($this->user); From d4858dfd66313b0cf9bf34673047845c2ef14be7 Mon Sep 17 00:00:00 2001 From: Teisha McRae Date: Mon, 27 Apr 2026 15:43:11 -0400 Subject: [PATCH 3/4] Handle self-service status in advanced filters Update task index filtering to recognize a 'Self Service' advanced filter and remove user_id constraints from PMQL so self-service tasks (which may have null user_id) are returned. Added helpers to parse advanced_filter payloads (getAdvancedFilterArray, advancedFilterHasSelfServiceStatus) and a removeUserIdFromPmql method that strips user_id clauses. Also adjusted advancedFilterHasStatus to use the new parser. Added a feature test (testSelfServiceFilterOverridesPmqlStatusAndUserId) to verify the behavior. --- .../Traits/TaskControllerIndexMethods.php | 46 +++++++++++++++---- tests/Feature/Api/TasksTest.php | 40 ++++++++++++++++ 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 2ecafe85a8..d773ffb241 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -297,6 +297,10 @@ private function applyPmql($query, $request, $user) if (!empty($pmql)) { if ($this->advancedFilterHasStatus($request)) { $pmql = $this->removeStatusFromPmql($pmql); + + if ($this->advancedFilterHasSelfServiceStatus($request)) { + $pmql = $this->removeUserIdFromPmql($pmql); + } } if (empty($pmql)) { @@ -314,24 +318,39 @@ private function applyPmql($query, $request, $user) } private function advancedFilterHasStatus($request): bool + { + return !empty($this->getAdvancedFilterArray($request)); + } + + private function advancedFilterHasSelfServiceStatus($request): bool + { + foreach ($this->getAdvancedFilterArray($request) as $filter) { + $values = (array) ($filter['value'] ?? []); + foreach ($values as $v) { + if (mb_strtolower($v) === 'self service') { + return true; + } + } + } + + return false; + } + + private function getAdvancedFilterArray($request): array { $advancedFilter = $request->input('advanced_filter', ''); if (empty($advancedFilter)) { - return false; + return []; } $filterArray = is_string($advancedFilter) ? json_decode($advancedFilter, true) : $advancedFilter; if (!is_array($filterArray)) { - return false; - } - - foreach ($filterArray as $filter) { - if (isset($filter['subject']['type']) && $filter['subject']['type'] === 'Status') { - return true; - } + return []; } - return false; + return array_filter($filterArray, function ($filter) { + return isset($filter['subject']['type']) && $filter['subject']['type'] === 'Status'; + }); } private function removeStatusFromPmql(string $pmql): string @@ -343,6 +362,15 @@ private function removeStatusFromPmql(string $pmql): string return trim($pmql); } + private function removeUserIdFromPmql(string $pmql): string + { + $pmql = preg_replace('/\s+AND\s+\(user_id\s*=\s*\d+\)/i', '', $pmql); + $pmql = preg_replace('/\(user_id\s*=\s*\d+\)\s+AND\s+/i', '', $pmql); + $pmql = preg_replace('/\(user_id\s*=\s*\d+\)/i', '', $pmql); + + return trim($pmql); + } + private function applyAdvancedFilter($query, $request) { if ($advancedFilter = $request->input('advanced_filter', '')) { diff --git a/tests/Feature/Api/TasksTest.php b/tests/Feature/Api/TasksTest.php index 53386e1e91..14279ea88a 100644 --- a/tests/Feature/Api/TasksTest.php +++ b/tests/Feature/Api/TasksTest.php @@ -899,6 +899,46 @@ public function testPmqlStatusPreservedWhenNoAdvancedStatusFilter() $this->assertNotContains($completedTask->id, $returnedIds); } + public function testSelfServiceFilterOverridesPmqlStatusAndUserId() + { + $user = User::factory()->create(['is_administrator' => true]); + $group = Group::factory()->create(); + GroupMember::factory()->create([ + 'group_id' => $group->id, + 'member_id' => $user->id, + 'member_type' => User::class, + ]); + $selfServiceTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'user_id' => null, + 'is_self_service' => 1, + 'self_service_groups' => ['groups' => [strval($group->id)], 'users' => []], + ]); + $regularTask = ProcessRequestToken::factory()->create([ + 'status' => 'ACTIVE', + 'element_type' => 'task', + 'user_id' => $user->id, + 'is_self_service' => 0, + ]); + $statusFilter = json_encode([ + [ + 'subject' => ['type' => 'Status'], + 'operator' => '=', + 'value' => 'Self Service', + ], + ]); + $response = $this->actingAs($user, 'api')->get(route('api.tasks.index', [ + 'pmql' => '(user_id = ' . $user->id . ') AND (status = "In Progress")', + 'advanced_filter' => $statusFilter, + ])); + $response->assertStatus(200); + $data = $response->json('data'); + $returnedIds = collect($data)->pluck('id')->toArray(); + $this->assertContains($selfServiceTask->id, $returnedIds); + $this->assertNotContains($regularTask->id, $returnedIds); + } + public function testGetScreenFields() { $this->be($this->user); From 54d758e173a84416fcc576b4e8a729ab35de583b Mon Sep 17 00:00:00 2001 From: Teisha McRae Date: Mon, 27 Apr 2026 15:53:20 -0400 Subject: [PATCH 4/4] Code cleanup --- tests/Feature/Api/TasksTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Feature/Api/TasksTest.php b/tests/Feature/Api/TasksTest.php index 14279ea88a..c95f360bcf 100644 --- a/tests/Feature/Api/TasksTest.php +++ b/tests/Feature/Api/TasksTest.php @@ -903,11 +903,13 @@ public function testSelfServiceFilterOverridesPmqlStatusAndUserId() { $user = User::factory()->create(['is_administrator' => true]); $group = Group::factory()->create(); + GroupMember::factory()->create([ 'group_id' => $group->id, 'member_id' => $user->id, 'member_type' => User::class, ]); + $selfServiceTask = ProcessRequestToken::factory()->create([ 'status' => 'ACTIVE', 'element_type' => 'task', @@ -915,12 +917,14 @@ public function testSelfServiceFilterOverridesPmqlStatusAndUserId() 'is_self_service' => 1, 'self_service_groups' => ['groups' => [strval($group->id)], 'users' => []], ]); + $regularTask = ProcessRequestToken::factory()->create([ 'status' => 'ACTIVE', 'element_type' => 'task', 'user_id' => $user->id, 'is_self_service' => 0, ]); + $statusFilter = json_encode([ [ 'subject' => ['type' => 'Status'], @@ -928,13 +932,16 @@ public function testSelfServiceFilterOverridesPmqlStatusAndUserId() 'value' => 'Self Service', ], ]); + $response = $this->actingAs($user, 'api')->get(route('api.tasks.index', [ 'pmql' => '(user_id = ' . $user->id . ') AND (status = "In Progress")', 'advanced_filter' => $statusFilter, ])); + $response->assertStatus(200); $data = $response->json('data'); $returnedIds = collect($data)->pluck('id')->toArray(); + $this->assertContains($selfServiceTask->id, $returnedIds); $this->assertNotContains($regularTask->id, $returnedIds); }