多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

462 lines
15 KiB

import type { ColumnType } from 'ant-design-vue/lib/table'
import dayjs from 'dayjs'
import { type FilterType, type LinkToAnotherRecordType, type TableType, UITypes, isDateMonthFormat } from 'nocodb-sdk'
type FormViewColumn = ColumnType & Record<string, any>
export class FormFilters {
allViewFilters: FilterType[]
protected groupedFilters: Record<string, FilterType[]>
nestedGroupedFilters: Record<string, FilterType[]>
formViewColumns: FormViewColumn[]
formViewColumnsMapByFkColumnId: Record<string, FormViewColumn>
formState: Record<string, any>
value: any
isSharedForm: boolean
isMysql?: (sourceId?: string) => boolean
getMeta?: (tableIdOrTitle: string) => Promise<TableType | null>
constructor({
data = [],
nestedGroupedFilters = {},
formViewColumns = [],
formViewColumnsMapByFkColumnId = {},
formState = {},
isMysql = undefined,
isSharedForm = false,
getMeta = undefined,
}: {
data?: FilterType[]
nestedGroupedFilters?: Record<string, FilterType[]>
formViewColumns?: FormViewColumn[]
formViewColumnsMapByFkColumnId?: Record<string, FormViewColumn>
formState?: Record<string, any>
isMysql?: (sourceId?: string) => boolean
isSharedForm?: boolean
getMeta?: (tableIdOrTitle: string) => Promise<TableType | null>
} = {}) {
this.allViewFilters = data
this.groupedFilters = {}
this.nestedGroupedFilters = nestedGroupedFilters
this.formViewColumns = formViewColumns
this.formViewColumnsMapByFkColumnId = formViewColumnsMapByFkColumnId
this.formState = formState
this.isSharedForm = isSharedForm
this.isMysql = isMysql
this.getMeta = getMeta
}
setFilters(filters: FilterType[]) {
this.allViewFilters = filters
}
getRootFilters(parentColId: string) {
return (this.groupedFilters[parentColId] || [])
.filter((f) => !f.fk_parent_id)
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
}
getParentFilters(parentColId: string, parentId: string) {
return (this.groupedFilters[parentColId] || [])
.filter((f) => f.fk_parent_id === parentId)
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
}
getAllChildFilters(filters: FilterType[], parentColId: string): any {
return filters.map((filter) => {
if (filter.id && filter.is_group) {
const childFilters = this.getParentFilters(parentColId, filter.id)
filter.children = this.getAllChildFilters(childFilters, parentColId)
}
return filter
})
}
loadFilters() {
for (const parentColId in this.groupedFilters) {
const rootFilters = this.getRootFilters(parentColId)
this.nestedGroupedFilters[parentColId] = this.getAllChildFilters(rootFilters, parentColId)
}
return this.nestedGroupedFilters
}
// Method to group filters by fk_parent_column_id
getNestedGroupedFilters() {
const groupedFilters = this.allViewFilters.reduce((acc, filter) => {
const groupingKey = filter.fk_parent_column_id || 'ungrouped'
if (!acc[groupingKey]) {
acc[groupingKey] = []
}
acc[groupingKey].push(filter)
return acc
}, {} as typeof this.groupedFilters)
this.groupedFilters = groupedFilters
const nestedGroupedFilters = this.loadFilters()
return nestedGroupedFilters
}
toString(value: any) {
return `${value || ''}`
}
isFieldAboveParentColumn(column: FormViewColumn, parentColumn: FormViewColumn) {
return column.order < parentColumn.order
}
async getOoOrBtColVal(column: FormViewColumn) {
const fk_related_model_id = (column?.colOptions as LinkToAnotherRecordType)?.fk_related_model_id
if (!fk_related_model_id || typeof this.getMeta !== 'function') return null
const relatedTableMeta = await this.getMeta(fk_related_model_id)
if (!relatedTableMeta || !Array.isArray(relatedTableMeta?.columns)) return null
const displayValTitle = (relatedTableMeta.columns.find((c) => c.pv) || relatedTableMeta.columns?.[0])?.title || ''
if (
!displayValTitle ||
!this.formState[column.title] ||
!ncIsObject(this.formState[column.title]) ||
this.formState[column.title][displayValTitle] === undefined
) {
return null
}
return this.formState[column.title][displayValTitle]
}
async validateCondition(
filters: FilterType[] = [],
parentCol: FormViewColumn,
errors: Record<string, string>,
): Promise<boolean | undefined> {
if (!filters.length) {
return true
}
let isValid
for (const filter of filters) {
let res
if (filter.is_group) {
res = await this.validateCondition(filter.children, parentCol, errors)
} else {
if (!filter.fk_column_id || !this.formViewColumnsMapByFkColumnId[filter.fk_column_id]) {
res = false
}
const column = this.formViewColumnsMapByFkColumnId[filter.fk_column_id]
// If the filter condition col is below parent column then this will be invalid condition so return false
if (!this.isFieldAboveParentColumn(column, parentCol)) {
errors[column.fk_column_id] = `Condition references a field (${column.title}) that comes later in the form.`
res = true
}
if (!column.show) {
errors[column.fk_column_id] = `Condition references a field (${column.title}) that was removed from the form.`
res = true
}
if (!column?.visible) {
res = false
}
const field = column.title
let val = this.formState[field]
if (res === undefined) {
if (
[UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(column.uidt) &&
!['empty', 'blank', 'notempty', 'notblank'].includes(filter.comparison_op)
) {
const dateFormat = this.isMysql?.(column.source_id) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
let now = dayjs(new Date())
const dateFormatFromMeta = column?.meta?.date_format
const dataVal: any = val
let filterVal: any = filter.value
if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) {
// reset to 1st
now = dayjs(now).date(1)
if (val) val = dayjs(val).date(1)
}
if (filterVal) res = dayjs(filterVal).isSame(dataVal, 'day')
// handle sub operation
switch (filter.comparison_sub_op) {
case 'today':
filterVal = now
break
case 'tomorrow':
filterVal = now.add(1, 'day')
break
case 'yesterday':
filterVal = now.add(-1, 'day')
break
case 'oneWeekAgo':
filterVal = now.add(-1, 'week')
break
case 'oneWeekFromNow':
filterVal = now.add(1, 'week')
break
case 'oneMonthAgo':
filterVal = now.add(-1, 'month')
break
case 'oneMonthFromNow':
filterVal = now.add(1, 'month')
break
case 'daysAgo':
if (!filterVal) return
filterVal = now.add(-filterVal, 'day')
break
case 'daysFromNow':
if (!filterVal) return
filterVal = now.add(filterVal, 'day')
break
case 'exactDate':
if (!filterVal) return
break
// sub-ops for `isWithin` comparison
case 'pastWeek':
filterVal = now.add(-1, 'week')
break
case 'pastMonth':
filterVal = now.add(-1, 'month')
break
case 'pastYear':
filterVal = now.add(-1, 'year')
break
case 'nextWeek':
filterVal = now.add(1, 'week')
break
case 'nextMonth':
filterVal = now.add(1, 'month')
break
case 'nextYear':
filterVal = now.add(1, 'year')
break
case 'pastNumberOfDays':
if (!filterVal) return
filterVal = now.add(-filterVal, 'day')
break
case 'nextNumberOfDays':
if (!filterVal) return
filterVal = now.add(filterVal, 'day')
break
}
if (dataVal) {
switch (filter.comparison_op) {
case 'eq':
res = dayjs(dataVal).isSame(filterVal, 'day')
break
case 'neq':
res = !dayjs(dataVal).isSame(filterVal, 'day')
break
case 'gt':
res = dayjs(dataVal).isAfter(filterVal, 'day')
break
case 'lt':
res = dayjs(dataVal).isBefore(filterVal, 'day')
break
case 'lte':
case 'le':
res = dayjs(dataVal).isSameOrBefore(filterVal, 'day')
break
case 'gte':
case 'ge':
res = dayjs(dataVal).isSameOrAfter(filterVal, 'day')
break
case 'empty':
case 'blank':
res = dataVal === '' || dataVal === null || dataVal === undefined
break
case 'notempty':
case 'notblank':
res = !(dataVal === '' || dataVal === null || dataVal === undefined)
break
case 'isWithin': {
let now = dayjs(new Date()).format(dateFormat).toString()
now = column.uidt === UITypes.Date ? now.substring(0, 10) : now
switch (filter.comparison_sub_op) {
case 'pastWeek':
case 'pastMonth':
case 'pastYear':
case 'pastNumberOfDays':
res = dayjs(dataVal).isBetween(filterVal, now, 'day')
break
case 'nextWeek':
case 'nextMonth':
case 'nextYear':
case 'nextNumberOfDays':
res = dayjs(dataVal).isBetween(now, filterVal, 'day')
break
}
}
}
}
} else {
switch (typeof filter.value) {
case 'boolean':
val = !!this.formState[field]
break
case 'number':
val = Number.isNaN(parseFloat(this.formState[field])) ? this.formState[field] : +this.formState[field]
break
}
switch (column.uidt) {
case UITypes.Links:
if (isMm(column) || isHm(column)) {
val = (this.formState[field] ?? []).length
}
break
case UITypes.LinkToAnotherRecord:
if (isOo(column) || isBt(column)) {
val = await this.getOoOrBtColVal(column)
}
break
}
switch (filter.comparison_op) {
case 'eq':
// eslint-disable-next-line eqeqeq
res = val == filter.value
break
case 'neq':
// eslint-disable-next-line eqeqeq
res = val != filter.value
break
case 'like':
res = this.toString(val).toLowerCase()?.includes(filter.value?.toLowerCase())
break
case 'nlike':
res = !this.toString(val).toLowerCase()?.includes(filter.value?.toLowerCase())
break
case 'empty':
case 'blank':
res = val === '' || val === null || val === undefined
break
case 'notempty':
case 'notblank':
res = !(val === '' || val === null || val === undefined)
break
case 'checked':
res = !!val
break
case 'notchecked':
res = !val
break
case 'null':
res = val === null
break
case 'notnull':
res = val !== null
break
case 'allof':
res = (
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).every((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'anyof':
res = (
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).some((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'nallof':
res = !(
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).every((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'nanyof':
res = !(
this.toString(filter.value)
.split(',')
.map((item) => item.trim()) ?? []
).some((item) => (this.toString(val).split(',') ?? []).includes(item))
break
case 'lt':
res = parseFloat(val) < +filter.value
break
case 'lte':
case 'le':
res = parseFloat(val) <= +filter.value
break
case 'gt':
res = parseFloat(val) > +filter.value
break
case 'gte':
case 'ge':
res = parseFloat(val) >= +filter.value
break
}
}
}
}
switch (filter.logical_op) {
case 'or':
isValid = isValid || !!res
break
case 'not':
isValid = isValid && !res
break
case 'and':
default:
isValid = (isValid ?? true) && res
break
}
}
return isValid
}
async validateVisibility() {
const res: Record<string, boolean> = {}
for (const column of this.formViewColumns) {
const columnFilters = this.nestedGroupedFilters[column.fk_column_id] ?? []
const errors: Record<string, string> = {}
const isValid = await this.validateCondition(columnFilters, column, errors)
if (this.isSharedForm) {
if (!column.meta?.preFilledHiddenField) {
column.show = !!isValid
column.visible = !!isValid
}
} else {
column.visible = !!isValid
column.meta = {
...parseProp(column.meta),
visibility: {
errors,
},
}
}
}
return res
}
validateErrors() {}
}