Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
170 changes: 152 additions & 18 deletions ProcessMaker/Traits/TaskControllerIndexMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ private function applyPmql($query, $request, $user)
if ($this->advancedFilterHasStatus($request)) {
$pmql = $this->removeStatusFromPmql($pmql);

// Exclusive Self Service: unassigned tasks cannot match a global user_id AND.
// Mixed Self Service + other statuses: also drop the global user_id AND here;
// applyMixedSelfServiceStatusFilter() re-applies it only on the regular-status branch.
if ($this->advancedFilterHasSelfServiceStatus($request)) {
$pmql = $this->removeUserIdFromPmql($pmql);
}
Expand All @@ -350,13 +353,46 @@ private function advancedFilterHasStatus($request): bool

private function advancedFilterHasSelfServiceStatus($request): bool
{
foreach ($this->getAdvancedFilterArray($request) as $filter) {
return $this->filterDefinitionsContainSelfService($this->getAdvancedFilterArray($request));
Comment thread
CarliPinell marked this conversation as resolved.
}

/**
* True when every Status criterion is Self Service (no In Progress / Completed / etc.).
*/
private function advancedFilterIsExclusiveSelfService($request): bool
{
$statusFilters = array_values($this->getAdvancedFilterArray($request));
if ($statusFilters === [] || !$this->filterDefinitionsContainSelfService($statusFilters)) {
return false;
}

foreach ($statusFilters as $filter) {
if ($this->stripSelfServiceFromStatusFilter($filter) !== null) {
return false;
}
}

return true;
}

/**
* Detect "Self Service" in Status filters, including nested `or` chains produced by the
* column filter UI (Status = X OR Status = Self Service).
*/
private function filterDefinitionsContainSelfService(array $filters): bool
{
foreach ($filters as $filter) {
$values = (array) ($filter['value'] ?? []);
foreach ($values as $v) {
if (mb_strtolower($v) === self::SELF_SERVICE_STATUS) {
foreach ($values as $value) {
if (is_string($value) && mb_strtolower($value) === self::SELF_SERVICE_STATUS) {
return true;
}
}

$nestedOr = $filter['or'] ?? [];
if (is_array($nestedOr) && $nestedOr !== [] && $this->filterDefinitionsContainSelfService($nestedOr)) {
return true;
}
}

return false;
Expand All @@ -379,6 +415,52 @@ private function getAdvancedFilterArray($request): array
});
}

/**
* Remove Self Service values from a Status filter (including nested `or`), keeping other statuses.
* Returns null when nothing remains after stripping Self Service.
*/
private function stripSelfServiceFromStatusFilter(array $filter): ?array
{
$values = is_array($filter['value'] ?? null) ? $filter['value'] : [$filter['value'] ?? null];
$values = array_values(array_filter($values, function ($value) {
return $value !== null && $value !== ''
&& (!is_string($value) || mb_strtolower($value) !== self::SELF_SERVICE_STATUS);
}));

$nestedOr = [];
foreach ($filter['or'] ?? [] as $orFilter) {
if (!is_array($orFilter)) {
continue;
}
$stripped = $this->stripSelfServiceFromStatusFilter($orFilter);
if ($stripped !== null) {
$nestedOr[] = $stripped;
}
}

if ($values === [] && $nestedOr === []) {
return null;
}

if ($values === [] && $nestedOr !== []) {
$first = array_shift($nestedOr);
if ($nestedOr !== []) {
$first['or'] = array_values(array_merge($first['or'] ?? [], $nestedOr));
}

return $first;
}

$filter['value'] = count($values) === 1 ? $values[0] : $values;
if ($nestedOr === []) {
unset($filter['or']);
} else {
$filter['or'] = $nestedOr;
}

return $filter;
}

private function removeStatusFromPmql(string $pmql): string
{
$pmql = preg_replace('/\s+AND\s+\(status\s*=\s*"[^"]*"\)/i', '', $pmql);
Expand Down Expand Up @@ -420,24 +502,12 @@ private function applyAdvancedFilter($query, $request)
continue;
}

$values = is_array($filter['value']) ? $filter['value'] : [$filter['value']];
$hasSelfServiceValue = in_array(
self::SELF_SERVICE_STATUS,
array_map(fn ($value) => is_string($value) ? mb_strtolower($value) : $value, $values),
true
);

if ($hasSelfServiceValue) {
if ($this->filterDefinitionsContainSelfService([$filter])) {
$hasSelfServiceFilter = true;
$values = array_values(array_filter($values, function ($value) {
return !is_string($value) || mb_strtolower($value) !== self::SELF_SERVICE_STATUS;
}));

if (empty($values)) {
$filter = $this->stripSelfServiceFromStatusFilter($filter);
if ($filter === null) {
continue;
}

$filter['value'] = is_array($filter['value']) ? $values : $values[0];
}

$statusFilters[] = $filter;
Expand Down Expand Up @@ -468,13 +538,77 @@ private function applyAdvancedFilter($query, $request)
} else {
$query->whereIn('process_request_tokens.id', $selfServiceTaskIds);
}
} elseif (
is_array($filterArray)
&& $this->filterDefinitionsContainSelfService(array_values($this->getAdvancedFilterArray($request)))
&& !$this->advancedFilterIsExclusiveSelfService($request)
) {
$this->applyMixedSelfServiceStatusFilter($query, $filterArray, $request);
} else {
// Normal behavior - apply the filter as-is
Filter::filter($query, $advancedFilter);
}
}
}

