Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
21054c6
FOUR-32126: INBOX >> First loading is to slow
rodriquelca Jul 28, 2026
b607dd8
feat: implement ETag middleware for Tasks page caching
devmiguelangel Jun 10, 2026
5ecf0f4
feat: update inbox and tasks routes to use ETag middleware for improv…
devmiguelangel Jun 10, 2026
8677dab
feat: enhance BrowserCache middleware to return response if ETag head…
devmiguelangel Jun 10, 2026
89c0183
test: add ETag handling tests for Tasks page to ensure proper caching…
devmiguelangel Jun 10, 2026
34bb989
feat: optimize TasksPageEtag for permission handling and user configu…
devmiguelangel Jun 11, 2026
c4f7e1f
fix: update inbox route to use no-cache middleware for improved respo…
devmiguelangel Jun 11, 2026
fd5cebc
docs: add Tasks Page ETag Sequence documentation
devmiguelangel Jun 11, 2026
9cf4563
feat: add api client caching functionality
devmiguelangel Jun 18, 2026
7b3f11f
feat: add debug logging to api client cache for improved diagnostics
devmiguelangel Jun 18, 2026
dfc6091
feat(FOUR-31831): review Improve Task page loading
rodriquelca Jun 19, 2026
40fc24a
fix: code style
rodriquelca Jun 19, 2026
e01a75f
feat: enhance api client cache with additional utility functions and …
devmiguelangel Jun 19, 2026
93f4e68
feat: return original request in api client cache to maintain promise…
devmiguelangel Jun 19, 2026
4489b89
test: add comprehensive tests for api client cache functionality
devmiguelangel Jun 19, 2026
cb592e8
Adding indexOptimized method to taskController api-1.1
AugustoLopezProcess Jun 19, 2026
4ee7901
Adding optimization parameter to nonSystem
AugustoLopezProcess Jun 19, 2026
a5ce0c5
Adding taskOptimized route to api.php v1.1
AugustoLopezProcess Jun 19, 2026
e4a1759
Adding indexOptimizedBaseQuery method
AugustoLopezProcess Jun 19, 2026
8a510a9
Adding config value from env, to activate the optimized endpoint on 1.1
AugustoLopezProcess Jun 23, 2026
c192ab0
feat(FOUR-32800): add v1.1 task completion endpoint with raw SQL path
gproly Aug 28, 2026
6795511
feat(FOUR-32801): improve Jobs execution when the PUT tasks endpoint …
rodriquelca Aug 28, 2026
3fc16e0
style(FOUR-32800): reorder imports in task completion raw service files
gproly Aug 28, 2026
b55a89c
Merge branch 'develop' into feature/FOUR-32800
gproly Sep 2, 2026
05c05a4
perf(FOUR-32947): reduce N+1 queries on requests listing (FOUR-32947)
gproly Sep 4, 2026
a5f315b
FOUR-32800: test compares legacy and optimized
pmPaulis Sep 7, 2026
da5e64f
Merge pull request #9021 from ProcessMaker/bugfix/FOUR-32801
pmPaulis Sep 8, 2026
5e7ca61
Merge pull request #9020 from ProcessMaker/feature/FOUR-32800
pmPaulis Sep 8, 2026
c2ea6f6
feat(FOUR-32800): solving observation about starts
pmPaulis Sep 8, 2026
daafb12
Merge pull request #9046 from ProcessMaker/feature/FOUR-32800
pmPaulis Sep 8, 2026
f600087
feat: optimize notification settings retrieval in Process model
devmiguelangel Sep 10, 2026
b36b825
feat: enhance process retrieval logic by refining relationship handli…
devmiguelangel Sep 10, 2026
a550247
feat: correct query parameters in ProcessMixin to ensure proper data …
devmiguelangel Sep 10, 2026
fdd28de
test: add tests for process listing, including pagination, sorting, a…
devmiguelangel Sep 11, 2026
009273c
Merge branch 'develop' into task/FOUR-32144
gproly Sep 28, 2026
c527057
Merge remote-tracking branch 'origin/develop' into FOUR-32946
devmiguelangel Oct 5, 2026
499436d
fix(FOUR-33645): guard activity execution time metric when ms timesta…
gproly Oct 6, 2026
e314c13
Merge pull request #9045 from ProcessMaker/task/FOUR-32144
pmPaulis Oct 8, 2026
2d97d70
Merge pull request #9056 from ProcessMaker/FOUR-32946
pmPaulis Oct 8, 2026
c1e2d66
Merge pull request #9036 from ProcessMaker/feature/FOUR-32947
pmPaulis Oct 8, 2026
8ab48d6
Merge pull request #9090 from ProcessMaker/bugfix/FOUR-33645
pmPaulis Oct 8, 2026
a7216fd
Merge pull request #8942 from ProcessMaker/feature/FOUR-32126
pmPaulis Oct 8, 2026
462794b
FOUR-30918: phase 1 perfomance
pmPaulis Oct 19, 2026
3031089
FOUR-32144: Phase 3: Platform Performance
pmPaulis Oct 19, 2026
d80baef
Bumped version
pmPaulis Oct 19, 2026
522c353
Pin FOUR-30918 JS branch prereleases and savedsearch dev branch.
nolanpro Oct 20, 2026
5b71be6
Version 2026.17.1-perf1
nolanpro Oct 20, 2026
de8d3e1
fixing version name
pmPaulis Oct 23, 2026
4b6f9ce
updating with develop
pmPaulis Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 20 additions & 47 deletions ProcessMaker/Http/Controllers/Api/ProcessController.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,21 @@ public function index(Request $request)
$user = Auth::user();

