- unselect all rows, e.g. after deleting selected rows
$emit('edit-row', rowId)"
@copy-row="rowId => $emit('copy-row', rowId)"
@@ -66,7 +66,7 @@ deselect-all-rows -> unselect all rows, e.g. after deleting selected rows
+ :on-export-filtered="() => $emit('download-filtered-csv', getSearchedRows)" />
0) || !!this.localViewSetting?.searchString
},
- sorting() {
- return this.viewSetting?.sorting
- },
- getSearchedAndFilteredRows() {
- // if we don't have to search and/or filter
- if (!this.viewSetting?.filter?.length > 0 && !this.viewSetting?.searchString) {
+ getSearchedRows() {
+ // if we don't have to search
+ if (!this.viewSetting?.searchString) {
// cleanup markers
if (this.rows && this.columns) {
this.rows.forEach(row => {
@@ -283,7 +277,6 @@ export default {
return
}
delete cell.searchStringFound
- delete cell.filterFound
})
}
})
@@ -302,14 +295,11 @@ export default {
if (!row || !row.data) {
continue
}
- let filterStatusRow = null
let searchStatusRow = false
// each column in a row => cell
this.columns.forEach(column => {
- let filterStatus = null
let searchStatus = true
- const filters = getFiltersForColumn(column, this.viewSetting)
let cell
if (column.id < 0) {
cell = { columnId: column.id }
@@ -343,56 +333,21 @@ export default {
}
// cleanup possible old markers
delete cell.searchStringFound
- delete cell.filterFound
- // apply filters (if any)
- filters.forEach(fil => {
- this.addMagicFieldsValues(fil)
- if (filterStatus === null || filterStatus === true) {
- filterStatus = column.isFilterFound(cell, fil)
- }
- })
// if we should search
if (searchString) {
searchStatus = column.isSearchStringFound(cell, searchString.toLowerCase())
}
- // if filterStatus is null, this result should be ignored
- if (filterStatus !== null && (filterStatusRow || filterStatusRow === null)) {
- filterStatusRow = filterStatus
- }
-
- // filterStatusRow = filterStatus
searchStatusRow = searchStatusRow || searchStatus
})
- if ((filterStatusRow || filterStatusRow === null) && searchStatusRow) {
+ if (searchStatusRow) {
data.push({ ...row })
}
}
return data
},
- getSearchedAndFilteredAndSortedRows() {
- const allColumns = this.columns.concat(MetaColumns)
- const sort = (sortCols) => {
- const sortColumn = allColumns.find(item => item.id === sortCols?.[0].columnId)
- const nextSorts = []
- for (let i = 1; i < sortCols.length; i++) {
- const sortColumn = allColumns.find(item => item.id === sortCols[i].columnId)
- nextSorts.push(sortColumn?.sort?.(sortCols[i].mode))
- }
- return [...this.getSearchedAndFilteredRows].sort(sortColumn?.sort?.(sortCols[0].mode, nextSorts))
- }
-
- // if we have to sort
- if (this.viewSetting?.presetSorting) {
- return sort(this.viewSetting.presetSorting)
- }
- if (this.viewSetting?.sorting) {
- return sort(this.viewSetting.sorting)
- }
- return this.getSearchedAndFilteredRows
- },
},
watch: {
localSelectedRows() {
@@ -434,18 +389,6 @@ export default {
this.localViewSetting.searchString = str !== '' ? str : null
this.localViewSetting = JSON.parse(JSON.stringify(this.localViewSetting))
},
- addMagicFieldsValues(filter) {
- if (FilterIds.ContainsItem === filter.operator.id) {
- return
- }
-
- Object.values(MagicFields).forEach(field => {
- const newFilterValue = filter.value.replace('@' + field.id, field.replace)
- if (filter.value !== newFilterValue) {
- filter.magicValuesEnriched = newFilterValue
- }
- })
- },
},
}
diff --git a/src/shared/components/ncTable/mixins/columnClass.js b/src/shared/components/ncTable/mixins/columnClass.js
index 95250ccc18..8ab34f54fc 100644
--- a/src/shared/components/ncTable/mixins/columnClass.js
+++ b/src/shared/components/ncTable/mixins/columnClass.js
@@ -26,23 +26,6 @@ export class AbstractColumn {
this.customSettings = data.customSettings
}
- canSort() {
- return typeof this.sort === 'function'
- }
-
- getNextSortsResult(nextSorts, rowA, rowB) {
- if (!nextSorts) {
- return 0
- }
- for (const sort of nextSorts) {
- const result = sort(rowA, rowB)
- if (result !== 0) {
- return result
- }
- }
- return 0
- }
-
getPossibleOperators() {
return Object.values(Filters).filter(fil => fil.goodFor.includes(this.type))
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/datetime.js b/src/shared/components/ncTable/mixins/columnsTypes/datetime.js
index e4d32af108..8dc3d12a21 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/datetime.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/datetime.js
@@ -6,10 +6,6 @@ import { AbstractDatetimeColumn } from '../columnClass.js'
import { ColumnTypes } from '../columnHandler.js'
import Moment from '@nextcloud/moment'
import { FilterIds } from '../filter.js'
-import {
- TYPE_META_CREATED_AT,
- TYPE_META_UPDATED_AT,
-} from '../../../../constants.ts'
export default class DatetimeColumn extends AbstractDatetimeColumn {
@@ -22,36 +18,6 @@ export default class DatetimeColumn extends AbstractDatetimeColumn {
return Moment(value, 'YYYY-MM-DD HH:mm:ss').format('lll')
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- let tmpA = rowA.data.find(item => item.columnId === this.id)?.value || ''
- let tmpB = rowB.data.find(item => item.columnId === this.id)?.value || ''
- if (this.id === TYPE_META_CREATED_AT) {
- tmpA = rowA.createdAt
- tmpB = rowB.createdAt
- }
- if (this.id === TYPE_META_UPDATED_AT) {
- tmpA = rowA.lastEditAt
- tmpB = rowB.lastEditAt
- }
-
- if (!tmpA && tmpB) {
- return -1 * factor
- }
- if (tmpA && !tmpB) {
- return 1 * factor
- }
- if (!tmpA && !tmpB) {
- return super.getNextSortsResult(nextSorts, rowA, rowB)
- }
-
- const valueA = new Moment(tmpA, 'YYYY-MM-DD HH:mm')
- const valueB = new Moment(tmpB, 'YYYY-MM-DD HH:mm')
- return (valueA.diff(valueB)) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
const date = new Moment(cell.value, 'YYYY-MM-DD HH:mm').format('lll')
return super.isSearchStringFound(date, cell, searchString)
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/datetimeDate.js b/src/shared/components/ncTable/mixins/columnsTypes/datetimeDate.js
index db3d9cf6d6..e20fdd4583 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/datetimeDate.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/datetimeDate.js
@@ -18,26 +18,6 @@ export default class DatetimeDateColumn extends AbstractDatetimeColumn {
return Moment(value, 'YYYY-MM-DD HH:mm:ss').format('ll')
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const tmpA = rowA.data.find(item => item.columnId === this.id)?.value || ''
- const valueA = new Moment(tmpA)
- const tmpB = rowB.data.find(item => item.columnId === this.id)?.value || ''
- const valueB = new Moment(tmpB)
- if (!tmpA && tmpB) {
- return -1 * factor
- }
- if (tmpA && !tmpB) {
- return 1 * factor
- }
- if (!tmpA && !tmpB) {
- return super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- return (valueA.diff(valueB)) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
const date = new Moment(cell.value, 'YYYY-MM-DD').format('ll')
return super.isSearchStringFound(date, cell, searchString)
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/datetimeTime.js b/src/shared/components/ncTable/mixins/columnsTypes/datetimeTime.js
index 04dc8a3a97..394b49bc6d 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/datetimeTime.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/datetimeTime.js
@@ -18,26 +18,6 @@ export default class DatetimeTimeColumn extends AbstractDatetimeColumn {
return Moment(value, 'HH:mm:ss').format('LT')
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const tmpA = rowA.data.find(item => item.columnId === this.id)?.value || ''
- const valueA = new Moment(tmpA, 'HH:mm')
- const tmpB = rowB.data.find(item => item.columnId === this.id)?.value || ''
- const valueB = new Moment(tmpB, 'HH:mm')
- if (!tmpA && tmpB) {
- return -1 * factor
- }
- if (tmpA && !tmpB) {
- return 1 * factor
- }
- if (!tmpA && !tmpB) {
- return super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- return (valueA.diff(valueB)) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
const time = new Moment(cell.value, 'YYYY-MM-DD HH:mm').format('lll')
return super.isSearchStringFound(time, cell, searchString)
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/number.js b/src/shared/components/ncTable/mixins/columnsTypes/number.js
index b343817351..2ff7c51361 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/number.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/number.js
@@ -5,8 +5,6 @@
import { AbstractNumberColumn } from '../columnClass.js'
import { ColumnTypes } from '../columnHandler.js'
import { FilterIds } from '../filter.js'
-import { TYPE_META_ID } from '../../../../constants.ts'
-
export default class NumberColumn extends AbstractNumberColumn {
constructor(data) {
@@ -19,28 +17,6 @@ export default class NumberColumn extends AbstractNumberColumn {
this.numberSuffix = data.numberSuffix
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- let valueA = rowA.data.find(item => item.columnId === this.id)?.value || null
- let valueB = rowB.data.find(item => item.columnId === this.id)?.value || null
- if (this.id === TYPE_META_ID) {
- valueA = rowA.id
- valueB = rowB.id
- }
- if (!valueA && valueB) {
- return -1 * factor
- }
- if (valueA && !valueB) {
- return 1 * factor
- }
- if (!valueA && !valueB) {
- return super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- return ((valueA < valueB) ? -1 : (valueA > valueB) ? 1 : 0) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
return super.isSearchStringFound(('' + cell.value), cell, searchString)
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/numberProgress.js b/src/shared/components/ncTable/mixins/columnsTypes/numberProgress.js
index a391c62ddd..d692daefab 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/numberProgress.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/numberProgress.js
@@ -13,17 +13,6 @@ export default class NumberProgressColumn extends AbstractNumberColumn {
this.type = ColumnTypes.NumberProgress
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const tmpA = rowA.data.find(item => item.columnId === this.id)?.value || 0
- const valueA = parseInt(tmpA)
- const tmpB = rowB.data.find(item => item.columnId === this.id)?.value || 0
- const valueB = parseInt(tmpB)
- return ((valueA < valueB) ? -1 : (valueA > valueB) ? 1 : 0) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
return super.isSearchStringFound(('' + cell.value), cell, searchString)
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/numberStars.js b/src/shared/components/ncTable/mixins/columnsTypes/numberStars.js
index 754d9f3aea..5dd4171e59 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/numberStars.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/numberStars.js
@@ -13,17 +13,6 @@ export default class NumberStarsColumn extends AbstractNumberColumn {
this.type = ColumnTypes.NumberStars
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const tmpA = rowA.data.find(item => item.columnId === this.id)?.value
- const valueA = Number.isNaN(parseInt(tmpA)) ? -1 : parseInt(tmpA)
- const tmpB = rowB.data.find(item => item.columnId === this.id)?.value
- const valueB = Number.isNaN(parseInt(tmpB)) ? -1 : parseInt(tmpB)
- return ((valueA < valueB) ? -1 : (valueA > valueB) ? 1 : 0) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
return super.isSearchStringFound(('' + cell.value), cell, searchString)
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/selection.js b/src/shared/components/ncTable/mixins/columnsTypes/selection.js
index 31d3f9a31a..4a211c5136 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/selection.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/selection.js
@@ -29,19 +29,6 @@ export default class SelectionColumn extends AbstractSelectionColumn {
return !!this.selectionOptions[i]?.deleted
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const selectionIdA = parseInt(rowA.data.find(item => item.columnId === this.id)?.value)
- const vA = Number.isNaN(selectionIdA) ? '' : this.selectionOptions.find(item => item.id === selectionIdA)?.label
- const valueA = this.removeEmoji(vA).trim()
- const selectionIdB = parseInt(rowB.data.find(item => item.columnId === this.id)?.value ?? null)
- const vB = Number.isNaN(selectionIdB) ? '' : this.selectionOptions.find(item => item.id === selectionIdB)?.label
- const valueB = this.removeEmoji(vB).trim()
- return ((valueA < valueB) ? -1 : (valueA > valueB) ? 1 : 0) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
removeEmoji(str) {
return str.replace(/([#0-9]\u20E3)|[\xA9\xAE\u203C\u2047-\u2049\u2122\u2139\u3030\u303D\u3297\u3299][\uFE00-\uFEFF]?|[\u2190-\u21FF][\uFE00-\uFEFF]?|[\u2300-\u23FF][\uFE00-\uFEFF]?|[\u2460-\u24FF][\uFE00-\uFEFF]?|[\u25A0-\u25FF][\uFE00-\uFEFF]?|[\u2600-\u27BF][\uFE00-\uFEFF]?|[\u2900-\u297F][\uFE00-\uFEFF]?|[\u2B00-\u2BF0][\uFE00-\uFEFF]?|(?:\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDEFF])[\uFE00-\uFEFF]?/g, '')
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/selectionCheck.js b/src/shared/components/ncTable/mixins/columnsTypes/selectionCheck.js
index e4730406b4..080ea8f234 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/selectionCheck.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/selectionCheck.js
@@ -13,17 +13,6 @@ export default class SelectionCheckColumn extends AbstractSelectionColumn {
this.type = ColumnTypes.SelectionCheck
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const tmpA = rowA.data.find(item => item.columnId === this.id)?.value || ''
- const valueA = (tmpA === true || tmpA === 'true')
- const tmpB = rowB.data.find(item => item.columnId === this.id)?.value || ''
- const valueB = (tmpB === true || tmpB === 'true')
- return (valueA - valueB) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
isSearchStringFound(cell, searchString) {
return false
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/textLine.js b/src/shared/components/ncTable/mixins/columnsTypes/textLine.js
index e1408f225f..6d574330c2 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/textLine.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/textLine.js
@@ -5,7 +5,6 @@
import { AbstractTextColumn } from '../columnClass.js'
import { ColumnTypes } from '../columnHandler.js'
import { FilterIds } from '../filter.js'
-import { TYPE_META_CREATED_BY, TYPE_META_UPDATED_BY } from '../../../../constants.ts'
export default class TextLineColumn extends AbstractTextColumn {
@@ -16,23 +15,6 @@ export default class TextLineColumn extends AbstractTextColumn {
this.textUnique = data.textUnique
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- let valueA = rowA.data.find(item => item.columnId === this.id)?.value?.toLowerCase() || ''
- let valueB = rowB.data.find(item => item.columnId === this.id)?.value?.toLowerCase() || ''
- if (this.id === TYPE_META_CREATED_BY) {
- valueA = rowA.createdBy?.toLowerCase() || ''
- valueB = rowB.createdBy?.toLowerCase() || ''
- }
- if (this.id === TYPE_META_UPDATED_BY) {
- valueA = rowA.lastEditBy?.toLowerCase() || ''
- valueB = rowB.lastEditBy?.toLowerCase() || ''
- }
- return valueA.localeCompare(valueB, undefined) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
getValueString(valueObject) {
return valueObject.value.replace(/(<([^>]+)>)/ig, '')
}
diff --git a/src/shared/components/ncTable/mixins/columnsTypes/textLink.js b/src/shared/components/ncTable/mixins/columnsTypes/textLink.js
index e0328f68f5..312b65a304 100644
--- a/src/shared/components/ncTable/mixins/columnsTypes/textLink.js
+++ b/src/shared/components/ncTable/mixins/columnsTypes/textLink.js
@@ -14,17 +14,6 @@ export default class TextLinkColumn extends AbstractTextColumn {
this.textAllowedPattern = data.textAllowedPattern
}
- sort(mode, nextSorts) {
- const factor = mode === 'DESC' ? -1 : 1
- return (rowA, rowB) => {
- const tmpA = rowA.data.find(item => item.columnId === this.id)?.value?.toLowerCase() || ''
- const valueA = this.getValueFromCellValue(tmpA)
- const tmpB = rowB.data.find(item => item.columnId === this.id)?.value?.toLowerCase() || ''
- const valueB = this.getValueFromCellValue(tmpB)
- return valueA.localeCompare(valueB, undefined) * factor || super.getNextSortsResult(nextSorts, rowA, rowB)
- }
- }
-
getValueFromCellValue(cellValue) {
// check if the value is the old string or the new object
try {
diff --git a/src/shared/components/ncTable/mixins/filter.js b/src/shared/components/ncTable/mixins/filter.js
index e3a4cf9655..d1b3df0712 100644
--- a/src/shared/components/ncTable/mixins/filter.js
+++ b/src/shared/components/ncTable/mixins/filter.js
@@ -74,27 +74,27 @@ export const Filters = {
BeginsWith: new Filter({
id: FilterIds.BeginsWith,
label: t('tables', 'Begins with'),
- goodFor: [ColumnTypes.TextLine, ColumnTypes.TextLink],
+ goodFor: [ColumnTypes.TextLine],
incompatibleWith: [FilterIds.IsEmpty, FilterIds.IsEqual, FilterIds.BeginsWith],
}),
EndsWith: new Filter({
id: FilterIds.EndsWith,
label: t('tables', 'Ends with'),
- goodFor: [ColumnTypes.TextLine, ColumnTypes.TextLink],
+ goodFor: [ColumnTypes.TextLine],
incompatibleWith: [FilterIds.IsEmpty, FilterIds.IsEqual, FilterIds.EndsWith],
}),
IsEqual: new Filter({
id: FilterIds.IsEqual,
label: t('tables', 'Is equal'),
shortLabel: '=',
- goodFor: [ColumnTypes.TextLine, ColumnTypes.Number, ColumnTypes.SelectionCheck, ColumnTypes.TextLink, ColumnTypes.NumberStars, ColumnTypes.NumberProgress, ColumnTypes.DatetimeDate, ColumnTypes.DatetimeTime, ColumnTypes.Datetime, ColumnTypes.Selection, ColumnTypes.SelectionMulti, ColumnTypes.Usergroup, ColumnTypes.Relation],
+ goodFor: [ColumnTypes.TextLine, ColumnTypes.Number, ColumnTypes.SelectionCheck, ColumnTypes.NumberStars, ColumnTypes.NumberProgress, ColumnTypes.DatetimeDate, ColumnTypes.DatetimeTime, ColumnTypes.Datetime, ColumnTypes.Selection, ColumnTypes.SelectionMulti, ColumnTypes.Usergroup, ColumnTypes.Relation],
incompatibleWith: [FilterIds.IsNotEqual, FilterIds.IsEmpty, FilterIds.IsEqual, FilterIds.BeginsWith, FilterIds.EndsWith, FilterIds.Contains, FilterIds.IsGreaterThan, FilterIds.IsGreaterThanOrEqual, FilterIds.IsLowerThan, FilterIds.IsLowerThanOrEqual],
}),
IsNotEqual: new Filter({
id: FilterIds.IsNotEqual,
label: t('tables', 'Is not equal'),
shortLabel: '!=',
- goodFor: [ColumnTypes.TextLine, ColumnTypes.Number, ColumnTypes.SelectionCheck, ColumnTypes.TextLink, ColumnTypes.NumberStars, ColumnTypes.NumberProgress, ColumnTypes.DatetimeDate, ColumnTypes.DatetimeTime, ColumnTypes.Datetime, ColumnTypes.Selection, ColumnTypes.SelectionMulti, ColumnTypes.Usergroup, ColumnTypes.Relation],
+ goodFor: [ColumnTypes.TextLine, ColumnTypes.Number, ColumnTypes.SelectionCheck, ColumnTypes.NumberStars, ColumnTypes.NumberProgress, ColumnTypes.DatetimeDate, ColumnTypes.DatetimeTime, ColumnTypes.Datetime, ColumnTypes.Selection, ColumnTypes.SelectionMulti, ColumnTypes.Usergroup, ColumnTypes.Relation],
incompatibleWith: [FilterIds.IsEmpty, FilterIds.IsEqual, FilterIds.BeginsWith, FilterIds.EndsWith, FilterIds.Contains, FilterIds.IsGreaterThan, FilterIds.IsGreaterThanOrEqual, FilterIds.IsLowerThan, FilterIds.IsLowerThanOrEqual],
}),
IsGreaterThan: new Filter({
diff --git a/src/shared/components/ncTable/mixins/filterSupport.js b/src/shared/components/ncTable/mixins/filterSupport.js
new file mode 100644
index 0000000000..18d540fb72
--- /dev/null
+++ b/src/shared/components/ncTable/mixins/filterSupport.js
@@ -0,0 +1,128 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * Backend filter query support
+ */
+import { TYPE_SELECTION } from '../../../constants.ts'
+import { FilterIds } from './filter.js'
+
+// Return a column object for a given id. For meta columns, which are not in
+// `this.columns`, return a small fallback object.
+export function getColumnForFilter(columns = [], columnId) {
+ return (columns || []).find(col => col.id === columnId) ?? { id: columnId, type: null }
+}
+
+export function parseBackendFilterGroups(customFiltersString) {
+ if (!customFiltersString) {
+ return []
+ }
+
+ try {
+ const parsed = JSON.parse(customFiltersString)
+ if (!Array.isArray(parsed)) {
+ return []
+ }
+
+ // URL query data can be invalid, so keep only filter groups and rules that
+ // match the backend format we expect.
+ return parsed
+ .filter(group => Array.isArray(group))
+ .map(group => group
+ .filter(rule => rule && typeof rule === 'object')
+ .filter(rule => Number.isInteger(Number.parseInt(rule.columnId, 10)) && typeof rule.operator === 'string')
+ .map(rule => ({
+ columnId: Number.parseInt(rule.columnId, 10),
+ operator: rule.operator,
+ value: rule.value ?? '',
+ })))
+ .filter(group => group.length > 0)
+ } catch (error) {
+ return []
+ }
+}
+// Convert filters currently stored in the page UI into backend filter groups.
+export function normalizeRuntimeFilterGroups(runtimeFilters = [], columns = []) {
+ if (!Array.isArray(runtimeFilters) || runtimeFilters.length === 0) {
+ return []
+ }
+
+ // The UI stores filters as one flat list. The backend expects groups, where
+ // every rule inside one group must match.
+ let groups = [[]]
+
+ for (const filter of runtimeFilters) {
+ if (!filter || typeof filter !== 'object') {
+ continue
+ }
+
+ const columnId = Number.parseInt(filter.columnId, 10)
+ if (!Number.isInteger(columnId)) {
+ continue
+ }
+
+ const operatorId = filter?.operator?.id
+ if (!operatorId) {
+ continue
+ }
+
+ if (operatorId === FilterIds.ContainsItem) {
+ const selectedOptions = Array.isArray(filter.value) ? filter.value : []
+ if (selectedOptions.length === 0) {
+ continue
+ }
+
+ const columnType = getColumnForFilter(columns, columnId)?.type
+ // A "contains item" filter can match several selected values, so expand
+ // it into several backend rules and keep them as alternatives.
+ const translatedRules = selectedOptions
+ .map(option => option?.id)
+ .filter(optionId => optionId !== null && optionId !== undefined)
+ .map(optionId => ({
+ columnId,
+ operator: columnType === TYPE_SELECTION ? FilterIds.IsEqual : FilterIds.Contains,
+ value: optionId,
+ }))
+
+ if (translatedRules.length === 0) {
+ continue
+ }
+
+ // Normal filters must all match together, so add the rule to every group.
+ groups = groups.flatMap(group => translatedRules.map(rule => [...group, rule]))
+ continue
+ }
+
+ const normalizedRule = {
+ columnId,
+ operator: operatorId,
+ value: filter.value ?? '',
+ }
+ groups = groups.map(group => [...group, normalizedRule])
+ }
+
+ return groups.filter(group => group.length > 0)
+}
+
+// This is used to merge URL filters with filters added in the current UI.
+export function mergeBackendFilterGroups(leftGroups, rightGroups) {
+ if (leftGroups.length === 0) {
+ return rightGroups
+ }
+
+ if (rightGroups.length === 0) {
+ return leftGroups
+ }
+
+ // Keep both sets of groups active at the same time by combining every left
+ // group with every right group.
+ return leftGroups.flatMap(leftGroup => rightGroups.map(rightGroup => [...leftGroup, ...rightGroup]))
+}
+
+export function buildBackendFilterQuery(customFiltersString, viewSetting = {}, columns = []) {
+ const routeCustomFilters = parseBackendFilterGroups(customFiltersString)
+ const runtimeFilterGroups = normalizeRuntimeFilterGroups(viewSetting?.filter, columns)
+ const mergedCustomFilters = mergeBackendFilterGroups(routeCustomFilters, runtimeFilterGroups)
+
+ return mergedCustomFilters.length > 0 ? JSON.stringify(mergedCustomFilters) : null
+}
diff --git a/src/shared/components/ncTable/mixins/sortSupport.js b/src/shared/components/ncTable/mixins/sortSupport.js
new file mode 100644
index 0000000000..c9dc08d56b
--- /dev/null
+++ b/src/shared/components/ncTable/mixins/sortSupport.js
@@ -0,0 +1,99 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import {
+ TYPE_META_CREATED_AT,
+ TYPE_META_CREATED_BY,
+ TYPE_META_ID,
+ TYPE_META_UPDATED_AT,
+ TYPE_META_UPDATED_BY,
+ TYPE_DATETIME,
+ TYPE_DATETIME_DATE,
+ TYPE_DATETIME_TIME,
+ TYPE_NUMBER,
+ TYPE_NUMBER_PROGRESS,
+ TYPE_NUMBER_STARS,
+ TYPE_TEXT_LINK,
+ TYPE_SELECTION,
+ TYPE_SELECTION_MULTI,
+ TYPE_TEXT_LINE,
+} from '../../../constants.ts'
+
+export const BACKEND_SORTABLE_TYPES = new Set([
+ TYPE_TEXT_LINE,
+ TYPE_TEXT_LINK,
+ TYPE_SELECTION,
+ TYPE_SELECTION_MULTI,
+ TYPE_NUMBER,
+ TYPE_NUMBER_STARS,
+ TYPE_NUMBER_PROGRESS,
+ TYPE_DATETIME_DATE,
+ TYPE_DATETIME_TIME,
+ TYPE_DATETIME,
+])
+
+export function getBackendSortableColumnType(columnId, columnType = null) {
+ // Meta columns reuse backend scalar sort behavior based on their stored row fields.
+ if (columnId === TYPE_META_ID) {
+ return TYPE_NUMBER
+ }
+
+ if (columnId === TYPE_META_CREATED_BY || columnId === TYPE_META_UPDATED_BY) {
+ return TYPE_TEXT_LINE
+ }
+
+ if (columnId === TYPE_META_CREATED_AT || columnId === TYPE_META_UPDATED_AT) {
+ return TYPE_DATETIME
+ }
+
+ return columnType
+}
+
+export function isBackendSortableColumn({ id, type }) {
+ if (id < 0) {
+ return true
+ }
+
+ return BACKEND_SORTABLE_TYPES.has(getBackendSortableColumnType(id, type))
+}
+
+export function getColumnForSort(columns = [], columnId) {
+ return (columns || []).find(col => col.id === columnId) ?? { id: columnId, type: null }
+}
+
+export function isBackendSortableRule(rule, columns = []) {
+ const normalizedRule = normalizeBackendSortRules([rule])[0]
+ if (!normalizedRule) {
+ return false
+ }
+
+ return isBackendSortableColumn(getColumnForSort(columns, normalizedRule.columnId))
+}
+
+export function normalizeBackendSortRules(sortRules = []) {
+ if (!Array.isArray(sortRules)) {
+ return []
+ }
+
+ // Keep the request contract narrow so unsupported UI state never reaches the backend.
+ return sortRules
+ .map(rule => ({
+ columnId: Number.parseInt(rule?.columnId, 10),
+ mode: rule?.mode === 'DESC' ? 'DESC' : 'ASC',
+ }))
+ .filter(rule => Number.isInteger(rule.columnId))
+}
+
+export function normalizeBackendSortRulesForColumns(sortRules = [], columns = []) {
+ return normalizeBackendSortRules(sortRules)
+ .filter(rule => isBackendSortableColumn(getColumnForSort(columns, rule.columnId)))
+}
+
+export function getExplicitSortRules(viewSetting = {}) {
+ if (Array.isArray(viewSetting?.sorting) && viewSetting.sorting.length > 0) {
+ return viewSetting.sorting
+ }
+
+ return []
+}
diff --git a/src/shared/components/ncTable/partials/FilterLabel.vue b/src/shared/components/ncTable/partials/FilterLabel.vue
index 6e7fd328c6..297b11f5f9 100644
--- a/src/shared/components/ncTable/partials/FilterLabel.vue
+++ b/src/shared/components/ncTable/partials/FilterLabel.vue
@@ -30,6 +30,10 @@ export default {
type: [String, Array],
default: '',
},
+ displayValue: {
+ type: String,
+ default: null,
+ },
type: {
type: String,
default: 'highlighted', // highlighted, outlined
@@ -45,11 +49,18 @@ export default {
],
computed: {
getValue() {
+ if (this.displayValue) {
+ return this.displayValue
+ }
+
if (FilterIds.ContainsItem === this.operator.id) {
return this.value
}
let value = this.value
+ if (typeof value !== 'string') {
+ return value
+ }
Object.values(MagicFields).forEach(field => {
value = value.replace('@' + field.id, field.label)
})
diff --git a/src/shared/components/ncTable/partials/TableHeader.vue b/src/shared/components/ncTable/partials/TableHeader.vue
index ec86961524..5bd8ea8a5f 100644
--- a/src/shared/components/ncTable/partials/TableHeader.vue
+++ b/src/shared/components/ncTable/partials/TableHeader.vue
@@ -43,6 +43,7 @@
:key="filter.columnId + filter.operator.id+ filter.value"
:operator="castToFilter(filter.operator.id)"
:value="filter.value"
+ :display-value="filter.displayValue"
@delete-filter="id => deleteFilter(id)" />
diff --git a/src/shared/components/ncTable/partials/TableHeaderColumnOptions.vue b/src/shared/components/ncTable/partials/TableHeaderColumnOptions.vue
index 7b4968bd21..0e0aaa9f15 100644
--- a/src/shared/components/ncTable/partials/TableHeaderColumnOptions.vue
+++ b/src/shared/components/ncTable/partials/TableHeaderColumnOptions.vue
@@ -42,7 +42,7 @@
v-model="searchOptions"
type="multiselect"
:multiple="true"
- :options="column.selectionOptions"
+ :options="selectionFilterOptions"
:placeholder="t('tables', 'Select options')" />