1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-23 18:34:02 +02:00
focalboard/webapp/src/cardFilter.ts
Scott Bishel bd1532decf
Gh 4482 fix empty filters (#4485)
* if no value on isBefore, isAfter return true

* add unit tests

Co-authored-by: Mattermost Build <build@mattermost.com>
2023-01-19 12:21:20 -07:00

305 lines
11 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DateUtils} from 'react-day-picker'
import {DateProperty} from './properties/date/date'
import {IPropertyTemplate} from './blocks/board'
import {Card} from './blocks/card'
import {FilterClause} from './blocks/filterClause'
import {FilterGroup, isAFilterGroupInstance} from './blocks/filterGroup'
import {Utils} from './utils'
const halfDay = 12 * 60 * 60 * 1000
class CardFilter {
static createDatePropertyFromString(initialValue: string): DateProperty {
let dateProperty: DateProperty = {}
if (initialValue) {
const singleDate = new Date(Number(initialValue))
if (singleDate && DateUtils.isDate(singleDate)) {
dateProperty.from = singleDate.getTime()
} else {
try {
dateProperty = JSON.parse(initialValue)
} catch {
//Don't do anything, return empty dateProperty
}
}
}
return dateProperty
}
static applyFilterGroup(filterGroup: FilterGroup, templates: readonly IPropertyTemplate[], cards: Card[]): Card[] {
return cards.filter((card) => this.isFilterGroupMet(filterGroup, templates, card))
}
static isFilterGroupMet(filterGroup: FilterGroup, templates: readonly IPropertyTemplate[], card: Card): boolean {
const {filters} = filterGroup
if (filterGroup.filters.length < 1) {
return true // No filters = always met
}
if (filterGroup.operation === 'or') {
for (const filter of filters) {
if (isAFilterGroupInstance(filter)) {
if (this.isFilterGroupMet(filter, templates, card)) {
return true
}
} else if (this.isClauseMet(filter, templates, card)) {
return true
}
}
return false
}
Utils.assert(filterGroup.operation === 'and')
for (const filter of filters) {
if (isAFilterGroupInstance(filter)) {
if (!this.isFilterGroupMet(filter, templates, card)) {
return false
}
} else if (!this.isClauseMet(filter, templates, card)) {
return false
}
}
return true
}
static isClauseMet(filter: FilterClause, templates: readonly IPropertyTemplate[], card: Card): boolean {
let value = card.fields.properties[filter.propertyId]
if (filter.propertyId === 'title') {
value = card.title.toLowerCase()
}
const template = templates.find((o) => o.id === filter.propertyId)
let dateValue: DateProperty | undefined
if (template?.type === 'date') {
dateValue = this.createDatePropertyFromString(value as string)
}
if (!value && template) {
if (template.type === 'createdBy') {
value = card.createdBy
} else if (template.type === 'updatedBy') {
value = card.modifiedBy
} else if (template && template.type === 'createdTime') {
value = card.createAt.toString()
dateValue = this.createDatePropertyFromString(value as string)
} else if (template && template.type === 'updatedTime') {
value = card.updateAt.toString()
dateValue = this.createDatePropertyFromString(value as string)
}
}
switch (filter.condition) {
case 'includes': {
if (filter.values?.length < 1) {
break
} // No values = ignore clause (always met)
return (filter.values.find((cValue) => (Array.isArray(value) ? value.includes(cValue) : cValue === value)) !== undefined)
}
case 'notIncludes': {
if (filter.values?.length < 1) {
break
} // No values = ignore clause (always met)
return (filter.values.find((cValue) => (Array.isArray(value) ? value.includes(cValue) : cValue === value)) === undefined)
}
case 'isEmpty': {
return (value || '').length <= 0
}
case 'isNotEmpty': {
return (value || '').length > 0
}
case 'isSet': {
return Boolean(value)
}
case 'isNotSet': {
return !value
}
case 'is': {
if (filter.values.length === 0) {
return true
}
if (dateValue !== undefined) {
const numericFilter = parseInt(filter.values[0], 10)
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
// createdTime and updatedTime include the time
// So to check if create and/or updated "is" date.
// Need to add and subtract 12 hours and check range
if (dateValue.from) {
return dateValue.from > (numericFilter - halfDay) && dateValue.from < (numericFilter + halfDay)
}
return false
}
if (dateValue.from && dateValue.to) {
return dateValue.from <= numericFilter && dateValue.to >= numericFilter
}
return dateValue.from === numericFilter
}
return filter.values[0]?.toLowerCase() === value
}
case 'contains': {
if (filter.values.length === 0) {
return true
}
return (value as string || '').includes(filter.values[0]?.toLowerCase())
}
case 'notContains': {
if (filter.values.length === 0) {
return true
}
return !(value as string || '').includes(filter.values[0]?.toLowerCase())
}
case 'startsWith': {
if (filter.values.length === 0) {
return true
}
return (value as string || '').startsWith(filter.values[0]?.toLowerCase())
}
case 'notStartsWith': {
if (filter.values.length === 0) {
return true
}
return !(value as string || '').startsWith(filter.values[0]?.toLowerCase())
}
case 'endsWith': {
if (filter.values.length === 0) {
return true
}
return (value as string || '').endsWith(filter.values[0]?.toLowerCase())
}
case 'notEndsWith': {
if (filter.values.length === 0) {
return true
}
return !(value as string || '').endsWith(filter.values[0]?.toLowerCase())
}
case 'isBefore': {
if (filter.values.length === 0) {
return true
}
if (dateValue !== undefined) {
const numericFilter = parseInt(filter.values[0], 10)
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
// createdTime and updatedTime include the time
// So to check if create and/or updated "isBefore" date.
// Need to subtract 12 hours to filter
if (dateValue.from) {
return dateValue.from < (numericFilter - halfDay)
}
return false
}
return dateValue.from ? dateValue.from < numericFilter : false
}
return false
}
case 'isAfter': {
if (filter.values.length === 0) {
return true
}
if (dateValue !== undefined) {
const numericFilter = parseInt(filter.values[0], 10)
if (template && (template.type === 'createdTime' || template.type === 'updatedTime')) {
// createdTime and updatedTime include the time
// So to check if create and/or updated "isAfter" date.
// Need to add 12 hours to filter
if (dateValue.from) {
return dateValue.from > (numericFilter + halfDay)
}
return false
}
if (dateValue.to) {
return dateValue.to > numericFilter
}
return dateValue.from ? dateValue.from > numericFilter : false
}
return false
}
default: {
Utils.assertFailure(`Invalid filter condition ${filter.condition}`)
}
}
return true
}
static propertiesThatMeetFilterGroup(filterGroup: FilterGroup | undefined, templates: readonly IPropertyTemplate[]): Record<string, string> {
// TODO: Handle filter groups
if (!filterGroup) {
return {}
}
const filters = filterGroup.filters.filter((o) => !isAFilterGroupInstance(o))
if (filters.length < 1) {
return {}
}
if (filterGroup.operation === 'or') {
// Just need to meet the first clause
const property = this.propertyThatMeetsFilterClause(filters[0] as FilterClause, templates)
const result: Record<string, string> = {}
if (property.value) {
result[property.id] = property.value
}
return result
}
// And: Need to meet all clauses
const result: Record<string, string> = {}
filters.forEach((filterClause) => {
const property = this.propertyThatMeetsFilterClause(filterClause as FilterClause, templates)
if (property.value) {
result[property.id] = property.value
}
})
return result
}
static propertyThatMeetsFilterClause(filterClause: FilterClause, templates: readonly IPropertyTemplate[]): { id: string, value?: string } {
const template = templates.find((o) => o.id === filterClause.propertyId)
if (!template) {
Utils.assertFailure(`propertyThatMeetsFilterClause. Cannot find template: ${filterClause.propertyId}`)
return {id: filterClause.propertyId}
}
if (template.type === 'createdBy' || template.type === 'updatedBy') {
return {id: filterClause.propertyId}
}
switch (filterClause.condition) {
case 'includes': {
if (filterClause.values.length < 1) {
return {id: filterClause.propertyId}
}
return {id: filterClause.propertyId, value: filterClause.values[0]}
}
case 'notIncludes': {
return {id: filterClause.propertyId}
}
case 'isEmpty': {
return {id: filterClause.propertyId}
}
case 'isNotEmpty': {
if (template.type === 'select') {
if (template.options.length > 0) {
const option = template.options[0]
return {id: filterClause.propertyId, value: option.id}
}
return {id: filterClause.propertyId}
}
// TODO: Handle non-select types
return {id: filterClause.propertyId}
}
default: {
// Handle filter clause that cannot be set
return {id: filterClause.propertyId}
}
}
}
}
export {CardFilter}