$orderBy = $this->getRequestSortBy($request, 'name');
if (!str_contains($orderBy[0], '.')) {
$orderBy[0] = 'processes.' . $orderBy[0];
}
$perPage = $this->getPerPage($request);
$include = $this->getRequestInclude($request);
$relationships = array_values(array_diff($include, ['events']));
$status = $request->input('status');
$pmql = $request->input('pmql', '');

$processes = Process::nonSystem()->notArchived()->with($include);
$processes = Process::nonSystem()->notArchived()->with($relationships);
if ($status === 'archived') {
$processes = Process::archived()->with($include);
$processes = Process::archived()->with($relationships);
}
if ($status === 'all') {
$processes = Process::active()->with($include);
$processes = Process::active()->with($relationships);
}

$filter = $request->input('filter', '');
Expand Down Expand Up @@ -188,60 +192,29 @@ public function index(Request $request)
// Get with launchpad
$launchpad = $request->input('launchpad', false);

$processes = $processes->with('events')
->select('processes.*')
$processes = $processes
->with('notification_settings')
->exclude(['bpmn', 'svg'])
->leftJoin(\DB::raw('(select id, uuid, name from process_categories) as category'), 'processes.process_category_id', '=', 'category.id')
->leftJoin(\DB::raw('(select id, uuid, username, lastname, firstname from users) as user'), 'processes.user_id', '=', 'user.id')
->orderBy(...$orderBy)
->get()
->collect();

