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
167 changes: 164 additions & 3 deletions resources/js/common/PMColumnFilterPopoverCommonMixin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { get, cloneDeep } from "lodash";
import { isTaskStatusColumnFilter } from "./setDefaultAdvancedFilterStatus";

const PMColumnFilterCommonMixin = {
props: {
Expand Down Expand Up @@ -37,19 +38,69 @@ const PMColumnFilterCommonMixin = {
});
},
methods: {
storeFilterConfiguration() {
const {order, type} = this.filterConfiguration();
/**
* Task inbox bootstrap only: if no Status column filter is present, default to the same
* behavior as tasks/index.js (In Progress, or Completed / Self Service from URL query).
* Do not call from storeFilterConfiguration()/onClear() or a user clear is silently restored.
*/
applyTaskInboxDefaultStatusIfMissing() {
if (typeof this.filterConfiguration !== "function") {
return;
}
if (this.filterConfiguration().type !== "taskFilter") {
return;
}
if (this.advancedFilterProp !== null) {
return;
}
const savedSearch = this.$props.savedSearch;
if (savedSearch !== false && savedSearch != null && savedSearch !== "") {
return;
}
this.advancedFilterInit();
const statusFilters = this.advancedFilter.status;
if (Array.isArray(statusFilters) && statusFilters.length > 0) {
return;
}

let value = "In Progress";
try {
const params = new URL(document.location).searchParams;
if (params.get("status") === "CLOSED") {
value = "Completed";
} else if (params.get("status") === "SELF_SERVICE") {
value = "Self Service";
}
} catch (e) {
// no window (tests)
}

this.advancedFilter.status = [
{
subject: { type: "Field", value: "status" },
operator: "=",
value,
_column_field: "status",
_column_label: "Status",
},
];
},
storeFilterConfiguration() {
// If advanced filter was provided as a prop, do not save the filter
// or overwrite the global advanced_filter, instead emit the filter.
if (this.advancedFilterProp !== null) {
const { order } = this.filterConfiguration();
this.$emit("advanced-filter-updated", {
filters: this.formattedFilter(),
order
});
return;
}

// Do not re-apply inbox Status defaults here: store/clear are user-initiated.
// Defaults belong in getFilterConfiguration() during bootstrap/reload only.
const { order, type } = this.filterConfiguration();

let url = "users/store_filter_configuration/";
if (this.$props.columns && this.savedSearch) {
url += "savedSearch|" + this.savedSearch;
Expand Down Expand Up @@ -151,8 +202,99 @@ const PMColumnFilterCommonMixin = {
getAliasColumnForOrderBy(value) {
return this.tableHeaders.find(column => column.field === value)?.order_column || value;
},
/**
* Group advanced_filter rows by column. Some saved payloads omit _column_field or use "N/A",
* so status (or process version) rows were split across buckets and never merged into `in`.
*/
resolveAdvancedFilterGroupKey(filter) {
let key = filter._column_field;
if (!key || key === "N/A") {
if (isTaskStatusColumnFilter(filter)) {
return "status";
}
if (filter.subject?.value === "process_version_alternative") {
return "process_version_alternative";
}
return "N/A";
}
return key;
},
/**
* Normalize legacy persisted filters only: flat sibling "=" rows on enum columns are AND-ed
* by the API and never match. Collapse them into nested `or` (same semantics as IN).
* New filters from PMColumnFilterForm already use explicit OR for stringSelect columns;
* this helper leaves OR-nested payloads unchanged.
*
* @param {Array} filters - Filters for one column
* @param {string} columnField - Column field name (e.g. status)
* @returns {Array}
*/
mergeFlatEnumEqualsToOrChainForColumn(filters, columnField) {
Comment thread
CarliPinell marked this conversation as resolved.
if (!['status', 'process_version_alternative'].includes(columnField)) {
return filters;
}
if (!Array.isArray(filters) || filters.length < 2) {
return filters;
}
const hasNestedOr = filters.some((f) => Array.isArray(f.or) && f.or.length > 0);
if (hasNestedOr) {
return filters;
}
const allEquals = filters.every((f) => f.operator === '=');
if (!allEquals) {
return filters;
}
const seen = new Set();
const uniqueRows = [];
for (const f of filters) {
if (seen.has(f.value)) {
continue;
}
seen.add(f.value);
uniqueRows.push(f);
}
if (uniqueRows.length < 2) {
return uniqueRows;
}
const subjectSource =
filters.find((f) => f.subject?.value === columnField)
Comment thread
CarliPinell marked this conversation as resolved.
|| filters.find((f) => f.subject?.type === "Field" && f.subject?.value)
|| filters.find((f) => isTaskStatusColumnFilter(f));
const defaultSubject = subjectSource
? { ...subjectSource.subject }
: { type: "Field", value: columnField };
const subjectFor = (row) =>
row.subject && (row.subject.value !== undefined || row.subject.type === "Status")
? { ...row.subject }
: defaultSubject;
const equalsLeaf = (row) => ({
subject: subjectFor(row),
operator: "=",
value: row.value,
});
let node = equalsLeaf(uniqueRows[uniqueRows.length - 1]);
Comment thread
CarliPinell marked this conversation as resolved.
for (let i = uniqueRows.length - 2; i >= 1; i--) {
const row = uniqueRows[i];
node = {
subject: subjectFor(row),
operator: "=",
value: row.value,
or: [node],
};
}
const first = uniqueRows[0];
return [
{
subject: subjectFor(first),
operator: "=",
value: first.value,
or: [node],
},
];
},
onApply(json, index) {
this.advancedFilterInit();
// Form emits explicit OR for stringSelect "=" rows; merge is a no-op for those payloads.
this.advancedFilter[index] = json;
this.markStyleWhenColumnSetAFilter();
this.storeFilterConfiguration();
Expand Down Expand Up @@ -182,7 +324,9 @@ const PMColumnFilterCommonMixin = {
Object.keys(filterCopy).forEach((key) => {
if (filterCopy[key].length === 0) {
delete filterCopy[key];
return;
}
filterCopy[key] = this.mergeFlatEnumEqualsToOrChainForColumn(filterCopy[key], key);
const label = this.tableHeaders.find(column => column.field === key)?.label;
this.addAliases(filterCopy[key], key, label);
});
Expand Down Expand Up @@ -303,14 +447,31 @@ const PMColumnFilterCommonMixin = {
}

inputAdvancedFilter.forEach((filter) => {
const key = filter._column_field || 'N/A';
const key = this.resolveAdvancedFilterGroupKey(filter);
if (!(key in filters)) {
filters[key] = [];
}
filters[key].push(filter);
});
Object.keys(filters).forEach((key) => {
filters[key] = this.mergeFlatEnumEqualsToOrChainForColumn(filters[key], key);
});
this.advancedFilter = filters;

this.applyTaskInboxDefaultStatusIfMissing();

if (
this.advancedFilterProp === null &&
typeof this.filterConfiguration === "function" &&
this.filterConfiguration().type === "taskFilter"
) {
window.ProcessMaker.advanced_filter = {
...get(window, "ProcessMaker.advanced_filter", {}),
filters: this.formattedFilter(),
order: order || get(window, "ProcessMaker.advanced_filter.order"),
};
}

if (order?.by && order?.direction) {
this.setOrderByProps(order.by, order.direction);
}
Expand Down
40 changes: 31 additions & 9 deletions resources/js/common/setDefaultAdvancedFilterStatus.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
import { get } from "lodash";

/**
* Whether this advanced filter entry targets the task Status column (saved or live form).
* Used so we do not add a duplicate default "In Progress" when a status filter already exists
* under Field/status or _column_field shape.
*
* @param {object} f
* @returns {boolean}
*/
export function isTaskStatusColumnFilter(f) {
if (!f || typeof f !== "object") {
return false;
}
return (
f._column_field === "status"
|| f.subject?.type === "Status"
|| (f.subject?.type === "Field" && f.subject?.value === "status")
);
}

export default (status, ignoreSavedFilter = false, requester = null) => {
let advancedFilter = get(window, 'ProcessMaker.advanced_filter.filters', []);
let advancedFilter = get(window, "ProcessMaker.advanced_filter.filters", []);
if (ignoreSavedFilter) {
// Remove any Status filters that might be set by the user
advancedFilter = advancedFilter.filter(f => f.subject?.type !== "Status" && f.subject?.value !== 'user_id');
} else if (advancedFilter.some(f => f.subject?.type === "Status")) {
advancedFilter = advancedFilter.filter(
(f) => !isTaskStatusColumnFilter(f) && f.subject?.value !== "user_id",
);
} else if (advancedFilter.some(isTaskStatusColumnFilter)) {
// Already has a status filter set by the user
return;
}


// Same subject shape as PMColumnFilterForm (Field + value) so merge with user rows works
advancedFilter.push({
subject: {
type: "Status"
type: "Field",
value: "status",
},
operator: "=",
value: status,
_column_field: "status",
_column_label: "Status"
_column_label: "Status",
});

if (requester) {
advancedFilter.push({
subject: {
type: "Field",
value: 'user_id'
value: "user_id",
},
operator: "=",
value: requester.id,
Expand All @@ -35,5 +58,4 @@ export default (status, ignoreSavedFilter = false, requester = null) => {
}

window.ProcessMaker.advanced_filter.filters = advancedFilter;

}
};
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.normalizeEnumEqualsLogicals();
let json = this.getValues();
this.$emit("onApply", json);
},
Expand All @@ -134,6 +135,7 @@
},
onClickButtonAdd() {
this.addItem(this.items.length);
this.normalizeEnumEqualsLogicals();
},
onClickButtonRemove(item, index) {
if (this.items.length === 1) {
Expand All @@ -143,11 +145,16 @@
},
onChangeOperator(item) {
this.switchViewControl(item);
this.normalizeEnumEqualsLogicals();
},
onChangeLogicalOp() {
onChangeLogicalOp(item, index) {
if (this.requiresOrLogicalOperator(index)) {
item.logical = "or";
}
},
setValues(json) {
let items = this.transformToFilterSyntax(json);
this.normalizeEnumEqualsLogicals(items);
this.items = items;
},
getValues() {
Expand Down Expand Up @@ -236,7 +243,33 @@
}
return operators;
},
getLogicals() {
/**
* stringSelect columns (status, process version) only support OR between "=" rows;
* a single row cannot satisfy two different enum values with AND.
*/
requiresOrLogicalOperator(index) {
Comment thread
CarliPinell marked this conversation as resolved.
if (this.format !== "stringSelect") {
return false;
}
const current = this.items[index];
const next = this.items[index + 1];
return current?.operator === "=" && next?.operator === "=";
},
normalizeEnumEqualsLogicals(items = this.items) {
if (this.format !== "stringSelect") {
return;
}
for (let i = 0; i < items.length - 1; i++) {
items[i].logical =
items[i].operator === "=" && items[i + 1].operator === "="
? "or"
: "and";
}
},
getLogicals(index) {
if (this.requiresOrLogicalOperator(index)) {
return [{ value: "or", text: "or" }];
}
return [
{value: "and", text: "and"},
{value: "or", text: "or"}
Expand Down
Loading