From 5d6710dabaa08f55bb43e9bddd0f15fe731188c2 Mon Sep 17 00:00:00 2001 From: Teisha McRae Date: Tue, 31 Mar 2026 12:08:15 -0400 Subject: [PATCH 1/2] 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/2] 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);