多维表格
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.
 
 
 
 
 
 

561 lines
18 KiB

import {
type ColumnType,
type FilterType,
type LinkToAnotherRecordType,
type LookupType,
type ViewType,
getEquivalentUIType,
} from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { SelectProps } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
export function useViewFilters(
view: Ref<ViewType | undefined>,
_parentId: Ref<string | null> | null | string,
autoApply?: ComputedRef<boolean>,
reloadData?: () => void,
_currentFilters?: Filter[],
isNestedRoot?: boolean,
isWebhook?: boolean,
) {
const parentId = ref(_parentId)
const currentFilters = ref(_currentFilters)
const btLookupTypesMap = ref({})
const reloadHook = inject(ReloadViewDataHookInj)
const { nestedFilters, allFilters } = useSmartsheetStoreOrThrow()
const { baseMeta } = storeToRefs(useBase())
const isPublic = inject(IsPublicInj, ref(false))
const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { metas, getMeta } = useMetas()
const { addUndo, clone, defineViewScope } = useUndoRedo()
const _filters = ref<Filter[]>([])
const nestedMode = computed(() => isPublic.value || !isUIAllowed('filterSync') || !isUIAllowed('filterChildrenRead'))
const filters = computed<Filter[]>({
get: () => {
return nestedMode.value ? currentFilters.value! : _filters.value
},
set: (value: Filter[]) => {
if (nestedMode.value) {
currentFilters.value = value
if (isNestedRoot) {
nestedFilters.value = value
}
nestedFilters.value = [...nestedFilters.value]
reloadHook?.trigger()
return
}
_filters.value = value
},
})
// when a filter is deleted with auto apply disabled, the status is marked as 'delete'
// nonDeletedFilters are those filters that are not deleted physically & virtually
const nonDeletedFilters = computed(() => filters.value.filter((f) => f.status !== 'delete'))
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
meta.value?.columns?.filter((c: ColumnType) => {
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
/** hide system columns if not enabled */
return showSystemFields.value
} else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID || c.system) {
return false
} else {
const isVirtualSystemField = c.colOptions && c.system
return !isVirtualSystemField
}
}),
)
const types = computed(() => {
if (!meta.value?.columns?.length) {
return {}
}
return meta.value?.columns?.reduce((obj: any, col: any) => {
if (col.uidt === UITypes.Formula) {
const formulaUIType = getEquivalentUIType({
formulaColumn: col,
})
obj[col.id] = formulaUIType || col.uidt
}
// if column is a lookup column, then use the lookup type extracted from the column
else if (btLookupTypesMap.value[col.id]) {
obj[col.id] = btLookupTypesMap.value[col.id].uidt
} else {
obj[col.id] = col.uidt
}
return obj
}, {})
})
const lastFilters = ref<Filter[]>([])
watchOnce(filters, (filters: Filter[]) => {
lastFilters.value = clone(filters)
})
// get delta between two objects and return the changed fields (value is from b)
const getFieldDelta = (a: any, b: any) => {
return Object.entries(b)
.filter(([key, val]) => a[key] !== val && key in a)
.reduce((a, [key, v]) => ({ ...a, [key]: v }), {})
}
const isComparisonOpAllowed = (
filter: FilterType,
compOp: {
text: string
value: string
ignoreVal?: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
},
) => {
const isNullOrEmptyOp = ['empty', 'notempty', 'null', 'notnull'].includes(compOp.value)
const uidt = types.value[filter.fk_column_id]
if (compOp.includedTypes) {
// include allowed values only if selected column type matches
if (filter.fk_column_id && compOp.includedTypes.includes(uidt)) {
// for 'empty', 'notempty', 'null', 'notnull',
// show them based on `showNullAndEmptyInFilter` in Base Settings
return isNullOrEmptyOp ? baseMeta.value.showNullAndEmptyInFilter : true
} else {
return false
}
} else if (compOp.excludedTypes) {
// include not allowed values only if selected column type not matches
if (filter.fk_column_id && !compOp.excludedTypes.includes(uidt)) {
// for 'empty', 'notempty', 'null', 'notnull',
// show them based on `showNullAndEmptyInFilter` in Base Settings
return isNullOrEmptyOp ? baseMeta.value.showNullAndEmptyInFilter : true
} else {
return false
}
}
// explicitly include for non-null / non-empty ops
return isNullOrEmptyOp ? baseMeta.value.showNullAndEmptyInFilter : true
}
const isComparisonSubOpAllowed = (
filter: FilterType,
compOp: {
text: string
value: string
ignoreVal?: boolean
includedTypes?: UITypes[]
excludedTypes?: UITypes[]
},
) => {
const uidt = types.value[filter.fk_column_id]
if (compOp.includedTypes) {
// include allowed values only if selected column type matches
return filter.fk_column_id && compOp.includedTypes.includes(uidt)
} else if (compOp.excludedTypes) {
// include not allowed values only if selected column type not matches
return filter.fk_column_id && !compOp.excludedTypes.includes(uidt)
}
}
const placeholderFilter = (): Filter => {
const logicalOps = new Set(filters.value.slice(1).map((filter) => filter.logical_op))
return {
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value as FilterType['comparison_op'],
value: null,
status: 'create',
logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and',
}
}
const placeholderGroupFilter = (): Filter => {
const logicalOps = new Set(filters.value.slice(1).map((filter) => filter.logical_op))
return {
is_group: true,
status: 'create',
logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and',
}
}
const loadAllChildFilters = async (filters: Filter[]) => {
// Array to store promises of child filter loading
const promises = []
// Array to store all child filters
const allChildFilters: Filter[] = []
// Iterate over all filters
for (const filter of filters) {
// Check if the filter is a group
if (filter.id && filter.is_group) {
// Load children filters from the backend
const childFilterPromise = $api.dbTableFilter.childrenRead(filter.id).then((response) => {
const childFilters = response.list as Filter[]
allChildFilters.push(...childFilters)
return loadAllChildFilters(childFilters)
})
promises.push(childFilterPromise)
}
}
// Wait for all promises to resolve
await Promise.all(promises)
// Push all child filters into the allFilters array
allFilters.value.push(...allChildFilters)
}
const loadFilters = async (hookId?: string, isWebhook = false, loadAllFilters = false) => {
if (!view.value?.id) return
if (nestedMode.value) {
// ignore restoring if not root filter group
return
}
try {
if (isWebhook || hookId) {
if (parentId.value) {
filters.value = (await $api.dbTableFilter.childrenRead(parentId.value)).list as Filter[]
} else if (hookId) {
filters.value = (await $api.dbTableWebhookFilter.read(hookId)).list as Filter[]
}
} else {
if (parentId.value) {
filters.value = (await $api.dbTableFilter.childrenRead(parentId.value)).list as Filter[]
} else {
filters.value = (await $api.dbTableFilter.read(view.value!.id!)).list as Filter[]
if (loadAllFilters) {
allFilters.value = [...filters.value]
await loadAllChildFilters(allFilters.value)
}
}
}
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
const sync = async (hookId?: string, _nested = false) => {
try {
for (const [i, filter] of Object.entries(filters.value)) {
if (filter.status === 'delete') {
await $api.dbTableFilter.delete(filter.id as string)
if (filter.is_group) {
deleteFilterGroupFromAllFilters(filter)
} else {
allFilters.value = allFilters.value.filter((f) => f.id !== filter.id)
}
} else if (filter.status === 'update') {
await $api.dbTableFilter.update(filter.id as string, {
...filter,
fk_parent_id: parentId.value,
})
} else if (filter.status === 'create') {
// extract children value if found to restore
const children = filters.value[+i]?.children
if (hookId) {
filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, {
...filter,
fk_parent_id: parentId.value,
})) as unknown as FilterType
} else {
filters.value[+i] = await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId.value,
})
}
if (children) filters.value[+i].children = children
allFilters.value.push(filters.value[+i])
}
}
if (!isWebhook) reloadData?.()
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
const saveOrUpdate = async (filter: Filter, i: number, force = false, undo = false, skipDataReload = false) => {
if (!view.value) return
if (!undo) {
const lastFilter = lastFilters.value[i]
if (lastFilter) {
const delta = clone(getFieldDelta(filter, lastFilter))
if (Object.keys(delta).length > 0) {
addUndo({
undo: {
fn: (prop: string, data: any) => {
const f = filters.value[i]
if (f) {
f[prop as keyof Filter] = data
saveOrUpdate(f, i, force, true)
}
},
args: [Object.keys(delta)[0], Object.values(delta)[0]],
},
redo: {
fn: (prop: string, data: any) => {
const f = filters.value[i]
if (f) {
f[prop as keyof Filter] = data
saveOrUpdate(f, i, force, true)
}
},
args: [Object.keys(delta)[0], filter[Object.keys(delta)[0] as keyof Filter]],
},
scope: defineViewScope({ view: activeView.value }),
})
}
}
}
try {
if (nestedMode.value) {
filters.value[i] = { ...filter }
filters.value = [...filters.value]
} else if (!autoApply?.value && !force) {
filter.status = filter.id ? 'update' : 'create'
} else if (filter.id && filter.status !== 'create') {
await $api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: parentId.value,
})
$e('a:filter:update', {
logical: filter.logical_op,
comparison: filter.comparison_op,
})
} else {
filters.value[i] = await $api.dbTableFilter.create(view.value.id!, {
...filter,
fk_parent_id: parentId.value,
})
allFilters.value.push(filters.value[+i])
}
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
}
lastFilters.value = clone(filters.value)
if (!isWebhook && !skipDataReload) reloadData?.()
}
function deleteFilterGroupFromAllFilters(filter: Filter) {
// Find all child filters of the specified parentId
const childFilters = allFilters.value.filter((f) => f.fk_parent_id === filter.id)
// Recursively delete child filter of child filter
childFilters.forEach((childFilter) => {
if (childFilter.is_group) {
deleteFilterGroupFromAllFilters(childFilter)
}
})
// Remove the parent object and its children from the array
allFilters.value = allFilters.value.filter((f) => f.id !== filter.id && f.fk_parent_id !== filter.id)
}
const deleteFilter = async (filter: Filter, i: number, undo = false) => {
if (!undo && !filter.is_group) {
addUndo({
undo: {
fn: async (fl: Filter) => {
fl.status = 'create'
filters.value.splice(i, 0, fl)
await saveOrUpdate(fl, i, false, true)
},
args: [clone(filter)],
},
redo: {
fn: async (index: number) => {
await deleteFilter(filters.value[index], index, true)
},
args: [i],
},
scope: defineViewScope({ view: activeView.value }),
})
}
// if shared or sync permission not allowed simply remove it from array
if (nestedMode.value) {
filters.value.splice(i, 1)
filters.value = [...filters.value]
if (!isWebhook) reloadData?.()
} else {
if (filter.id) {
// if auto-apply disabled mark it as disabled
if (!autoApply?.value) {
filter.status = 'delete'
// if auto-apply enabled invoke delete api and remove from array
// no splice is required here
} else {
try {
await $api.dbTableFilter.delete(filter.id)
if (!isWebhook) reloadData?.()
filters.value.splice(i, 1)
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
// if not synced yet remove it from array
} else {
filters.value.splice(i, 1)
}
$e('a:filter:delete', { length: nonDeletedFilters.value.length })
}
if (filter.is_group) {
deleteFilterGroupFromAllFilters(filter)
} else {
allFilters.value = allFilters.value.filter((f) => f.id !== filter.id)
}
}
const saveOrUpdateDebounced = useDebounceFn(saveOrUpdate, 500)
const addFilter = async (undo = false, draftFilter: Partial<FilterType> = {}) => {
filters.value.push(draftFilter?.fk_column_id ? { ...placeholderFilter(), ...draftFilter } : placeholderFilter())
if (!undo) {
addUndo({
undo: {
fn: async function undo(this: UndoRedoAction, i: number) {
this.redo.args = [i, clone(filters.value[i])]
await deleteFilter(filters.value[i], i, true)
},
args: [filters.value.length - 1],
},
redo: {
fn: async (i: number, fl: Filter) => {
fl.status = 'create'
filters.value.splice(i, 0, fl)
await saveOrUpdate(fl, i, false, true)
},
args: [],
},
scope: defineViewScope({ view: activeView.value }),
})
}
lastFilters.value = clone(filters.value)
$e('a:filter:add', { length: filters.value.length })
}
const addFilterGroup = async () => {
const child = placeholderFilter()
const placeHolderGroupFilter: Filter = placeholderGroupFilter()
if (nestedMode.value) placeHolderGroupFilter.children = [child]
filters.value.push(placeHolderGroupFilter)
const index = filters.value.length - 1
await saveOrUpdate(filters.value[index], index)
lastFilters.value = clone(filters.value)
$e('a:filter:add', { length: filters.value.length, group: true })
}
/** on column delete reload filters, identify by checking columns count */
watch(
() => {
if (!view?.value || !metas?.value?.[view?.value?.fk_model_id as string]) {
return 0
}
return metas?.value?.[view?.value?.fk_model_id as string]?.columns?.length || 0
},
async (nextColsLength: number, oldColsLength: number) => {
if (nextColsLength && nextColsLength < oldColsLength) await loadFilters()
},
)
// method to extract looked up column meta for all bt lookup columns
// it helps to decide the condition operations for the column
const loadBtLookupTypes = async () => {
const btLookupTypes = {}
try {
for (const col of meta.value?.columns || []) {
if (col.uidt !== UITypes.Lookup) continue
let nextCol = col
// check all the relation of nested lookup columns is bt or not
// include the column only if all only if all relations are bt
while (nextCol && nextCol.uidt === UITypes.Lookup) {
// extract the relation column meta
const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find(
(c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id,
)
const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id)
nextCol = relatedTableMeta?.columns?.find((c) => c.id === (nextCol.colOptions as LookupType).fk_lookup_column_id)
// if next column is same as root lookup column then break the loop
// since it's going to be a circular loop
if (nextCol.id === col.id) {
break
}
}
btLookupTypes[col.id] = nextCol
}
btLookupTypesMap.value = btLookupTypes
} catch (e) {
// ignore error since it is not blocking any functionality of the app
console.error(e)
}
}
return {
filters,
nonDeletedFilters,
loadFilters,
sync,
deleteFilter,
saveOrUpdate,
addFilter,
addFilterGroup,
saveOrUpdateDebounced,
isComparisonOpAllowed,
isComparisonSubOpAllowed,
loadBtLookupTypes,
btLookupTypesMap,
types,
}
}