Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
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
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 @@ -126,6 +127,9 @@ class TaskController extends Controller
*/
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);
}
// 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
82 changes: 82 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,33 @@

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;

class TaskController extends Controller
{
use TaskControllerIndexMethods;

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

protected $defaultFields = [
'id',
'element_id',
Expand All @@ -29,6 +42,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 +214,23 @@ 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);
}
}
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
77 changes: 77 additions & 0 deletions ProcessMaker/Http/Middleware/Etag/TasksPageEtag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace ProcessMaker\Http\Middleware\Etag;

use Closure;
use Illuminate\Http\Request;
use ProcessMaker\Http\Resources\Caching\TasksPageEtag as TasksPageEtagResource;
use Symfony\Component\HttpFoundation\Response;

class TasksPageEtag
{
public function __construct(
private TasksPageEtagResource $tasksPageEtag
) {
}

/**
* Handle Tasks page validation before rendering the full page shell.
*
* The ETag is computed from stable page context before controller execution, so a
* matching conditional request can return 304 without paying the render cost.
*/
public function handle(Request $request, Closure $next): Response
{
if (!config('etag.enabled') || (!$request->isMethod('GET') && !$request->isMethod('HEAD'))) {
return $next($request);
}

$etag = $this->tasksPageEtag->getEtag($request);

if ($this->buildResponseWithEtag($etag)->isNotModified($request)) {
return $this->withPrivateCacheHeaders($this->buildNotModifiedResponse($etag, $request));
}

$response = $next($request);
$response->setEtag($etag);

return $this->withPrivateCacheHeaders($response);
}

/**
* Build a framework-compatible 304 response for a matched Tasks page ETag.
*/
private function buildNotModifiedResponse(string $etag, Request $request): Response
{
$response = $this->buildResponseWithEtag($etag);
$response->isNotModified($request);

return $response;
}

/**
* Create a response carrying the Tasks page validator.
*
* Weak ETags are used because the HTML may be transformed by gzip while the
* rendered representation remains equivalent for browser revalidation.
*/
private function buildResponseWithEtag(string $etag): Response
{
$response = new Response();
$response->setEtag($etag, true);

return $response;
}

/**
* Apply browser-cache headers that allow private conditional revalidation.
*/
private function withPrivateCacheHeaders(Response $response): Response
{
$response->headers->set('Cache-Control', 'private, must-revalidate');
$response->headers->remove('Pragma');
$response->headers->remove('Expires');

return $response;
}
}
Loading
Loading