Browse Source

Merge pull request #9609 from nocodb/nc-feat/enable-conditional-field-in-oss

Nc feat/enable conditional field in oss
pull/9614/head
Ramesh Mane 2 months ago committed by GitHub
parent
commit
cdd10811d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/nc-gui/components/cell/Currency.vue
  2. 1
      packages/nc-gui/components/cell/Decimal.vue
  3. 1
      packages/nc-gui/components/cell/Duration.vue
  4. 1
      packages/nc-gui/components/cell/Email.vue
  5. 1
      packages/nc-gui/components/cell/Float.vue
  6. 1
      packages/nc-gui/components/cell/Integer.vue
  7. 1
      packages/nc-gui/components/cell/Percent.vue
  8. 1
      packages/nc-gui/components/cell/PhoneNumber.vue
  9. 1
      packages/nc-gui/components/cell/Text.vue
  10. 1
      packages/nc-gui/components/cell/TextArea.vue
  11. 1
      packages/nc-gui/components/cell/Url.vue
  12. 38
      packages/nc-gui/components/smartsheet/form/field-config-error.vue
  13. 124
      packages/nc-gui/components/smartsheet/form/field-settings/visibility.vue
  14. 46
      packages/nc-gui/composables/useFormViewStore.ts
  15. 421
      packages/nc-gui/lib/form.ts
  16. 9
      packages/nocodb/src/models/Filter.ts
  17. 2
      tests/playwright/tests/db/views/viewForm.spec.ts

1
packages/nc-gui/components/cell/Currency.vue