foreach ($processes as $key => $process) {
// filter the start events that can be used manually (no timer start events);
// TODO: startEvents is not a real property on Process.
// Move below to $process->getManualStartEvents();
$process->startEvents = $process->events->filter(function ($event) {
$eventIsTimerStart = collect($event['eventDefinitions'])
->filter(function ($eventDefinition) {
return $eventDefinition['$type'] == 'timerEventDefinition';
})->count() > 0;

// Filter out web entry start events and email start events
$eventIsWebEntry = false;
$eventIsEmailStart = false;
if (isset($event['config'])) {
$config = json_decode($event['config'], true);
if (isset($config['web_entry']) && $config['web_entry'] !== null) {
$eventIsWebEntry = true;
} elseif (isset($config['email_start']) && $config['email_start'] !== null) {
$eventIsEmailStart = true;
}
}
->paginate($perPage);

return !$eventIsTimerStart && !$eventIsWebEntry && !$eventIsEmailStart;
})->values();
foreach ($processes as $process) {
$process->setAppends(array_values(array_diff($process->getAppends(), ['projects'])));
$process->makeHidden('notification_settings');

// Get the id bookmark related
$process->bookmark_id = Bookmark::getBookmarked($bookmark, $process->id, $user->id);
// Get the launchpad configuration
$process->launchpad = ProcessLaunchpad::getLaunchpad($launchpad, $process->id);
$process->bookmark_id = $bookmark
? Bookmark::getBookmarked($bookmark, $process->id, $user->id)
: 0;
$process->launchpad = $launchpad
? ProcessLaunchpad::getLaunchpad($launchpad, $process->id)
: null;

$process->case_retention_tier_adjustment_notice = false;
if ($user->is_administrator && config('app.case_retention_policy_enabled')) {
$process->case_retention_tier_adjustment_notice = CaseRetentionTierService::adjustmentNoticeIsActive($process);
}

// Filter all processes that have event definitions (start events like message event, conditional event, signal event, timer event)
if ($request->has('without_event_definitions') && $request->input('without_event_definitions') == 'true') {
$startEvents = $process->events->filter(function ($event) {
return collect($event['eventDefinitions'])->isEmpty();
});
}

// filter only valid executable processes
if (!$process->isValidForExecution()) {
$processes->startEvents = [];
}
}

return new ProcessCollection($processes);
Expand Down
14 changes: 11 additions & 3 deletions ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
use ProcessMaker\Nayra\Contracts\Bpmn\CatchEventInterface;
use ProcessMaker\Notifications\ProcessCanceledNotification;
use ProcessMaker\Query\SyntaxError;
use ProcessMaker\RetryProcessRequest;
use ProcessMaker\Repositories\ProcessRequestListingRawRepository;
use ProcessMaker\Traits\ProcessMapTrait;
use Symfony\Component\HttpFoundation\IpUtils;
use Throwable;
Expand Down Expand Up @@ -117,7 +117,7 @@ public function index(Request $request, $getTotal = false, User $user = null)
$query = ProcessRequest::forUser($user);
$includes = $request->input('include', '');
foreach (array_filter(explode(',', $includes)) as $include) {
if (in_array($include, ProcessRequest::$allowedIncludes)) {
if (in_array($include, ProcessRequest::$allowedIncludes) && $include !== 'process') {
$query->with($include);
}
}
Expand Down Expand Up @@ -176,6 +176,7 @@ public function index(Request $request, $getTotal = false, User $user = null)
->withAggregate('processVersion', 'alternative')
->paginate($request->input('per_page', 10));
$total = $response->total();
app(ProcessRequestListingRawRepository::class)->hydrate($response, $includes);
}
} catch (QueryException $e) {
throw $e;
Expand Down Expand Up @@ -900,7 +901,14 @@ public function getRequestsByCase(Request $request, User $user = null)

// Apply ordering only if a valid order_by field is provided
$query->applyOrdering($request);
$response = $query->applyPagination($request);
$response = $query
->select('process_requests.*')
->withAggregate('processVersion', 'alternative')
->applyPagination($request);
app(ProcessRequestListingRawRepository::class)->hydrate(
$response,
$request->input('include', '')
);

// Get activeTasks and participants
$response = $response->map(function ($processRequest) use ($request) {
Expand Down
4 changes: 4 additions & 0 deletions ProcessMaker/Http/Controllers/Api/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ProcessMaker\Events\ActivityReassignment;
use ProcessMaker\Facades\WorkflowManager;
use ProcessMaker\Filters\Filter;
use ProcessMaker\Http\Controllers\Api\V1_1\TaskController as V1_1TaskController;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Http\Resources\ApiResource;
use ProcessMaker\Http\Resources\Task as Resource;
Expand Down Expand Up @@ -134,6 +135,9 @@ private function processExecutionRaw(): ProcessExecutionRawRepository
*/
public function index(Request $request, $getTotal = false, User $user = null)
{
if (config('app.processmaker_optimized_tasks_enabled')) {
return (new V1_1TaskController())->indexOptimized($request, $getTotal, $user);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimized tasks controller missing constructor argument

High Severity

GET /api/1.0/tasks now does new V1_1TaskController() while that class requires TaskCompletionRawService. Manual new skips the container, so the inbox path throws ArgumentCountError whenever processmaker_optimized_tasks_enabled is on, which is the default.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d80baef. Configure here.

// If a specific user is specified, use it; otherwise use the authorized user
// This is necessary to produce accurate counts for Saved Searches
if (!$user) {
Expand Down
112 changes: 112 additions & 0 deletions ProcessMaker/Http/Controllers/Api/V1_1/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,41 @@

namespace ProcessMaker\Http\Controllers\Api\V1_1;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Cache\Screens\ScreenCacheFactory;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Http\Resources\TaskCollection;
use ProcessMaker\Http\Resources\V1_1\TaskInterstitialResource;
use ProcessMaker\Http\Resources\V1_1\TaskResource;
use ProcessMaker\Http\Resources\V1_1\TaskScreen;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Models\User;
use ProcessMaker\ProcessTranslations\TranslationManager;
use ProcessMaker\Traits\TaskControllerIndexMethods;
use ProcessMaker\Services\TaskCompletionRawService;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;


class TaskController extends Controller
{
use TaskControllerIndexMethods;

public function __construct(
private readonly TaskCompletionRawService $taskCompletionRawService,
) {
}

public $doNotSanitize = [
'data',
'pmql',
];

protected $defaultFields = [
'id',
'element_id',
Expand All @@ -29,6 +50,56 @@ class TaskController extends Controller
'process_request_id',
];

public function indexOptimized(Request $request, $getTotal = false, ?User $user = null)
{
if (!$user) {
$user = Auth::user();
}

$request->merge(['optimized' => true]);

$query = $this->indexOptimizedBaseQuery($request);
$this->applyIndexFieldSelection($query, $request);
$this->applyFilters($query, $request);
$this->excludeNonVisibleTasks($query, $request);
$this->applyColumnOrdering($query, $request);
$this->applyStatusFilter($query, $request);

if ($request->input('processesIManage') === 'true') {
$this->applyProcessManager($query, $user, $request);
} else {
$this->applyForCurrentUser($query, $user);
}

$this->applyPmql($query, $request, $user);
$this->applyAdvancedFilter($query, $request);
$query->overdue($request->input('overdue'));

if ($getTotal === true) {
return $query->count();
}

try {
$response = $query->paginate($request->input('per_page', 10));
} catch (QueryException $e) {
return $this->handleQueryException($e);
}

$response = $this->applyUserFilter($response, $request, $user);

if ($response->total() > 0 && $request->input('processesIManage') === 'true') {
$this->enableUserManager($user);
}

$inOverdueQuery = ProcessRequestToken::query()
->whereIn('id', $response->pluck('id'))
->where('due_at', '<', Carbon::now());

$response->inOverdue = $inOverdueQuery->count();

return new TaskCollection($response);
}

/**
* Display a listing of the resource.
*/
Expand Down Expand Up @@ -151,4 +222,45 @@ public function showInterstitial($taskId)

return $response;
}

private function handleQueryException(QueryException $e)
{
$regex = '~Column not found: 1054 Unknown column \'(.*?)\' in \'where clause\'~';

preg_match($regex, $e->getMessage(), $m);

$message = __('PMQL Is Invalid.');

if (count($m) > 1) {
$message .= ' ' . __('Column not found: ') . '"' . $m[1] . '"';
}

\Log::error($e->getMessage());

return response([
'message' => $message,
], 422);
}

/**
* Complete a task using the raw-query optimized path.
*/
public function update(Request $request, int $taskId)
{
if ($request->input('status') !== 'COMPLETED') {
abort(422, __('Only task completion is supported on this endpoint. Use PUT /api/1.0/tasks/{id} for other updates.'));
}

try {
$task = $this->taskCompletionRawService->completeTask(
$taskId,
json_optimize_decode($request->getContent(), true) ?: [],
$request->user(),
);
} catch (NotFoundHttpException $exception) {
return response()->json(['message' => $exception->getMessage()], 404);
}

return response()->json($task);
}
}
23 changes: 15 additions & 8 deletions ProcessMaker/Http/Controllers/TaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,16 @@ public function edit(ProcessRequestToken $task, string $preview = '')
{
$task = $task->loadTokenInstance();
$dataManager = new DataManager();
$userHasComments = Comment::where('commentable_type', ProcessRequestToken::class)
->where('commentable_id', $task->id)
->where('body', 'like', '%{{' . \Auth::user()->id . '}}%')
->count() > 0;

if (!\Auth::user()->can('update', $task) && !$userHasComments) {
$this->authorize('update', $task);
if (!\Auth::user()->can('update', $task)) {
$userHasComments = Comment::where('commentable_type', ProcessRequestToken::class)
->where('commentable_id', $task->id)
->where('body', 'like', '%{{' . \Auth::user()->id . '}}%')
->count() > 0;

if (!$userHasComments) {
$this->authorize('update', $task);
}
}

//Mark notification as read
Expand Down Expand Up @@ -183,7 +186,9 @@ public function edit(ProcessRequestToken $task, string $preview = '')
]);
}

UserResourceView::setViewed(Auth::user(), $task);
dispatch(function () use ($task) {
UserResourceView::setViewed(Auth::user(), $task);
})->afterResponse();
$currentUser = Auth::user()->only([
'id',
'username',
Expand All @@ -194,11 +199,13 @@ public function edit(ProcessRequestToken $task, string $preview = '')
'timezone',
'datetime_format',
]);
$userConfiguration = (new UserConfigurationController())->index();
$userConfiguration = app(UserConfigurationController::class)->index();
[$hitlEnabled, $iframeSrc] = $this->smartExtractHitlConfiguration($task, $isSmartExtractTask);
$canUpdateTask = Auth::user()->can('update', $task);

return view('tasks.edit', [
'task' => $task,
'canUpdateTask' => $canUpdateTask,
'dueLabels' => self::$dueLabels,
'manager' => $manager,
'submitUrl' => $submitUrl,
Expand Down
4 changes: 4 additions & 0 deletions ProcessMaker/Http/Middleware/BrowserCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public function handle($request, Closure $next)
return $response;
}

if ($response->headers->has('ETag')) {
return $response;
}

$response->header('pragma', 'no-cache');
$response->header('Cache-Control', 'no-store');

Expand Down
Loading
Loading