From aa0144e0f2115895f5482166330a90baa93f2ca5 Mon Sep 17 00:00:00 2001 From: CarliPinell Date: Mon, 20 Apr 2026 11:03:44 -0400 Subject: [PATCH 1/3] Applying fix to multiple criteria in tasks filters --- .../PMColumnFilterPopoverCommonMixin.js | 166 +++++++++++++++++- .../common/setDefaultAdvancedFilterStatus.js | 40 ++++- 2 files changed, 193 insertions(+), 13 deletions(-) diff --git a/resources/js/common/PMColumnFilterPopoverCommonMixin.js b/resources/js/common/PMColumnFilterPopoverCommonMixin.js index a1d17aa8dc..cf5abf5986 100644 --- a/resources/js/common/PMColumnFilterPopoverCommonMixin.js +++ b/resources/js/common/PMColumnFilterPopoverCommonMixin.js @@ -1,4 +1,5 @@ import { get, cloneDeep } from "lodash"; +import { isTaskStatusColumnFilter } from "./setDefaultAdvancedFilterStatus"; const PMColumnFilterCommonMixin = { props: { @@ -37,12 +38,57 @@ const PMColumnFilterCommonMixin = { }); }, methods: { - storeFilterConfiguration() { - const {order, type} = this.filterConfiguration(); + /** + * Task inbox: 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). + */ + 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 @@ -50,6 +96,9 @@ const PMColumnFilterCommonMixin = { return; } + this.applyTaskInboxDefaultStatusIfMissing(); + const { order, type } = this.filterConfiguration(); + let url = "users/store_filter_configuration/"; if (this.$props.columns && this.savedSearch) { url += "savedSearch|" + this.savedSearch; @@ -151,9 +200,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; + }, + /** + * Columns that use stringSelect with "=" rows; multiple sibling "=" filters are AND-ed + * by the API and can never match (e.g. status In Progress AND Completed). Collapse those + * into one filter object using nested `or` so each value stays operator "=" (OR semantics + * in SQL — same as IN). Leaves existing OR-nested structures unchanged. + * + * @param {Array} filters - Filters for one column from PMColumnFilterForm + * @param {string} columnField - Column field name (e.g. status) + * @returns {Array} + */ + mergeFlatEnumEqualsToOrChainForColumn(filters, columnField) { + 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) + || 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]); + 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(); - this.advancedFilter[index] = json; + this.advancedFilter[index] = this.mergeFlatEnumEqualsToOrChainForColumn(json, index); this.markStyleWhenColumnSetAFilter(); this.storeFilterConfiguration(); this.fetch(true); @@ -182,7 +321,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); }); @@ -303,14 +444,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); } diff --git a/resources/js/common/setDefaultAdvancedFilterStatus.js b/resources/js/common/setDefaultAdvancedFilterStatus.js index 2b2090b5a8..336714c9df 100644 --- a/resources/js/common/setDefaultAdvancedFilterStatus.js +++ b/resources/js/common/setDefaultAdvancedFilterStatus.js @@ -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, @@ -35,5 +58,4 @@ export default (status, ignoreSavedFilter = false, requester = null) => { } window.ProcessMaker.advanced_filter.filters = advancedFilter; - -} \ No newline at end of file +}; From de993c3b1b93f9dff74ff355ce5d59a130270580 Mon Sep 17 00:00:00 2001 From: CarliPinell Date: Fri, 3 Jul 2026 18:08:43 -0400 Subject: [PATCH 2/3] Applying observations --- .../PMColumnFilterPopoverCommonMixin.js | 13 ++++--- .../PMColumnFilterForm.vue | 38 +++++++++++++++++-- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/resources/js/common/PMColumnFilterPopoverCommonMixin.js b/resources/js/common/PMColumnFilterPopoverCommonMixin.js index cf5abf5986..dd66e26b9e 100644 --- a/resources/js/common/PMColumnFilterPopoverCommonMixin.js +++ b/resources/js/common/PMColumnFilterPopoverCommonMixin.js @@ -218,12 +218,12 @@ const PMColumnFilterCommonMixin = { return key; }, /** - * Columns that use stringSelect with "=" rows; multiple sibling "=" filters are AND-ed - * by the API and can never match (e.g. status In Progress AND Completed). Collapse those - * into one filter object using nested `or` so each value stays operator "=" (OR semantics - * in SQL — same as IN). Leaves existing OR-nested structures unchanged. + * 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 from PMColumnFilterForm + * @param {Array} filters - Filters for one column * @param {string} columnField - Column field name (e.g. status) * @returns {Array} */ @@ -292,7 +292,8 @@ const PMColumnFilterCommonMixin = { }, onApply(json, index) { this.advancedFilterInit(); - this.advancedFilter[index] = this.mergeFlatEnumEqualsToOrChainForColumn(json, index); + // Form emits explicit OR for stringSelect "=" rows; merge is a no-op for those payloads. + this.advancedFilter[index] = json; this.markStyleWhenColumnSetAFilter(); this.storeFilterConfiguration(); this.fetch(true); diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue index 6afe5f5dd5..157b097150 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue @@ -41,7 +41,7 @@ Date: Wed, 8 Jul 2026 09:01:18 -0400 Subject: [PATCH 3/3] Applying CR observations --- resources/js/common/PMColumnFilterPopoverCommonMixin.js | 8 +++++--- .../PMColumnFilterPopover/PMColumnFilterForm.vue | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/resources/js/common/PMColumnFilterPopoverCommonMixin.js b/resources/js/common/PMColumnFilterPopoverCommonMixin.js index dd66e26b9e..62d29689cd 100644 --- a/resources/js/common/PMColumnFilterPopoverCommonMixin.js +++ b/resources/js/common/PMColumnFilterPopoverCommonMixin.js @@ -39,8 +39,9 @@ const PMColumnFilterCommonMixin = { }, methods: { /** - * Task inbox: 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). + * 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") { @@ -96,7 +97,8 @@ const PMColumnFilterCommonMixin = { return; } - this.applyTaskInboxDefaultStatusIfMissing(); + // 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/"; diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue index 157b097150..cc84bb3bfe 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterForm.vue @@ -260,9 +260,10 @@ return; } for (let i = 0; i < items.length - 1; i++) { - if (items[i].operator === "=" && items[i + 1].operator === "=") { - items[i].logical = "or"; - } + items[i].logical = + items[i].operator === "=" && items[i + 1].operator === "=" + ? "or" + : "and"; } }, getLogicals(index) {