/**
* Mixed Status filter (e.g. In Progress OR Self Service):
* (assignee AND regular statuses) OR eligible Self Service tasks.
*/
private function applyMixedSelfServiceStatusFilter($query, array $filterArray, $request): void
{
$nonStatusFilters = [];
$regularStatusFilters = [];

foreach ($filterArray as $filter) {
$isStatusFilter = isset($filter['subject']['type']) && $filter['subject']['type'] === 'Status';
if (!$isStatusFilter) {
$nonStatusFilters[] = $filter;
continue;
}

$stripped = $this->stripSelfServiceFromStatusFilter($filter);
if ($stripped !== null) {
$regularStatusFilters[] = $stripped;
}
}

if ($nonStatusFilters !== []) {
Filter::filter($query, $nonStatusFilters);
}

$requestedUserId = $this->parseUserIdFromPmql((string) $request->input('pmql', ''));
$user = $request->user() ?? Auth::user();

$query->where(function ($query) use ($regularStatusFilters, $requestedUserId, $user) {
$query->where(function ($regularQuery) use ($regularStatusFilters, $requestedUserId) {
if ($regularStatusFilters !== []) {
Filter::filter($regularQuery, $regularStatusFilters);
}
if ($requestedUserId) {
$regularQuery->where('process_request_tokens.user_id', $requestedUserId);
}
});

if ($user) {
$query->orWhereIn('process_request_tokens.id', $user->availableSelfServiceTasksQuery());
}
});
}

private function parseUserIdFromPmql(string $pmql): ?int
{
if ($pmql === '') {
return null;
}

if (preg_match('/user_id\s*=\s*(\d+)/i', $pmql, $matches)) {
return (int) $matches[1];
}

return null;
}

private function applyUserFilter($response, $request, $user)
{
// Only filter results if the user id was specified
Expand Down
32 changes: 32 additions & 0 deletions resources/js/common/PMColumnFilterPopoverCommonMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,41 @@ const PMColumnFilterCommonMixin = {
},
onClear(index) {
this.advancedFilter[index] = [];
// Inbox/tasks: clearing Status restores the default tray filter (In Progress, etc.)
// so Clear does not leave "no status" (all tasks) or fail to persist.
if (index === "status" && this.shouldRestoreDefaultTaskStatusFilter()) {
this.advancedFilter.status = [this.buildDefaultTaskStatusFilter()];
}
this.markStyleWhenColumnSetAFilter();
this.storeFilterConfiguration();
this.fetch(true);
},
shouldRestoreDefaultTaskStatusFilter() {
return typeof this.filterConfiguration === "function"
&& this.filterConfiguration()?.type === "taskFilter";
},
getDefaultTaskInboxStatus() {
const statusParam = new URL(document.location).searchParams.get("status");
switch (statusParam) {
case "CLOSED":
return "Completed";
case "SELF_SERVICE":
return "Self Service";
default:
return "In Progress";
}
},
buildDefaultTaskStatusFilter() {
return {
subject: {
type: "Status",
},
operator: "=",
value: this.getDefaultTaskInboxStatus(),
_column_field: "status",
_column_label: "Status",
};
},
onChangeSort(value, field) {
this.setOrderByProps(field, value);
this.markStyleWhenColumnSetAFilter();
Expand All @@ -182,6 +213,7 @@ const PMColumnFilterCommonMixin = {
Object.keys(filterCopy).forEach((key) => {
if (filterCopy[key].length === 0) {
delete filterCopy[key];
return;
}
const label = this.tableHeaders.find(column => column.field === key)?.label;
this.addAliases(filterCopy[key], key, label);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<b-form-group :key="'logical' + index"
v-if="switchLogical(index)">
<b-form-select v-model="item.logical"
:options="getLogicals()"
:options="getLogicals(index)"
:data-cy="'logical' + index"
class="pm-filter-form-logical-operators"
@change="onChangeLogicalOp(item,index)"
Expand Down Expand Up @@ -121,6 +121,7 @@
this.$emit("onChangeSort", value);
},
onApply() {
this.normalizeStatusLogicals();
let json = this.getValues();
this.$emit("onApply", json);
},
Expand All @@ -134,22 +135,47 @@
},
onClickButtonAdd() {
this.addItem(this.items.length);
this.normalizeStatusLogicals();
},
onClickButtonRemove(item, index) {
if (this.items.length === 1) {
return;
}
this.removeItem(index);
this.normalizeStatusLogicals();
},
onChangeOperator(item) {
this.switchViewControl(item);
this.normalizeStatusLogicals();
},
onChangeLogicalOp() {
onChangeLogicalOp(item, index) {
if (this.requiresStatusOrLogical(index)) {
item.logical = "or";
}
},
setValues(json) {
let items = this.transformToFilterSyntax(json);
this.normalizeStatusLogicals(items);
this.items = items;
},
/**
* Status is a single-value enum column: AND between different statuses can never match.
* Force OR so multi-status filters mean "any of these statuses".
*/
isStatusColumn() {
return this.value === "status";
},
requiresStatusOrLogical(index) {
return this.isStatusColumn() && this.switchLogical(index);
},
normalizeStatusLogicals(items = this.items) {
if (!this.isStatusColumn()) {
return;
}
for (let i = 0; i < items.length - 1; i++) {
items[i].logical = "or";
}
},
getValues() {
let json = JSON.parse(JSON.stringify(this.items));
return this.transformToPmSyntax(json);
Expand Down Expand Up @@ -236,7 +262,10 @@
}
return operators;
},
getLogicals() {
getLogicals(index) {
if (this.requiresStatusOrLogical(index)) {
return [{ value: "or", text: "or" }];
}
return [
{value: "and", text: "and"},
{value: "or", text: "or"}
Expand Down
Loading
Loading