@ -101,6 +101,7 @@ onMounted(() => {
{{ currencyMeta.currency_code }} {{ currencyMeta.currency_code }}
</span> </span>
</div> </div>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn && editEnabled)" v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn && editEnabled)"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Decimal.vue

@ -98,6 +98,7 @@ watch(isExpandedFormOpen, () => {
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Duration.vue

@ -76,6 +76,7 @@ const focus: VNodeRef = (el) =>
<template> <template>
<div class="duration-cell-wrapper"> <div class="duration-cell-wrapper">
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Email.vue

@ -76,6 +76,7 @@ watch(
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Float.vue

@ -46,6 +46,7 @@ const focus: VNodeRef = (el) =>
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Integer.vue

@ -93,6 +93,7 @@ function onKeyDown(e: any) {
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Percent.vue

@ -139,6 +139,7 @@ const onTabPress = (e: KeyboardEvent) => {
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
@focus="onWrapperFocus" @focus="onWrapperFocus"
> >
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled && (isExpandedFormOpen ? expandedEditEnabled || !percentMeta.is_progress : true)" v-if="!readOnly && editEnabled && (isExpandedFormOpen ? expandedEditEnabled || !percentMeta.is_progress : true)"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/PhoneNumber.vue

@ -60,6 +60,7 @@ watch(
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Text.vue

@ -30,6 +30,7 @@ const focus: VNodeRef = (el) =>
</script> </script>
<template> <template>
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/TextArea.vue

@ -242,6 +242,7 @@ watch(inputWrapperRef, () => {
> >
<LazyCellRichText v-model:value="vModel" sync-value-change read-only /> <LazyCellRichText v-model:value="vModel" sync-value-change read-only />
</div> </div>
<!-- eslint-disable vue/use-v-on-exact -->
<textarea <textarea
v-else-if="(editEnabled && !isVisible) || isForm" v-else-if="(editEnabled && !isVisible) || isForm"
:ref="focus" :ref="focus"

1
packages/nc-gui/components/cell/Url.vue

@ -81,6 +81,7 @@ watch(
<template> <template>
<div class="flex flex-row items-center justify-between w-full h-full"> <div class="flex flex-row items-center justify-between w-full h-full">
<!-- eslint-disable vue/use-v-on-exact -->
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"

38
packages/nc-gui/components/smartsheet/form/field-config-error.vue

@ -5,7 +5,41 @@ interface Props {
column: ColumnType column: ColumnType
mode: 'preview' | 'list' mode: 'preview' | 'list'
} }
defineProps<Props>() const props = defineProps<Props>()
const { column, mode } = toRefs(props)
const visibilityError = computed(() => {
return parseProp(column.value?.meta)?.visibility?.errors || {}
})
const firstErrorMsg = computed(() => {
const visibilityErr = Object.values(visibilityError.value ?? [])
if (visibilityErr.length) {
return visibilityErr[0]
}
})
</script> </script>
<template><div></div></template> <template>
<template v-if="mode === 'preview'">
<div v-if="Object.keys(visibilityError ?? {}).length" class="flex mt-2">
<NcTooltip :disabled="!firstErrorMsg" class="flex cursor-pointer" placement="bottom">
<template #title>
<div class="flex flex-col">
{{ firstErrorMsg }}
</div>
</template>
<div
class="nc-field-config-error validation-error text-[#CB3F36] bg-[#FFF2F1] rounded-lg inline-flex items-center gap-2 px-2 py-1"
>
<GeneralIcon icon="alertTriangle" />
<div class="flex">Configuration error</div>
</div>
</NcTooltip>
</div>
</template>
<template v-else>
<GeneralIcon v-if="Object.keys(visibilityError ?? {}).length" icon="alertTriangle" class="ml-1 flex-none !text-red-500" />
</template>
</template>

124
packages/nc-gui/components/smartsheet/form/field-settings/visibility.vue

@ -1,7 +1,46 @@
<script lang="ts" setup> <script lang="ts" setup>
const { activeField } = useFormViewStoreOrThrow() import type { ColumnType } from 'nocodb-sdk'
const { visibleColumns, activeField, allViewFilters, localColumnsMapByFkColumnId } = useFormViewStoreOrThrow()
const isOpen = ref<boolean>(false) const isOpen = ref<boolean>(false)
const allFilters = ref({})
provide(AllFiltersInj, allFilters)
const visibilityError = computed(() => {
return parseProp(activeField.value?.meta)?.visibility?.errors || {}
})
const hasvisibilityError = computed(() => Object.keys(visibilityError.value).length)
const visibilityFilters = computed(() => {
if (activeField.value?.fk_column_id && !allViewFilters.value[activeField.value.fk_column_id]) {
allViewFilters.value[activeField.value.fk_column_id] = []
}
return allViewFilters.value[`${activeField.value?.fk_column_id}`]
})
const isFirstField = computed(() => {
return !!(visibleColumns.value.length && visibleColumns.value[0].id === activeField.value?.id)
})
const filterOption = (column: ColumnType) => {
// hide active field from filter option
const isNotActiveField = column.id !== activeField.value?.fk_column_id
// show only form view visible columns and order is less than active field
const orderIsLessThanActiveField =
column.id && localColumnsMapByFkColumnId.value[column.id]
? (localColumnsMapByFkColumnId.value[column.id].order ?? Infinity) < (activeField.value?.order ?? Infinity)
: false
const isVisible = localColumnsMapByFkColumnId.value[column.id]?.show
return isNotActiveField && orderIsLessThanActiveField && isVisible
}
</script> </script>
<template> <template>
@ -11,28 +50,79 @@ const isOpen = ref<boolean>(false)
<div class="text-gray-800 font-medium">{{ $t('labels.showOnConditions') }}</div> <div class="text-gray-800 font-medium">{{ $t('labels.showOnConditions') }}</div>
<div class="flex flex-col"> <div class="flex flex-col">
<NcTooltip placement="left"> <NcDropdown
<template #title> v-if="visibilityFilters"
<div class="text-center"> v-model:visible="isOpen"
{{ $t('msg.info.thisFeatureIsOnlyAvailableInEnterpriseEdition') }} placement="bottomLeft"
:disabled="isFirstField && !visibilityFilters.length && !isOpen"
overlay-class-name="nc-form-field-visibility-dropdown"
>
<NcTooltip placement="left" :disabled="!isFirstField">
<template #title> Cannot add conditions to the first field in a form. </template>
<div
class="nc-form-field-visibility-btn border-1 rounded-lg py-1 px-3 flex items-center justify-between gap-2 !min-w-[170px] transition-all cursor-pointer select-none text-sm"
:class="{
'!border-brand-500 shadow-selected': isOpen,
'border-gray-200': !isOpen,
'bg-[#F0F3FF]': visibilityFilters.length,
'opacity-70 cursor-default nc-disabled': isFirstField && !visibilityFilters.length,
}"
data-testid="nc-form-field-visibility-btn"
>
<div
class="nc-form-field-visibility-conditions-count flex-1"
:class="{
'text-brand-500 ': visibilityFilters.length,
}"
>
{{
visibilityFilters.length
? `${visibilityFilters.length} condition${visibilityFilters.length !== 1 ? 's' : ''}`
: 'No conditions'
}}
</div>
<GeneralIcon v-if="hasvisibilityError" icon="alertTriangle" class="flex-none !text-red-500" />
<GeneralIcon
icon="settings"
class="flex-none w-4 h-4"
:class="{
'text-brand-500 ': visibilityFilters.length,
}"
/>
</div>
</NcTooltip>
<template #overlay>
<div
class="nc-form-field-visibility-dropdown-container"
:class="{
'py-2': !visibilityFilters.length,
}"
>
<SmartsheetToolbarColumnFilter
ref="fieldVisibilityRef"
v-model="visibilityFilters"
class="w-full"
:auto-save="true"
data-testid="nc-filter-menu"
:show-loading="false"
:parent-col-id="activeField.fk_column_id"
:filter-option="filterOption"
:visibility-error="visibilityError"
:disable-add-new-filter="isFirstField"
/>
</div> </div>
</template> </template>
<div </NcDropdown>
class="nc-form-field-visibility-btn border-1 rounded-lg py-1 px-3 flex items-center justify-between gap-2 !min-w-[170px] transition-all select-none text-sm opacity-50 cursor-not-allowed"
:class="{
'!border-brand-500': isOpen,
'border-gray-200': !isOpen,
}"
>
<div class="nc-form-field-visibility-conditions-count flex-1">No conditions</div>
<GeneralIcon icon="settings" class="flex-none w-4 h-4" />
</div>
</NcTooltip>
</div> </div>
</div> </div>
<div> <div>
<div class="text-sm text-gray-500">{{ $t('labels.showFieldOnConditionsMet') }}</div> <div class="text-sm text-gray-500">{{ $t('labels.showFieldOnConditionsMet') }}</div>
<div v-if="hasvisibilityError" class="mt-2 visibility-condition-input-error text-red-500">
Error conditions will not be used for determining field visibility.
</div>
</div> </div>
</div> </div>
</div> </div>

46
packages/nc-gui/composables/useFormViewStore.ts

@ -18,6 +18,10 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
const { t } = useI18n() const { t } = useI18n()
const { isMysql } = useBase()
const { getMeta } = useMetas()
const formResetHook = createEventHook<void>() const formResetHook = createEventHook<void>()
const allViewFilters = ref<Record<string, FilterType[]>>({}) const allViewFilters = ref<Record<string, FilterType[]>>({})
@ -36,6 +40,17 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
}, {} as Record<string, ColumnType & Record<string, any>>) }, {} as Record<string, ColumnType & Record<string, any>>)
}) })
const fieldVisibilityValidator = computed(() => {
return new FormFilters({
nestedGroupedFilters: allViewFilters.value,
formViewColumns: localColumns.value,
formViewColumnsMapByFkColumnId: localColumnsMapByFkColumnId.value,
formState: formState.value,
isMysql,
getMeta,
})
})
const visibleColumns = computed(() => const visibleColumns = computed(() =>
localColumns.value.filter((f) => f.show).sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)), localColumns.value.filter((f) => f.show).sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)),
) )
@ -71,7 +86,7 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
{ {
validator: (_rule: RuleObject, value: any) => { validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (isRequired(column, column.required)) { if (isRequired(column, column.required) && column.visible) {
if (typeof value === 'string') { if (typeof value === 'string') {
value = value.trim() value = value.trim()
} }
@ -88,6 +103,14 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
}) })
}, },
}, },
{
validator: (_rule: RuleObject) => {
return new Promise((resolve) => {
checkFieldVisibility()
return resolve()
})
},
},
] ]
const additionalRules = extractFieldValidator(parseProp(column.meta).validators ?? [], column) const additionalRules = extractFieldValidator(parseProp(column.meta).validators ?? [], column)
@ -179,9 +202,26 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
return required || (columnObj && columnObj.rqd && !columnObj.cdf) return required || (columnObj && columnObj.rqd && !columnObj.cdf)
} }
const loadAllviewFilters = async () => {} const loadAllviewFilters = async () => {
if (!viewMeta.value?.id) return
try {
const formViewFilters = (await $api.dbTableFilter.read(viewMeta.value.id, { includeAllFilters: true })).list || []
if (!formViewFilters.length) return
const formFilter = new FormFilters({ data: formViewFilters })
const allFilters = formFilter.getNestedGroupedFilters()
function checkFieldVisibility() {} allViewFilters.value = { ...allFilters }
} catch (e: any) {
console.error('Error loading view filters:', e)
}
}
async function checkFieldVisibility() {
await fieldVisibilityValidator.value.validateVisibility()
}
return { return {
onReset: formResetHook.on, onReset: formResetHook.on,

421
packages/nc-gui/lib/form.ts

@ -1,5 +1,6 @@
import type { ColumnType } from 'ant-design-vue/lib/table' import type { ColumnType } from 'ant-design-vue/lib/table'
import { type FilterType } from 'nocodb-sdk' import dayjs from 'dayjs'
import { type FilterType, type LinkToAnotherRecordType, type TableType, UITypes, isDateMonthFormat } from 'nocodb-sdk'
type FormViewColumn = ColumnType & Record<string, any> type FormViewColumn = ColumnType & Record<string, any>
@ -11,7 +12,9 @@ export class FormFilters {
formViewColumnsMapByFkColumnId: Record<string, FormViewColumn> formViewColumnsMapByFkColumnId: Record<string, FormViewColumn>
formState: Record<string, any> formState: Record<string, any>
value: any value: any
isSharedForm: boolean
isMysql?: (sourceId?: string) => boolean isMysql?: (sourceId?: string) => boolean
getMeta?: (tableIdOrTitle: string) => Promise<TableType | null>
constructor({ constructor({
data = [], data = [],
@ -20,6 +23,8 @@ export class FormFilters {
formViewColumnsMapByFkColumnId = {}, formViewColumnsMapByFkColumnId = {},
formState = {}, formState = {},
isMysql = undefined, isMysql = undefined,
isSharedForm = false,
getMeta = undefined,
}: { }: {
data?: FilterType[] data?: FilterType[]
nestedGroupedFilters?: Record<string, FilterType[]> nestedGroupedFilters?: Record<string, FilterType[]>
@ -27,6 +32,8 @@ export class FormFilters {
formViewColumnsMapByFkColumnId?: Record<string, FormViewColumn> formViewColumnsMapByFkColumnId?: Record<string, FormViewColumn>
formState?: Record<string, any> formState?: Record<string, any>
isMysql?: (sourceId?: string) => boolean isMysql?: (sourceId?: string) => boolean
isSharedForm?: boolean
getMeta?: (tableIdOrTitle: string) => Promise<TableType | null>
} = {}) { } = {}) {
this.allViewFilters = data this.allViewFilters = data
this.groupedFilters = {} this.groupedFilters = {}
@ -34,14 +41,422 @@ export class FormFilters {
this.formViewColumns = formViewColumns this.formViewColumns = formViewColumns
this.formViewColumnsMapByFkColumnId = formViewColumnsMapByFkColumnId this.formViewColumnsMapByFkColumnId = formViewColumnsMapByFkColumnId
this.formState = formState this.formState = formState
this.isSharedForm = isSharedForm
this.isMysql = isMysql this.isMysql = isMysql
this.getMeta = getMeta
} }
setFilters(filters: FilterType[]) { setFilters(filters: FilterType[]) {
this.allViewFilters = filters this.allViewFilters = filters
} }
getNestedGroupedFilters() {} 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
}
validateVisibility() {} validateErrors() {}
} }

9
packages/nocodb/src/models/Filter.ts

@ -171,9 +171,14 @@ export default class Filter implements FilterType {
filter: Partial<FilterType>, filter: Partial<FilterType>,
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
) { ) {
if (!(id && (filter.fk_view_id || filter.fk_hook_id))) { if (
!(
id &&
(filter.fk_view_id || filter.fk_hook_id || filter.fk_parent_column_id)
)
) {
throw new Error( throw new Error(
`Mandatory fields missing in FILTER_EXP cache population : id(${id}), fk_view_id(${filter.fk_view_id}), fk_hook_id(${filter.fk_hook_id})`, `Mandatory fields missing in FILTER_EXP cache population : id(${id}), fk_view_id(${filter.fk_view_id}), fk_hook_id(${filter.fk_hook_id}), fk_parent_column_id(${filter.fk_parent_column_id})`,
); );
} }
const key = `${CacheScope.FILTER_EXP}:${id}`; const key = `${CacheScope.FILTER_EXP}:${id}`;

2
tests/playwright/tests/db/views/viewForm.spec.ts

@ -1436,7 +1436,7 @@ test.describe('Form view: field validation', () => {
}); });
test.describe('Form view: conditional fields', () => { test.describe('Form view: conditional fields', () => {
if (enableQuickRun() || !isEE()) test.skip(); if (enableQuickRun()) test.skip();
let dashboard: DashboardPage; let dashboard: DashboardPage;
let form: FormPage; let form: FormPage;

Loading…
Cancel
Save