diff --git a/packages/nc-gui/components/cell/Currency.vue b/packages/nc-gui/components/cell/Currency.vue
index b91e51f173..cfd2f0bbea 100644
--- a/packages/nc-gui/components/cell/Currency.vue
+++ b/packages/nc-gui/components/cell/Currency.vue
@@ -107,7 +107,7 @@ onMounted(() => {
v-model="vModel"
type="number"
class="nc-cell-field h-full border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
- :class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
+ :class="isForm && !isEditColumn && !hidePrefix ? 'flex flex-1' : 'w-full'"
:placeholder="placeholder"
:disabled="readOnly"
@blur="onBlur"
diff --git a/packages/nc-gui/components/smartsheet/Form.vue b/packages/nc-gui/components/smartsheet/Form.vue
index cd9085a3ae..e97b6898a6 100644
--- a/packages/nc-gui/components/smartsheet/Form.vue
+++ b/packages/nc-gui/components/smartsheet/Form.vue
@@ -93,15 +93,24 @@ const {
clearValidate,
fieldMappings,
isValidRedirectUrl,
+ loadAllviewFilters,
+ allViewFilters,
+ checkFieldVisibility,
} = useProvideFormViewStore(meta, view, formViewData, updateFormView, isEditable)
const { preFillFormSearchParams } = storeToRefs(useViewsStore())
const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook())
-reloadEventHook.on(async () => {
- await Promise.all([loadFormView(), loadReleatedMetas()])
- setFormData()
+reloadEventHook.on(async (params) => {
+ if (params?.isFormFieldFilters) {
+ setTimeout(() => {
+ checkFieldVisibility()
+ }, 100)
+ } else {
+ await Promise.all([loadFormView(), loadReleatedMetas()])
+ setFormData()
+ }
})
const { fields, showAll, hideAll } = useViewColumnsOrThrow()
@@ -303,10 +312,16 @@ const updatePreFillFormSearchParams = useDebounceFn(() => {
async function submitForm() {
if (isLocked.value || !isUIAllowed('dataInsert')) return
- for (const col of visibleColumns.value) {
- if (isRequired(col, col.required) && formState.value[col.title] === undefined) {
+ for (const col of localColumns.value) {
+ if (col.show && col.title && isRequired(col, col.required) && formState.value[col.title] === undefined) {
formState.value[col.title] = null
}
+
+ // handle filter out conditionally hidden field data
+ if ((!col.visible || !col.show) && col.title) {
+ delete formState.value[col.title]
+ delete state.value[col.title]
+ }
}
try {
@@ -417,6 +432,8 @@ async function onMove(event: any, isVisibleFormFields = false) {
return 0
})
+ checkFieldVisibility()
+
$e('a:form-view:reorder')
}
@@ -520,6 +537,8 @@ function setFormData() {
.filter((f) => !hiddenColTypes.includes(f.uidt) && !systemFieldsIds.value.includes(f.fk_column_id))
.sort((a, b) => a.order - b.order)
.map((c) => ({ ...c, required: !!c.required }))
+
+ checkFieldVisibility()
}
async function updateEmail() {
@@ -687,6 +706,13 @@ async function loadReleatedMetas() {
)
}
+const updateActiveFieldDescription = (value) => {
+ if (!activeField.value || activeField.value?.description === value) return
+
+ activeField.value.description = value
+ updateColMeta(activeField.value)
+}
+
onMounted(async () => {
if (imageCropperData.value.src) {
URL.revokeObjectURL(imageCropperData.value.imageConfig.src)
@@ -696,9 +722,10 @@ onMounted(async () => {
isLoadingFormView.value = true
- await Promise.all([loadFormView(), loadReleatedMetas()])
+ await Promise.all([loadFormView(), loadReleatedMetas(), loadAllviewFilters()])
setFormData()
+
isLoadingFormView.value = false
})
@@ -762,8 +789,8 @@ watch(activeField, (newValue, oldValue) => {
}
})
-watch([focusLabel, activeField], () => {
- if (activeField && focusLabel.value) {
+watch(focusLabel, () => {
+ if (activeField.value && focusLabel.value) {
nextTick(() => {
focusLabel.value?.focus()
})
@@ -1214,13 +1241,34 @@ useEventListener(
/>
-
-
- {{ element.label || element.title }}
-
-
*
+
+ Conditionally visible field
+
+
+
+
+
+
+
+ {{ element.label || element.title }}
+
+ *
+
-
+
+
+
@@ -1423,13 +1473,13 @@ useEventListener(
/>
@@ -1559,8 +1609,12 @@ useEventListener(
class="flex-1 flex items-center cursor-pointer max-w-[calc(100%_-_40px)]"
@click.prevent="onFormItemClick(field, true)"
>
-
-
+
+
@@ -1588,9 +1642,13 @@ useEventListener(
)
-
*
+
+
+ *
+
+
+
+
@@ -2009,6 +2067,32 @@ useEventListener(
}
}
}
+
+.icon-fade-enter-active,
+.icon-fade-leave-active {
+ transition: opacity 0.5s ease, transform 0.5s ease; /* Added scaling transition */
+ position: absolute;
+}
+
+.icon-fade-enter-from {
+ opacity: 0;
+ transform: scale(0.5); /* Start smaller and scale up */
+}
+
+.icon-fade-enter-to {
+ opacity: 1;
+ transform: scale(1); /* Scale to full size */
+}
+
+.icon-fade-leave-from {
+ opacity: 1;
+ transform: scale(1); /* Start at full size */
+}
+
+.icon-fade-leave-to {
+ opacity: 0;
+ transform: scale(0.5); /* Scale down and fade out */
+}
+
+
diff --git a/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue b/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
index d9990ffda3..7988db2571 100644
--- a/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
+++ b/packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
@@ -15,7 +15,12 @@ interface Props {
isOpen?: boolean
rootMeta?: any
linkColId?: string
+ parentColId?: string
actionBtnType?: 'text' | 'secondary'
+ /** Custom filter function */
+ filterOption?: (column: ColumnType) => boolean
+ visibilityError?: Record
+ disableAddNewFilter?: boolean
}
const props = withDefaults(defineProps(), {
@@ -27,7 +32,10 @@ const props = withDefaults(defineProps(), {
webHook: false,
link: false,
linkColId: undefined,
+ parentColId: undefined,
actionBtnType: 'text',
+ visibilityError: () => ({}),
+ disableAddNewFilter: false,
})
const emit = defineEmits(['update:filtersLength', 'update:draftFilter', 'update:modelValue'])
@@ -40,7 +48,19 @@ const draftFilter = useVModel(props, 'draftFilter', emit)
const modelValue = useVModel(props, 'modelValue', emit)
-const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook, link, linkColId } = toRefs(props)
+const {
+ nestedLevel,
+ parentId,
+ autoSave,
+ hookId,
+ showLoading,
+ webHook,
+ link,
+ linkColId,
+ parentColId,
+ visibilityError,
+ disableAddNewFilter,
+} = toRefs(props)
const nested = computed(() => nestedLevel.value > 0)
@@ -63,7 +83,7 @@ const isPublic = inject(IsPublicInj, ref(false))
const { $e } = useNuxtApp()
-const { nestedFilters } = useSmartsheetStoreOrThrow()
+const { nestedFilters, isForm } = useSmartsheetStoreOrThrow()
const currentFilters = modelValue.value || (!link.value && !webHook.value && nestedFilters.value) || []
@@ -72,7 +92,10 @@ const columns = computed(() => meta.value?.columns)
const fieldsToFilter = computed(() =>
(columns.value || []).filter((c) => {
if (link.value && isSystemColumn(c) && !c.pk && !isCreatedOrLastModifiedTimeCol(c)) return false
- return !excludedFilterColUidt.includes(c.uidt as UITypes)
+
+ const customFilter = props.filterOption ? props.filterOption(c) : true
+
+ return !excludedFilterColUidt.includes(c.uidt as UITypes) && customFilter
}),
)
@@ -96,7 +119,11 @@ const {
parentId,
computed(() => autoSave.value),
() => {
- reloadDataHook.trigger({ shouldShowLoading: showLoading.value, offset: 0 })
+ reloadDataHook.trigger({
+ shouldShowLoading: showLoading.value,
+ offset: 0,
+ isFormFieldFilters: isForm.value && !webHook.value,
+ })
reloadAggregate?.trigger()
},
currentFilters,
@@ -105,6 +132,7 @@ const {
link.value,
linkColId,
fieldsToFilter,
+ parentColId,
)
const { getPlanLimit } = useWorkspace()
@@ -247,7 +275,7 @@ const applyChanges = async (hookOrColId?: string, nested = false, isConditionSup
for (const nestedFilter of localNestedFilters.value) {
if (nestedFilter.parentId) {
- await nestedFilter.applyChanges(hookOrColId, true)
+ await nestedFilter.applyChanges(hookOrColId, true, undefined)
}
}
}
@@ -525,56 +553,59 @@ const changeToDynamic = async (filter, i) => {
data-testid="nc-filter"
class="menu-filter-dropdown w-min"
:class="{
- 'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested,
+ 'max-h-[max(80vh,500px)] min-w-122 py-2 pl-4': !nested,
+ '!min-w-127.5': isForm && !webHook,
'!min-w-full !w-full !pl-0': !nested && webHook,
'min-w-full': nested,
}"
>
-
+
-
-
+
+
+
+
-
+
-
+
- {{ $t('activity.addFilter') }}
+ {{ isForm && !webHook ? $t('activity.addCondition') : $t('activity.addFilter') }}
-
+
- {{ $t('activity.addFilterGroup') }}
+ {{ isForm && !webHook ? $t('activity.addConditionGroup') : $t('activity.addFilterGroup') }}
-
+
- {{ $t('activity.addFilter') }}
+ {{ isForm && !webHook ? $t('activity.addCondition') : $t('activity.addFilter') }}
-
+
- {{ $t('activity.addFilterGroup') }}
+ {{ isForm && !webHook ? $t('activity.addConditionGroup') : $t('activity.addFilterGroup') }}
-
+
@@ -608,6 +639,10 @@ const changeToDynamic = async (filter, i) => {
:show-loading="false"
:root-meta="rootMeta"
:link-col-id="linkColId"
+ :parent-col-id="parentColId"
+ :filter-option="filterOption"
+ :visibility-error="visibilityError"
+ :disable-add-new-filter="disableAddNewFilter"
>
{{
@@ -695,190 +730,197 @@ const changeToDynamic = async (filter, i) => {
-
-
-
- {{ visibilityError[filter.fk_column_id!] ?? '' }}
+
+ {{ $t('title.fieldInaccessible') }}
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ compSubOp.text }}
+ {{ compSubOp.text }}
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
-
-
-
- {{ compSubOp.text }}
- {{ compSubOp.text }}
-
-
-
-
-
-
-
-
-
-
+
updateFilterValue(value, filter, i)"
+ @click.stop
+ />
-
-
-
- updateFilterValue(value, filter, i)"
- @click.stop
- />
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
Static condition
-
+
+
+
Filter based on static value
-
Filter based on static value
-
-
-
-
Dynamic condition
-
+
+
+
Filter based on dynamic value
-
Filter based on dynamic value
-
-
-
-
-
+
+
+
+
+
{
'mt-1 mb-2': filters.length,
}"
>
-
+
- {{ $t('activity.addFilter') }}
+ {{ isForm && !webHook ? $t('activity.addCondition') : $t('activity.addFilter') }}
-
+
- {{ $t('activity.addFilterGroup') }}
+ {{ isForm && !webHook ? $t('activity.addConditionGroup') : $t('activity.addFilterGroup') }}
@@ -929,11 +986,11 @@ const changeToDynamic = async (filter, i) => {
'mt-1 mb-2': filters.length,
}"
>
-
+
- {{ $t('activity.addFilter') }}
+ {{ isForm && !webHook ? $t('activity.addCondition') : $t('activity.addFilter') }}
@@ -942,12 +999,13 @@ const changeToDynamic = async (filter, i) => {
class="nc-btn-focus"
:type="actionBtnType"
size="small"
+ data-testid="add-filter-group"
@click.stop="addFilterGroup()"
>
- {{ $t('activity.addFilterGroup') }}
+ {{ isForm && !webHook ? $t('activity.addConditionGroup') : $t('activity.addFilterGroup') }}
@@ -961,7 +1019,7 @@ const changeToDynamic = async (filter, i) => {
'ml-0.5': !nested,
}"
>
- {{ $t('title.noFiltersAdded') }}
+ {{ isForm && !webHook ? $t('title.noConditionsAdded') : $t('title.noFiltersAdded') }}
diff --git a/packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue b/packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
index 4b673ffa03..e35e34d91d 100644
--- a/packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
+++ b/packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
@@ -164,6 +164,16 @@ const componentProps = computed(() => {
}
return {}
}
+ case 'isCurrency': {
+ return { hidePrefix: true }
+ }
+ case 'isRating': {
+ return {
+ style: {
+ minWidth: `${(column.value?.meta?.max || 5) * 19}px`,
+ },
+ }
+ }
default: {
return {}
}
diff --git a/packages/nc-gui/composables/useApi/interceptors.ts b/packages/nc-gui/composables/useApi/interceptors.ts
index 487cdadd98..d194a5a590 100644
--- a/packages/nc-gui/composables/useApi/interceptors.ts
+++ b/packages/nc-gui/composables/useApi/interceptors.ts
@@ -49,7 +49,8 @@ export function addAxiosInterceptors(api: Api) {
},
// Handle Error
async (error) => {
- let isSharedPage = route.value?.params?.typeOrId === 'base' || route.value?.params?.typeOrId === 'ERD' || route.value.meta.public;
+ const isSharedPage =
+ route.value?.params?.typeOrId === 'base' || route.value?.params?.typeOrId === 'ERD' || route.value.meta.public
// if cancel request then throw error
if (error.code === 'ERR_CANCELED') return Promise.reject(error)
diff --git a/packages/nc-gui/composables/useFormViewStore.ts b/packages/nc-gui/composables/useFormViewStore.ts
index 68efa1e975..b6cf015577 100644
--- a/packages/nc-gui/composables/useFormViewStore.ts
+++ b/packages/nc-gui/composables/useFormViewStore.ts
@@ -1,6 +1,6 @@
import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form'
-import type { ColumnType, FormType, TableType, ViewType } from 'nocodb-sdk'
+import type { ColumnType, FilterType, FormType, TableType, ViewType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { ValidateInfo } from 'ant-design-vue/es/form/useForm'
@@ -20,14 +20,25 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
const formResetHook = createEventHook()
+ const allViewFilters = ref>({})
+
const formState = ref>({})
+ const activeRow = ref('')
+
const localColumns = ref[]>([])
- const activeRow = ref('')
+ const localColumnsMapByFkColumnId = computed(() => {
+ return localColumns.value.reduce((acc, c) => {
+ acc[c.fk_column_id] = c
- const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => a.order - b.order))
+ return acc
+ }, {} as Record>)
+ })
+ const visibleColumns = computed(() =>
+ localColumns.value.filter((f) => f.show).sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)),
+ )
const activeField = computed(() => visibleColumns.value.find((c) => c.id === activeRow.value) || null)
const activeColumn = computed(() => {
@@ -168,6 +179,10 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
+ const loadAllviewFilters = async () => {}
+
+ function checkFieldVisibility() {}
+
return {
onReset: formResetHook.on,
formState,
@@ -185,6 +200,10 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
fieldMappings,
isValidRedirectUrl,
formViewData,
+ loadAllviewFilters,
+ allViewFilters,
+ localColumnsMapByFkColumnId,
+ checkFieldVisibility,
}
},
'form-view-store',
diff --git a/packages/nc-gui/composables/useSharedFormViewStore.ts b/packages/nc-gui/composables/useSharedFormViewStore.ts
index b5e725cba9..69fcb5db23 100644
--- a/packages/nc-gui/composables/useSharedFormViewStore.ts
+++ b/packages/nc-gui/composables/useSharedFormViewStore.ts
@@ -2,6 +2,7 @@ import dayjs from 'dayjs'
import type {
BoolType,
ColumnType,
+ FilterType,
FormColumnType,
FormType,
LinkToAnotherRecordType,
@@ -67,6 +68,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const preFilledDefaultValueformState = ref>({})
+ const allViewFilters = ref>({})
+
const isValidRedirectUrl = computed(
() => typeof sharedFormView.value?.redirect_url === 'string' && !!sharedFormView.value?.redirect_url?.trim(),
)
@@ -80,15 +83,45 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
}),
)
+ const localColumns = computed<(ColumnType & Record)[]>(() => {
+ return (columns.value || [])?.filter((c) => supportedFields(c))
+ })
+
+ const localColumnsMapByFkColumnId = computed(() => {
+ return localColumns.value.reduce((acc, c) => {
+ acc[c.fk_column_id] = c
+
+ return acc
+ }, {} as Record>)
+ })
+
+ const fieldVisibilityValidator = computed(() => {
+ return new FormFilters({
+ nestedGroupedFilters: allViewFilters.value,
+ formViewColumns: localColumns.value,
+ formViewColumnsMapByFkColumnId: localColumnsMapByFkColumnId.value,
+ formState: { ...(formState.value || {}), ...(additionalState.value || {}) },
+ isSharedForm: true,
+ isMysql: (_sourceId?: string) => {
+ return ['mysql', ClientType.MYSQL].includes(sharedView.value?.client || ClientType.MYSQL)
+ },
+ getMeta,
+ })
+ })
+
const formColumns = computed(
() =>
- columns.value
- ?.filter((c) => c.show)
- .filter(
- (col) => !isSystemColumn(col) && col.uidt !== UITypes.SpecificDBType && (!isVirtualCol(col) || isLinksOrLTAR(col.uidt)),
- ) || [],
+ columns.value?.filter((col) => {
+ const isVisible = col.show
+
+ return isVisible && supportedFields(col)
+ }) || [],
)
+ function supportedFields(col: ColumnType) {
+ return !isSystemColumn(col) && col.uidt !== UITypes.SpecificDBType && (!isVirtualCol(col) || isLinksOrLTAR(col.uidt))
+ }
+
const loadSharedView = async () => {
passwordError.value = null
@@ -105,6 +138,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
sharedFormView.value = viewMeta.view
meta.value = viewMeta.model
+ loadAllviewFilters(Array.isArray(viewMeta?.filter?.children) ? viewMeta?.filter?.children : [])
+
const fieldById = (viewMeta.columns || []).reduce(
(o: Record, f: Record) => ({
...o,
@@ -114,8 +149,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
)
columns.value = (viewMeta.model?.columns || [])
- .filter((c) => fieldById[c.id])
- .map((c) => {
+ .filter((c: ColumnType) => fieldById[c.id])
+ .map((c: ColumnType) => {
if (
!isSystemColumn(c) &&
!isVirtualCol(c) &&
@@ -145,10 +180,13 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
return {
...c,
+ order: fieldById[c.id].order || c.order,
+ visible: true,
meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) },
description: fieldById[c.id].description,
}
})
+ .sort((a: ColumnType, b: ColumnType) => (a.order ?? Infinity) - (b.order ?? Infinity))
const _sharedViewMeta = (viewMeta as any).meta
sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta
@@ -175,6 +213,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
}
await handlePreFillForm()
+
+ checkFieldVisibility()
} catch (e: any) {
const error = await extractSdkResponseErrorMsgv2(e)
@@ -212,27 +252,35 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
{
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
- if (isRequired(column)) {
+ if (isRequired(column) && column.show) {
if (typeof value === 'string') {
value = value.trim()
}
+ if (column.uidt === UITypes.Rating && (!value || Number(value) < 1)) {
+ return reject(t('msg.error.fieldRequired'))
+ }
+
if (
(column.uidt === UITypes.Checkbox && !value) ||
(column.uidt !== UITypes.Checkbox && !requiredFieldValidatorFn(value))
) {
return reject(t('msg.error.fieldRequired'))
}
-
- if (column.uidt === UITypes.Rating && (!value || Number(value) < 1)) {
- return reject(t('msg.error.fieldRequired'))
- }
}
return resolve()
})
},
},
+ {
+ validator: (_rule: RuleObject) => {
+ return new Promise((resolve) => {
+ checkFieldVisibility()
+ return resolve()
+ })
+ },
+ },
]
const additionalRules = extractFieldValidator(parseProp(column.meta).validators ?? [], column)
@@ -264,25 +312,36 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const { validate, validateInfos, clearValidate } = useForm(validationFieldState, validators)
- const handleAddMissingRequiredFieldDefaultState = () => {
- for (const col of formColumns.value) {
+ const handleAddMissingRequiredFieldDefaultState = async () => {
+ for (const col of localColumns.value) {
if (
col.title &&
+ col.show &&
+ col.visible &&
isRequired(col) &&
formState.value[col.title] === undefined &&
additionalState.value[col.title] === undefined
) {
if (isVirtualCol(col)) {
- additionalState.value[col.title] = null
+ additionalState.value = {
+ ...(additionalState.value || {}),
+ [col.title]: null,
+ }
} else {
formState.value[col.title] = null
}
}
+
+ // handle filter out conditionally hidden field data
+ if (!col.visible && col.title) {
+ delete formState.value[col.title]
+ delete additionalState.value[col.title]
+ }
}
}
const validateAllFields = async () => {
- handleAddMissingRequiredFieldDefaultState()
+ await handleAddMissingRequiredFieldDefaultState()
try {
// filter `undefined` keys which is hidden prefilled fields
@@ -362,6 +421,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
}
clearValidate()
+ checkFieldVisibility()
}
async function handlePreFillForm() {
@@ -403,6 +463,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
switch (sharedViewMeta.value.preFilledMode) {
case PreFilledMode.Hidden: {
c.show = false
+ c.meta = { ...parseProp(c.meta), preFilledHiddenField: true }
break
}
case PreFilledMode.Locked: {
@@ -636,7 +697,6 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
clearInterval(intvl)
}
clearForm()
- clearValidate()
}
})
@@ -661,6 +721,20 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
return false
}
+ function loadAllviewFilters(formViewFilters: FilterType[]) {
+ if (!formViewFilters.length) return
+
+ const formFilter = new FormFilters({ data: formViewFilters })
+
+ const allFilters = formFilter.getNestedGroupedFilters()
+
+ allViewFilters.value = { ...allFilters }
+ }
+
+ async function checkFieldVisibility() {
+ await fieldVisibilityValidator.value.validateVisibility()
+ }
+
watch(password, (next, prev) => {
if (next !== prev && passwordError.value) passwordError.value = null
})
@@ -720,6 +794,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
handleAddMissingRequiredFieldDefaultState,
fieldMappings,
isValidRedirectUrl,
+ loadAllviewFilters,
+ checkFieldVisibility,
}
}, 'shared-form-view-store')
diff --git a/packages/nc-gui/composables/useSmartsheetStore.ts b/packages/nc-gui/composables/useSmartsheetStore.ts
index c487bdff54..2cafb585a9 100644
--- a/packages/nc-gui/composables/useSmartsheetStore.ts
+++ b/packages/nc-gui/composables/useSmartsheetStore.ts
@@ -58,7 +58,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const sorts = ref(unref(initialSorts) ?? [])
const nestedFilters = ref(unref(initialFilters) ?? [])
- const allFilters = ref([])
+ const allFilters = ref([])
watch(
sorts,
diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts
index 7192f4b359..5260d41e1b 100644
--- a/packages/nc-gui/composables/useViewData.ts
+++ b/packages/nc-gui/composables/useViewData.ts
@@ -304,9 +304,17 @@ export function useViewData(
fk_column_id: c.id,
fk_view_id: viewMeta.value?.id,
...(fieldById[c.id!] ? fieldById[c.id!] : {}),
- meta: { validators: [], ...parseProp(fieldById[c.id!]?.meta), ...parseProp(c.meta) },
+ meta: {
+ validators: [],
+ visibility: {
+ errors: {},
+ },
+ ...parseProp(fieldById[c.id!]?.meta),
+ ...parseProp(c.meta),
+ },
order: (fieldById[c.id!] && fieldById[c.id!].order) || order++,
id: fieldById[c.id!] && fieldById[c.id!].id,
+ visible: true,
}))
.sort((a: Record, b: Record) => a.order - b.order) as Record[]
} catch (e: any) {
diff --git a/packages/nc-gui/composables/useViewFilters.ts b/packages/nc-gui/composables/useViewFilters.ts
index fc297e00ee..48ba6e323a 100644
--- a/packages/nc-gui/composables/useViewFilters.ts
+++ b/packages/nc-gui/composables/useViewFilters.ts
@@ -23,6 +23,7 @@ export function useViewFilters(
isLink?: boolean,
linkColId?: Ref,
fieldsToFilter?: Ref,
+ parentColId?: Ref,
) {
const savingStatus: Record = {}
@@ -34,7 +35,7 @@ export function useViewFilters(
const reloadHook = inject(ReloadViewDataHookInj)
- const { nestedFilters, allFilters } = useSmartsheetStoreOrThrow()
+ const { nestedFilters, allFilters, isForm } = useSmartsheetStoreOrThrow()
const { baseMeta } = storeToRefs(useBase())
@@ -54,10 +55,13 @@ export function useViewFilters(
const filters = computed({
get: () => {
- return nestedMode.value && !isLink && !isWebhook ? currentFilters.value! : _filters.value
+ return (nestedMode.value && !isLink && !isWebhook) || (isForm.value && !isWebhook) ? currentFilters.value! : _filters.value
},
set: (value: ColumnFilterType[]) => {
- if (nestedMode.value) {
+ if (isForm.value && !isWebhook) {
+ currentFilters.value = value
+ return
+ } else if (nestedMode.value) {
currentFilters.value = value
if (!isLink && !isWebhook) {
if (!isNestedRoot) {
@@ -204,6 +208,7 @@ export function useViewFilters(
fieldsToFilter?.value?.filter((col) => {
return !isSystemColumn(col)
})?.[0]?.id ?? undefined,
+ ...(parentColId?.value ? { fk_parent_column_id: parentColId.value } : {}),
}
}
@@ -214,6 +219,7 @@ export function useViewFilters(
is_group: true,
status: 'create',
logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and',
+ ...(parentColId?.value ? { fk_parent_column_id: parentColId.value, children: [] } : {}),
}
}
@@ -258,7 +264,7 @@ export function useViewFilters(
} = {}) => {
if (!view.value?.id) return
- if (nestedMode.value) {
+ if (nestedMode.value || (isForm.value && !isWebhook)) {
// ignore restoring if not root filter group
return
}
@@ -363,7 +369,7 @@ export function useViewFilters(
if (!view.value && !linkColId?.value) return
- if (!undo) {
+ if (!undo && !(isForm.value && !isWebhook)) {
const lastFilter = lastFilters.value[i]
if (lastFilter) {
const delta = clone(getFieldDelta(filter, lastFilter))
@@ -473,7 +479,7 @@ export function useViewFilters(
const deleteFilter = async (filter: ColumnFilterType, i: number, undo = false) => {
// update the filter status
filter.status = 'delete'
- if (!undo && !filter.is_group) {
+ if (!undo && !filter.is_group && !(isForm.value && !isWebhook)) {
addUndo({
undo: {
fn: async (fl: ColumnFilterType) => {
@@ -534,7 +540,7 @@ export function useViewFilters(
filters.value.push(
(draftFilter?.fk_column_id ? { ...placeholderFilter(), ...draftFilter } : placeholderFilter()) as ColumnFilterType,
)
- if (!undo) {
+ if (!undo && !(isForm.value && !isWebhook)) {
addUndo({
undo: {
fn: async function undo(this: UndoRedoAction, i: number) {
diff --git a/packages/nc-gui/context/index.ts b/packages/nc-gui/context/index.ts
index 9b9298a32c..aaff6c4ccc 100644
--- a/packages/nc-gui/context/index.ts
+++ b/packages/nc-gui/context/index.ts
@@ -26,8 +26,9 @@ export const ReadonlyInj: InjectionKey[> = Symbol('readonly-injectio
export const RowHeightInj: InjectionKey][> = Symbol('row-height-injection')
export const ScrollParentInj: InjectionKey][> = Symbol('scroll-parent-injection')
/** when shouldShowLoading bool is passed, it indicates if a loading spinner should be visible while reloading */
-export const ReloadViewDataHookInj: InjectionKey> =
- Symbol('reload-view-data-injection')
+export const ReloadViewDataHookInj: InjectionKey<
+ EventHook<{ shouldShowLoading?: boolean; offset?: number; isFormFieldFilters?: boolean } | void>
+> = Symbol('reload-view-data-injection')
export const ReloadViewMetaHookInj: InjectionKey> = Symbol('reload-view-meta-injection')
export const ReloadRowDataHookInj: InjectionKey> =
Symbol('reload-row-data-injection')
diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json
index 6d1e7b30eb..9e117d58f9 100644
--- a/packages/nc-gui/lang/en.json
+++ b/packages/nc-gui/lang/en.json
@@ -620,7 +620,9 @@
"fromScratch": "From scratch",
"fromFileAndExternalSources": "From files & external sources",
"directlyInRealTime": "Directly in real time",
- "categories": "Categories"
+ "categories": "Categories",
+ "fieldInaccessible": "Field inaccessible",
+ "noConditionsAdded": "No conditions added"
},
"labels": {
"modifiedOn": "Modified on",
@@ -985,7 +987,7 @@
"appearanceSettings": "Appearance settings",
"backgroundColor": "Background Color",
"hideNocodbBranding": "Hide NocoDB Branding",
- "showOnConditions": "Show on condtions",
+ "showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
@@ -1256,7 +1258,9 @@
},
"getPreFilledLink": "Get Pre-filled Link",
"group": "Group",
- "goToDocs": "Go to Docs"
+ "goToDocs": "Go to Docs",
+ "addCondition": "Add condition",
+ "addConditionGroup": "Add condition group"
},
"tooltip": {
"currentDateNotAvail": "Current date option not available for the data source or the data type",
diff --git a/packages/nc-gui/lib/form.ts b/packages/nc-gui/lib/form.ts
new file mode 100644
index 0000000000..f1022b74d1
--- /dev/null
+++ b/packages/nc-gui/lib/form.ts
@@ -0,0 +1,47 @@
+import type { ColumnType } from 'ant-design-vue/lib/table'
+import { type FilterType } from 'nocodb-sdk'
+
+type FormViewColumn = ColumnType & Record
+
+export class FormFilters {
+ allViewFilters: FilterType[]
+ protected groupedFilters: Record
+ nestedGroupedFilters: Record
+ formViewColumns: FormViewColumn[]
+ formViewColumnsMapByFkColumnId: Record
+ formState: Record
+ value: any
+ isMysql?: (sourceId?: string) => boolean
+
+ constructor({
+ data = [],
+ nestedGroupedFilters = {},
+ formViewColumns = [],
+ formViewColumnsMapByFkColumnId = {},
+ formState = {},
+ isMysql = undefined,
+ }: {
+ data?: FilterType[]
+ nestedGroupedFilters?: Record
+ formViewColumns?: FormViewColumn[]
+ formViewColumnsMapByFkColumnId?: Record
+ formState?: Record
+ isMysql?: (sourceId?: string) => boolean
+ } = {}) {
+ this.allViewFilters = data
+ this.groupedFilters = {}
+ this.nestedGroupedFilters = nestedGroupedFilters
+ this.formViewColumns = formViewColumns
+ this.formViewColumnsMapByFkColumnId = formViewColumnsMapByFkColumnId
+ this.formState = formState
+ this.isMysql = isMysql
+ }
+
+ setFilters(filters: FilterType[]) {
+ this.allViewFilters = filters
+ }
+
+ getNestedGroupedFilters() {}
+
+ validateVisibility() {}
+}
diff --git a/packages/nc-gui/utils/dataUtils.ts b/packages/nc-gui/utils/dataUtils.ts
index b0cbdd6a3d..bbe2a7369e 100644
--- a/packages/nc-gui/utils/dataUtils.ts
+++ b/packages/nc-gui/utils/dataUtils.ts
@@ -4,19 +4,19 @@ import { isColumnRequiredAndNull } from './columnUtils'
import type { Row } from '~/lib/types'
export const isValidValue = (val: unknown) => {
- if (val === null || val === undefined) {
+ if (ncIsNull(val) || ncIsUndefined(val)) {
return false
}
- if (typeof val === 'string' && val === '') {
+ if (ncIsString(val) && val === '') {
return false
}
- if (Array.isArray(val) && val.length === 0) {
+ if (ncIsEmptyArray(val)) {
return false
}
- if (typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 0) {
+ if (ncIsEmptyObject(val)) {
return false
}
diff --git a/packages/nc-gui/utils/is.ts b/packages/nc-gui/utils/is.ts
new file mode 100644
index 0000000000..8d46b4c008
--- /dev/null
+++ b/packages/nc-gui/utils/is.ts
@@ -0,0 +1,178 @@
+/**
+ * Checks if a value is an object (excluding null).
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is an object, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = { key: 'value' };
+ * console.log(ncIsObject(value)); // true
+ * ```
+ */
+export function ncIsObject(value: any): boolean {
+ return value !== null && typeof value === 'object' && !ncIsArray(value)
+}
+
+/**
+ * Checks if a value is an empty object.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is an empty object, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = {};
+ * console.log(ncIsEmptyObject(value)); // true
+ * ```
+ */
+export function ncIsEmptyObject(value: any): boolean {
+ return ncIsObject(value) && Object.keys(value).length === 0
+}
+
+/**
+ * Checks if a value is an array.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is an array, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = [1, 2, 3];
+ * console.log(ncIsArray(value)); // true
+ * ```
+ */
+export function ncIsArray(value: any): boolean {
+ return Array.isArray(value)
+}
+
+/**
+ * Checks if a value is an empty array.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is an empty array, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = [];
+ * console.log(ncIsEmptyArray(value)); // true
+ *
+ * const nonEmptyArray = [1, 2, 3];
+ * console.log(ncIsEmptyArray(nonEmptyArray)); // false
+ * ```
+ */
+export function ncIsEmptyArray(value: any): boolean {
+ return ncIsArray(value) && value.length === 0
+}
+
+/**
+ * Checks if a value is a string.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is a string, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = 'Hello, world!';
+ * console.log(ncIsString(value)); // true
+ * ```
+ */
+export function ncIsString(value: any): boolean {
+ return typeof value === 'string'
+}
+
+/**
+ * Checks if a value is a number.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is a number, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = 42;
+ * console.log(ncIsNumber(value)); // true
+ * ```
+ */
+export function ncIsNumber(value: any): boolean {
+ return typeof value === 'number' && !isNaN(value)
+}
+
+/**
+ * Checks if a value is a boolean.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is a boolean, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = true;
+ * console.log(ncIsBoolean(value)); // true
+ * ```
+ */
+export function ncIsBoolean(value: any): boolean {
+ return typeof value === 'boolean'
+}
+
+/**
+ * Checks if a value is undefined.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is undefined, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = undefined;
+ * console.log(ncIsUndefined(value)); // true
+ * ```
+ */
+export function ncIsUndefined(value: any): boolean {
+ return typeof value === 'undefined'
+}
+
+/**
+ * Checks if a value is null.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is null, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = null;
+ * console.log(ncIsNull(value)); // true
+ * ```
+ */
+export function ncIsNull(value: any): boolean {
+ return value === null
+}
+
+/**
+ * Checks if a value is a function.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is a function, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = () => {};
+ * console.log(ncIsFunction(value)); // true
+ * ```
+ */
+export function ncIsFunction(value: any): boolean {
+ return typeof value === 'function'
+}
+
+/**
+ * Checks if a value is a promise.
+ *
+ * @param value - The value to check.
+ * @returns {boolean} - True if the value is a Promise, false otherwise.
+ *
+ * @example
+ * ```typescript
+ * const value = new Promise((resolve) => resolve(true));
+ * console.log(ncIsPromise(value)); // true
+ * ```
+ */
+export function ncIsPromise(value: any): boolean {
+ return value instanceof Promise
+}
diff --git a/packages/noco-docs/docs/090.views/040.view-types/030.form.md b/packages/noco-docs/docs/090.views/040.view-types/030.form.md
index f26307d5e5..2a7dd54503 100644
--- a/packages/noco-docs/docs/090.views/040.view-types/030.form.md
+++ b/packages/noco-docs/docs/090.views/040.view-types/030.form.md
@@ -233,9 +233,6 @@ Example
**Configuration Steps**
1. Click on the required field in the **Form Area**.
-2. `Limit file types` Enter the permitted MIME types separated by a comma.
-3. `Limit number of files` Enter the maximum number of files that can be uploaded.
-1. Click on the required field in the **Form Area**.
2. In the `Limit file types` section, enter the permitted MIME types separated by a comma.
3. In the `Limit number of files` section, specify the maximum number of files that can be uploaded.
4. In the `Limit file size` section, specify the maximum allowable file size for the attachment.
@@ -247,7 +244,21 @@ Check out this video for a more detailed explanation with examples:
-## Pre-fill Form Fields
+## Field "Show On Conditions"
+Conditional fields in Form View allow you to control the visibility and behavior of chosen fields based on specific data entered in previous fields. You can dynamically hide or show form fields when certain filter conditions are met. This enables a tailored form experience, displaying only relevant fields based on user input.
+
+Note that, for constructing the condition, you can only use fields that are present in the form & are defined prior to the field for which the condition is being set. This also limits reordering of the fields in the form - reordering of the fields will be allowed as long as the condition requirement above is not broken.
+
+To enable conditional fields in Form View, Open form-view builder & follow the steps below:
+1. Select the field for which you want to set the condition.
+2. On the right side configuration panel, click on the Settings icon next to `Show On Condition` to open the conditional field settings.
+3. Click on `Add Filter` OR `Add Filter Group` to add new condition(s) as required.
+
+![Show On Condition](/img/v2/views/form-view/show-on-conditions.png)
+
+NocoDB suffixes an eye icon next to the field name in the form builder to indicate that the field is conditionally visible.
+
+## Pre-Filling Form Fields
NocoDB offers a convenient feature that allows pre-filling form fields with specific values by setting URL parameters. This functionality enables the creation of custom URLs with desired field values, streamlining data entry and enhancing user experience.
To construct a pre-filled form URL manually, ensure that the URL parameters are appropriately encoded in the following format: `?key1=value1&key2=value2`.
diff --git a/packages/noco-docs/static/img/v2/views/form-view/show-on-conditions.png b/packages/noco-docs/static/img/v2/views/form-view/show-on-conditions.png
new file mode 100644
index 0000000000..049bfe8475
Binary files /dev/null and b/packages/noco-docs/static/img/v2/views/form-view/show-on-conditions.png differ
diff --git a/packages/nocodb/src/controllers/filters.controller.ts b/packages/nocodb/src/controllers/filters.controller.ts
index 567e136962..95c5f5b2b1 100644
--- a/packages/nocodb/src/controllers/filters.controller.ts
+++ b/packages/nocodb/src/controllers/filters.controller.ts
@@ -7,6 +7,7 @@ import {
Param,
Patch,
Post,
+ Query,
Req,
UseGuards,
} from '@nestjs/common';
@@ -32,10 +33,12 @@ export class FiltersController {
async filterList(
@TenantContext() context: NcContext,
@Param('viewId') viewId: string,
+ @Query('includeAllFilters') includeAllFilters: string,
) {
return new PagedResponseImpl(
await this.filtersService.filterList(context, {
viewId,
+ includeAllFilters: includeAllFilters === 'true'
}),
);
}
diff --git a/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
index 46b8e7bc6d..733f7d64e3 100644
--- a/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
+++ b/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
@@ -49,6 +49,7 @@ import * as nc_059_invited_by from '~/meta/migrations/v2/nc_059_invited_by';
import * as nc_060_descriptions from '~/meta/migrations/v2/nc_060_descriptions';
import * as nc_061_integration_is_default from '~/meta/migrations/v2/nc_061_integration_is_default';
import * as nc_062_integration_store from '~/meta/migrations/v2/nc_062_integration_store';
+import * as nc_063_form_field_filter from '~/meta/migrations/v2/nc_063_form_field_filter';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@@ -109,6 +110,7 @@ export default class XcMigrationSourcev2 {
'nc_060_descriptions',
'nc_061_integration_is_default',
'nc_062_integration_store',
+ 'nc_063_form_field_filter',
]);
}
@@ -220,6 +222,8 @@ export default class XcMigrationSourcev2 {
return nc_061_integration_is_default;
case 'nc_062_integration_store':
return nc_062_integration_store;
+ case 'nc_063_form_field_filter':
+ return nc_063_form_field_filter;
}
}
}
diff --git a/packages/nocodb/src/meta/migrations/v2/nc_063_form_field_filter.ts b/packages/nocodb/src/meta/migrations/v2/nc_063_form_field_filter.ts
new file mode 100644
index 0000000000..0d545ed7e0
--- /dev/null
+++ b/packages/nocodb/src/meta/migrations/v2/nc_063_form_field_filter.ts
@@ -0,0 +1,16 @@
+import type { Knex } from 'knex';
+import { MetaTable } from '~/utils/globals';
+
+const up = async (knex: Knex) => {
+ await knex.schema.alterTable(MetaTable.FILTER_EXP, (table) => {
+ table.string('fk_parent_column_id', 20).index();
+ });
+};
+
+const down = async (knex: Knex) => {
+ await knex.schema.alterTable(MetaTable.FILTER_EXP, (table) => {
+ table.dropColumn('fk_parent_column_id');
+ });
+};
+
+export { up, down };
diff --git a/packages/nocodb/src/models/Column.ts b/packages/nocodb/src/models/Column.ts
index ea558d8046..4358e91705 100644
--- a/packages/nocodb/src/models/Column.ts
+++ b/packages/nocodb/src/models/Column.ts
@@ -957,6 +957,10 @@ export default class Column implements ColumnType {
await Filter.delete(context, filter.id, ncMeta);
}
}
+ {
+ await Filter.deleteAllByParentColumn(context, id, ncMeta);
+ }
+
// Set Gallery & Kanban view `fk_cover_image_col_id` value to null
await Column.deleteCoverImageColumnId(context, id, ncMeta);
diff --git a/packages/nocodb/src/models/Filter.ts b/packages/nocodb/src/models/Filter.ts
index 1835cc5b24..05d7d49d65 100644
--- a/packages/nocodb/src/models/Filter.ts
+++ b/packages/nocodb/src/models/Filter.ts
@@ -24,6 +24,7 @@ export default class Filter implements FilterType {
fk_model_id?: string;
fk_view_id?: string;
fk_hook_id?: string;
+ fk_parent_column_id?: string;
fk_column_id?: string;
fk_parent_id?: string;
fk_link_col_id?: string;
@@ -40,6 +41,7 @@ export default class Filter implements FilterType {
base_id?: string;
source_id?: string;
column?: Column;
+ order?: number;
constructor(data: Filter | FilterType) {
Object.assign(this, data);
@@ -73,7 +75,7 @@ export default class Filter implements FilterType {
public static async insert(
context: NcContext,
- filter: Partial & { order?: number },
+ filter: Partial,
ncMeta = Noco.ncMeta,
) {
const insertObj = extractProps(filter, [
@@ -82,6 +84,7 @@ export default class Filter implements FilterType {
'fk_hook_id',
'fk_link_col_id',
'fk_value_col_id',
+ 'fk_parent_column_id',
'fk_column_id',
'comparison_op',
'comparison_sub_op',
@@ -95,6 +98,7 @@ export default class Filter implements FilterType {
]);
const referencedModelColName = [
+ 'fk_parent_column_id',
'fk_view_id',
'fk_hook_id',
'fk_link_col_id',
@@ -106,7 +110,7 @@ export default class Filter implements FilterType {
if (!filter.source_id) {
let model: { base_id?: string; source_id?: string };
- if (filter.fk_view_id) {
+ if (filter.fk_view_id && !filter.fk_parent_column_id) {
model = await View.get(context, filter.fk_view_id, ncMeta);
} else if (filter.fk_hook_id) {
model = await Hook.get(context, filter.fk_hook_id, ncMeta);
@@ -116,6 +120,12 @@ export default class Filter implements FilterType {
{ colId: filter.fk_link_col_id },
ncMeta,
);
+ } else if (filter.fk_parent_column_id) {
+ model = await Column.get(
+ context,
+ { colId: filter.fk_parent_column_id },
+ ncMeta,
+ );
} else if (filter.fk_column_id) {
model = await Column.get(
context,
@@ -163,7 +173,7 @@ export default class Filter implements FilterType {
) {
if (!(id && (filter.fk_view_id || filter.fk_hook_id))) {
throw new Error(
- `Mandatory fields missing in FITLER_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})`,
);
}
const key = `${CacheScope.FILTER_EXP}:${id}`;
@@ -199,6 +209,15 @@ export default class Filter implements FilterType {
),
);
}
+ if (filter.fk_parent_column_id) {
+ p.push(
+ NocoCache.appendToList(
+ CacheScope.FILTER_EXP,
+ [filter.fk_parent_column_id],
+ key,
+ ),
+ );
+ }
if (filter.fk_parent_id) {
if (filter.fk_view_id) {
p.push(
@@ -218,6 +237,15 @@ export default class Filter implements FilterType {
),
);
}
+ if (filter.fk_parent_column_id) {
+ p.push(
+ NocoCache.appendToList(
+ CacheScope.FILTER_EXP,
+ [filter.fk_parent_column_id, filter.fk_parent_id],
+ key,
+ ),
+ );
+ }
p.push(
NocoCache.appendToList(
CacheScope.FILTER_EXP,
@@ -435,16 +463,18 @@ export default class Filter implements FilterType {
viewId,
hookId,
linkColId,
+ parentColId,
}: {
viewId?: string;
hookId?: string;
linkColId?: string;
+ parentColId?: string;
},
ncMeta = Noco.ncMeta,
): Promise {
const cachedList = await NocoCache.getList(
CacheScope.FILTER_EXP,
- [viewId || hookId || linkColId],
+ [parentColId || viewId || hookId || linkColId],
{
key: 'order',
},
@@ -454,12 +484,14 @@ export default class Filter implements FilterType {
if (!isNoneList && !filters.length) {
const condition: Record = {};
- if (viewId) {
+ if (viewId && !parentColId) {
condition.fk_view_id = viewId;
} else if (hookId) {
condition.fk_hook_id = hookId;
} else if (linkColId) {
condition.fk_link_col_id = linkColId;
+ } else if (parentColId) {
+ condition.fk_parent_column_id = parentColId;
}
filters = await ncMeta.metaList2(
@@ -476,7 +508,7 @@ export default class Filter implements FilterType {
await NocoCache.setList(
CacheScope.FILTER_EXP,
- [viewId || hookId || linkColId],
+ [parentColId || viewId || hookId || linkColId],
filters,
);
}
@@ -574,6 +606,32 @@ export default class Filter implements FilterType {
await deleteRecursively(filter);
}
+ static async deleteAllByParentColumn(
+ context: NcContext,
+ parentColId: string,
+ ncMeta = Noco.ncMeta,
+ ) {
+ const filter = await this.getFilterObject(context, { parentColId }, ncMeta);
+
+ const deleteRecursively = async (filter) => {
+ if (!filter) return;
+ for (const f of filter?.children || []) await deleteRecursively(f);
+ if (filter.id) {
+ await ncMeta.metaDelete(
+ context.workspace_id,
+ context.base_id,
+ MetaTable.FILTER_EXP,
+ filter.id,
+ );
+ await NocoCache.deepDel(
+ `${CacheScope.FILTER_EXP}:${filter.id}`,
+ CacheDelDirection.CHILD_TO_PARENT,
+ );
+ }
+ };
+ await deleteRecursively(filter);
+ }
+
public static async get(
context: NcContext,
id: string,
@@ -599,9 +657,33 @@ export default class Filter implements FilterType {
return this.castType(filterObj);
}
+ static async allViewFilterList(
+ context: NcContext,
+ { viewId }: { viewId: string },
+ ncMeta = Noco.ncMeta,
+ ) {
+ const cachedList = await NocoCache.getList(CacheScope.FILTER_EXP, [viewId]);
+ let { list: filterObjs } = cachedList;
+ const { isNoneList } = cachedList;
+
+ if (!isNoneList && !filterObjs.length) {
+ filterObjs = await ncMeta.metaList2(
+ context.workspace_id,
+ context.base_id,
+ MetaTable.FILTER_EXP,
+ {
+ condition: { fk_view_id: viewId },
+ },
+ );
+ await NocoCache.setList(CacheScope.FILTER_EXP, [viewId], filterObjs);
+ }
+
+ return filterObjs?.map((f) => this.castType(f)) || [];
+ }
+
static async rootFilterList(
context: NcContext,
- { viewId }: { viewId: any },
+ { viewId }: { viewId: string },
ncMeta = Noco.ncMeta,
) {
const cachedList = await NocoCache.getList(
@@ -613,6 +695,7 @@ export default class Filter implements FilterType {
);
let { list: filterObjs } = cachedList;
const { isNoneList } = cachedList;
+
if (!isNoneList && !filterObjs.length) {
filterObjs = await ncMeta.metaList2(
context.workspace_id,
@@ -627,6 +710,7 @@ export default class Filter implements FilterType {
);
await NocoCache.setList(CacheScope.FILTER_EXP, [viewId], filterObjs);
}
+
return filterObjs
?.filter((f) => !f.fk_parent_id)
?.map((f) => this.castType(f));
@@ -634,7 +718,7 @@ export default class Filter implements FilterType {
static async rootFilterListByHook(
context: NcContext,
- { hookId }: { hookId: any },
+ { hookId }: { hookId: string },
ncMeta = Noco.ncMeta,
) {
const cachedList = await NocoCache.getList(
@@ -663,12 +747,43 @@ export default class Filter implements FilterType {
?.map((f) => this.castType(f));
}
+ static async rootFilterListByParentColumn(
+ context: NcContext,
+ { parentColId }: { parentColId: string },
+ ncMeta = Noco.ncMeta,
+ ) {
+ const cachedList = await NocoCache.getList(
+ CacheScope.FILTER_EXP,
+ [parentColId],
+ { key: 'order' },
+ );
+ let { list: filterObjs } = cachedList;
+ const { isNoneList } = cachedList;
+ if (!isNoneList && !filterObjs.length) {
+ filterObjs = await ncMeta.metaList2(
+ context.workspace_id,
+ context.base_id,
+ MetaTable.FILTER_EXP,
+ {
+ condition: { fk_parent_column_id: parentColId },
+ orderBy: {
+ order: 'asc',
+ },
+ },
+ );
+ await NocoCache.setList(CacheScope.FILTER_EXP, [parentColId], filterObjs);
+ }
+ return filterObjs
+ ?.filter((f) => !f.fk_parent_id)
+ ?.map((f) => this.castType(f));
+ }
+
static async parentFilterList(
context: NcContext,
{
parentId,
}: {
- parentId: any;
+ parentId: string;
},
ncMeta = Noco.ncMeta,
) {
@@ -705,8 +820,8 @@ export default class Filter implements FilterType {
hookId,
parentId,
}: {
- hookId: any;
- parentId: any;
+ hookId: string;
+ parentId: string;
},
ncMeta = Noco.ncMeta,
) {
@@ -743,6 +858,50 @@ export default class Filter implements FilterType {
return filterObjs?.map((f) => this.castType(f));
}
+ static async parentFilterListByParentColumn(
+ context: NcContext,
+ {
+ parentColId,
+ parentId,
+ }: {
+ parentColId: string;
+ parentId: string;
+ },
+ ncMeta = Noco.ncMeta,
+ ) {
+ const cachedList = await NocoCache.getList(
+ CacheScope.FILTER_EXP,
+ [parentColId, parentId],
+ {
+ key: 'order',
+ },
+ );
+ let { list: filterObjs } = cachedList;
+ const { isNoneList } = cachedList;
+ if (!isNoneList && !filterObjs.length) {
+ filterObjs = await ncMeta.metaList2(
+ context.workspace_id,
+ context.base_id,
+ MetaTable.FILTER_EXP,
+ {
+ condition: {
+ fk_parent_id: parentId,
+ fk_parent_column_id: parentColId,
+ },
+ orderBy: {
+ order: 'asc',
+ },
+ },
+ );
+ await NocoCache.setList(
+ CacheScope.FILTER_EXP,
+ [parentColId, parentId],
+ filterObjs,
+ );
+ }
+ return filterObjs?.map((f) => this.castType(f));
+ }
+
static async hasEmptyOrNullFilters(
context: NcContext,
baseId: string,
@@ -787,7 +946,7 @@ export default class Filter implements FilterType {
static async rootFilterListByLink(
_context: NcContext,
- { columnId: _columnId }: { columnId: any },
+ { columnId: _columnId }: { columnId: string },
_ncMeta = Noco.ncMeta,
) {
return [];
diff --git a/packages/nocodb/src/models/HookFilter.ts b/packages/nocodb/src/models/HookFilter.ts
index 43873a5d5b..9f4eff723b 100644
--- a/packages/nocodb/src/models/HookFilter.ts
+++ b/packages/nocodb/src/models/HookFilter.ts
@@ -107,7 +107,7 @@ export default class Filter {
) {
if (!(id && filter.fk_view_id)) {
throw new Error(
- `Mandatory fields missing in FITLER_EXP cache population : id(${id}), fk_view_id(${filter.fk_view_id}), fk_parent_id(${filter.fk_view_id})`,
+ `Mandatory fields missing in FILTER_EXP cache population : id(${id}), fk_view_id(${filter.fk_view_id}), fk_parent_id(${filter.fk_view_id})`,
);
}
const key = `${CacheScope.FILTER_EXP}:${id}`;
diff --git a/packages/nocodb/src/models/View.ts b/packages/nocodb/src/models/View.ts
index c5e72a0e62..c3d4fb4853 100644
--- a/packages/nocodb/src/models/View.ts
+++ b/packages/nocodb/src/models/View.ts
@@ -417,6 +417,7 @@ export default class View implements ViewType {
{
...extractProps(filter, [
'id',
+ 'fk_parent_column_id',
'fk_column_id',
'comparison_op',
'comparison_sub_op',
@@ -1761,7 +1762,10 @@ export default class View implements ViewType {
if (viewColumns) {
for (let i = 0; i < viewColumns.length; i++) {
- const column = viewColumns[i];
+ const column =
+ view.type === ViewTypes.FORM
+ ? prepareForDb(viewColumns[i])
+ : viewColumns[i];
insertObjs.push({
...extractProps(column, [
@@ -2136,6 +2140,7 @@ export default class View implements ViewType {
filterInsertObjs.push({
...extractProps(filter, [
+ 'fk_parent_column_id',
'fk_column_id',
'comparison_op',
'comparison_sub_op',
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
index 6d5be8f364..4dd4b379d3 100644
--- a/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
@@ -408,7 +408,7 @@ export class DuplicateProcessor {
});
}
- this.debugLog(`job completed for ${job.id} (${JobTypes.DuplicateModel})`);
+ this.debugLog(`job completed for ${job.id} (${JobTypes.DuplicateColumn})`);
return { id: findWithIdentifier(idMap, sourceColumn.id) };
}
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
index e0b86e062c..df273fb338 100644
--- a/packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
@@ -242,6 +242,9 @@ export class ExportService {
for (const fl of view.filter.children) {
const tempFl = {
id: `${idMap.get(view.id)}::${fl.id}`,
+ fk_parent_column_id: fl.fk_parent_column_id
+ ? idMap.get(fl.fk_parent_column_id)
+ : null,
fk_column_id: idMap.get(fl.fk_column_id),
fk_parent_id: `${idMap.get(view.id)}::${fl.fk_parent_id}`,
is_group: fl.is_group,
diff --git a/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts b/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
index 37cdfdf7ab..82428ae930 100644
--- a/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
+++ b/packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
@@ -1344,6 +1344,7 @@ export class ImportService {
viewId: vw.id,
filter: withoutId({
...fl,
+ fk_parent_column_id: getIdOrExternalId(fl.fk_parent_column_id),
fk_column_id: getIdOrExternalId(fl.fk_column_id),
fk_parent_id: getIdOrExternalId(fl.fk_parent_id),
}),
diff --git a/packages/nocodb/src/schema/swagger-v2.json b/packages/nocodb/src/schema/swagger-v2.json
index 4797e46af4..681b17a086 100644
--- a/packages/nocodb/src/schema/swagger-v2.json
+++ b/packages/nocodb/src/schema/swagger-v2.json
@@ -5933,6 +5933,13 @@
"parameters": [
{
"$ref": "#/components/parameters/xc-token"
+ },
+ {
+ "schema": {
+ "type": "boolean"
+ },
+ "name": "includeAllFilters",
+ "in": "query"
}
]
},
@@ -14326,6 +14333,7 @@
"comparison_op": "eq",
"comparison_sub_op": null,
"created_at": "2023-03-02 18:18:05",
+ "fk_parent_column_id": "cd_d7ah9n2qfupgys",
"fk_column_id": "cl_d7ah9n2qfupgys",
"fk_hook_id": null,
"fk_parent_id": null,
@@ -14425,6 +14433,10 @@
],
"description": "Comparison Sub-Operator"
},
+ "fk_parent_column_id": {
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Foreign Key to parent column"
+ },
"fk_column_id": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Foreign Key to Column"
@@ -14479,6 +14491,11 @@
},
"value": {
"description": "The filter value. Can be NULL for some operators."
+ },
+ "order": {
+ "type": "number",
+ "description": "The order of the filter",
+ "example": 1
}
},
"readOnly": true,
diff --git a/packages/nocodb/src/schema/swagger.json b/packages/nocodb/src/schema/swagger.json
index b86fc4f94b..621171a8a3 100644
--- a/packages/nocodb/src/schema/swagger.json
+++ b/packages/nocodb/src/schema/swagger.json
@@ -6626,6 +6626,13 @@
"parameters": [
{
"$ref": "#/components/parameters/xc-auth"
+ },
+ {
+ "schema": {
+ "type": "boolean"
+ },
+ "name": "includeAllFilters",
+ "in": "query"
}
]
},
@@ -20121,6 +20128,7 @@
"comparison_op": "eq",
"comparison_sub_op": null,
"created_at": "2023-03-02 18:18:05",
+ "fk_parent_column_id": "cd_d7ah9n2qfupgys",
"fk_column_id": "cl_d7ah9n2qfupgys",
"fk_hook_id": null,
"fk_parent_id": null,
@@ -20220,6 +20228,10 @@
],
"description": "Comparison Sub-Operator"
},
+ "fk_parent_column_id": {
+ "$ref": "#/components/schemas/StringOrNull",
+ "description": "Foreign Key to parent column"
+ },
"fk_column_id": {
"$ref": "#/components/schemas/StringOrNull",
"description": "Foreign Key to Column"
@@ -20282,6 +20294,11 @@
},
"value": {
"description": "The filter value. Can be NULL for some operators."
+ },
+ "order": {
+ "type": "number",
+ "description": "The order of the filter",
+ "example": 1
}
},
"readOnly": true,
diff --git a/packages/nocodb/src/services/filters.service.ts b/packages/nocodb/src/services/filters.service.ts
index 8b4af05426..46e2eb3350 100644
--- a/packages/nocodb/src/services/filters.service.ts
+++ b/packages/nocodb/src/services/filters.service.ts
@@ -134,10 +134,14 @@ export class FiltersService {
return filter;
}
- async filterList(context: NcContext, param: { viewId: string }) {
- const filter = await Filter.rootFilterList(context, {
- viewId: param.viewId,
- });
+ async filterList(
+ context: NcContext,
+ param: { viewId: string; includeAllFilters?: boolean },
+ ) {
+ const filter = await (param.includeAllFilters
+ ? Filter.allViewFilterList(context, { viewId: param.viewId })
+ : Filter.rootFilterList(context, { viewId: param.viewId }));
+
return filter;
}
diff --git a/tests/playwright/pages/Dashboard/Form/formConditionalFields.ts b/tests/playwright/pages/Dashboard/Form/formConditionalFields.ts
new file mode 100644
index 0000000000..61165a0493
--- /dev/null
+++ b/tests/playwright/pages/Dashboard/Form/formConditionalFields.ts
@@ -0,0 +1,60 @@
+import { getTextExcludeIconText } from '../../../tests/utils/general';
+import BasePage from '../../Base';
+import { FormPage } from './index';
+import { expect } from '@playwright/test';
+
+export class FormConditionalFieldsPage extends BasePage {
+ readonly parent: FormPage;
+
+ constructor(parent: FormPage) {
+ super(parent.rootPage);
+ this.parent = parent;
+ }
+
+ get() {
+ return this.rootPage.getByTestId('nc-form-field-visibility-btn');
+ }
+
+ async click() {
+ await this.get().waitFor({ state: 'visible' });
+ await this.get().click();
+ await this.rootPage.getByTestId('nc-filter-menu').waitFor({ state: 'visible' });
+ }
+
+ async verify({ isDisabled, count, isVisible }: { isDisabled: boolean; count?: string; isVisible?: boolean }) {
+ const conditionalFieldBtn = this.get();
+
+ await conditionalFieldBtn.waitFor({ state: 'visible' });
+
+ if (isDisabled) {
+ await expect(conditionalFieldBtn).toHaveClass(/nc-disabled/);
+ } else {
+ await expect(conditionalFieldBtn).not.toHaveClass(/nc-disabled/);
+ }
+
+ if (count !== undefined) {
+ const conditionCount = await getTextExcludeIconText(conditionalFieldBtn);
+
+ await expect(conditionCount).toContain(count);
+ }
+
+ if (isVisible !== undefined) {
+ if (isVisible) {
+ }
+ }
+ }
+
+ async verifyVisibility({ title, isVisible }: { title: string; isVisible: boolean }) {
+ const field = this.parent.get().locator(`[data-testid="nc-form-fields"][data-title="${title}"]`);
+ await field.scrollIntoViewIfNeeded();
+
+ // Wait for icon change transition complete
+ await this.rootPage.waitForTimeout(300);
+
+ if (isVisible) {
+ await expect(field.locator('.nc-field-visibility-icon')).toHaveClass(/nc-field-visible/);
+ } else {
+ await expect(field.locator('.nc-field-visibility-icon')).not.toHaveClass(/nc-field-visible/);
+ }
+ }
+}
diff --git a/tests/playwright/pages/Dashboard/Form/index.ts b/tests/playwright/pages/Dashboard/Form/index.ts
index 7bcffffeed..c96cdf95bf 100644
--- a/tests/playwright/pages/Dashboard/Form/index.ts
+++ b/tests/playwright/pages/Dashboard/Form/index.ts
@@ -4,11 +4,13 @@ import { DashboardPage } from '..';
import BasePage from '../../Base';
import { ToolbarPage } from '../common/Toolbar';
import { TopbarPage } from '../common/Topbar';
+import { FormConditionalFieldsPage } from './formConditionalFields';
export class FormPage extends BasePage {
readonly dashboard: DashboardPage;
readonly toolbar: ToolbarPage;
readonly topbar: TopbarPage;
+ readonly conditionalFields: FormConditionalFieldsPage;
// todo: All the locator should be private
readonly addOrRemoveAllButton: Locator;
@@ -34,6 +36,7 @@ export class FormPage extends BasePage {
this.dashboard = dashboard;
this.toolbar = new ToolbarPage(this);
this.topbar = new TopbarPage(this);
+ this.conditionalFields = new FormConditionalFieldsPage(this);
this.addOrRemoveAllButton = dashboard
.get()
@@ -970,4 +973,15 @@ export class FormPage extends BasePage {
},
};
}
+
+ async verifyFieldConfigError({ title, hasError }: { title: string; hasError: boolean }) {
+ const field = this.get().locator(`[data-testid="nc-form-fields"][data-title="${title}"]`);
+ await field.scrollIntoViewIfNeeded();
+
+ if (hasError) {
+ await expect(field.locator('.nc-field-config-error')).toBeVisible();
+ } else {
+ await expect(field.locator('.nc-field-config-error')).not.toBeVisible();
+ }
+ }
}
diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts b/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
index 562be6c99a..98a83d845e 100644
--- a/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
+++ b/tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
@@ -34,7 +34,7 @@ export class ToolbarFilterPage extends BasePage {
}
async clickAddFilter() {
- await this.get().locator(`button:has-text("Add Filter")`).first().click();
+ await this.get().getByTestId('add-filter').first().click();
}
// can reuse code for addFilterGroup and addFilter
@@ -62,12 +62,12 @@ export class ToolbarFilterPage extends BasePage {
filterGroupIndex?: number;
filterLogicalOperator?: string;
}) {
- await this.get().locator(`button:has-text("Add Filter Group")`).last().click();
+ await this.get().getByTestId('add-filter-group').last().click();
const filterDropdown = this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex);
await filterDropdown.waitFor({ state: 'visible' });
const ADD_BUTTON_SELECTOR = `span:has-text("add")`;
const FILTER_GROUP_SUB_MENU_SELECTOR = `.nc-dropdown-filter-group-sub-menu`;
- const ADD_FILTER_SELECTOR = `.nc-menu-item:has-text("Add Filter")`;
+ const ADD_FILTER_SELECTOR = `[data-testid="add-filter-menu"].nc-menu-item`;
await filterDropdown.locator(ADD_BUTTON_SELECTOR).first().click();
const filterGroupSubMenu = this.rootPage.locator(FILTER_GROUP_SUB_MENU_SELECTOR).last();
@@ -132,7 +132,7 @@ export class ToolbarFilterPage extends BasePage {
skipWaitingResponse?: boolean;
}) {
if (!openModal) {
- await this.get().locator(`button:has-text("Add Filter")`).first().click();
+ await this.get().getByTestId('add-filter').first().click();
}
const filterCount = await this.get().locator('.nc-filter-wrapper').count();
@@ -164,9 +164,9 @@ export class ToolbarFilterPage extends BasePage {
}
}
- const selectedOpType = await getTextExcludeIconText(this.rootPage.locator('.nc-filter-operation-select'));
+ const selectedOpType = await getTextExcludeIconText(this.rootPage.locator('.nc-filter-operation-select').last());
if (selectedOpType !== operation) {
- await this.rootPage.locator('.nc-filter-operation-select').click();
+ await this.rootPage.locator('.nc-filter-operation-select').last().click();
// first() : filter list has >, >=
if (skipWaitingResponse || filterCount === 1) {
@@ -448,7 +448,7 @@ export class ToolbarFilterPage extends BasePage {
}
async columnOperatorList(param: { columnTitle: string }) {
- await this.get().locator(`button:has-text("Add Filter")`).first().click();
+ await this.get().getByTestId('add-filter').first().click();
const selectedField = await this.rootPage.locator('.nc-filter-field-select').textContent();
if (selectedField !== param.columnTitle) {
diff --git a/tests/playwright/pages/SharedForm/index.ts b/tests/playwright/pages/SharedForm/index.ts
index 18b7c01dbf..941545a2d4 100644
--- a/tests/playwright/pages/SharedForm/index.ts
+++ b/tests/playwright/pages/SharedForm/index.ts
@@ -107,4 +107,14 @@ export class SharedFormPage extends BasePage {
},
};
}
+
+ async verifyField({ title, isVisible }: { title: string; isVisible: boolean }) {
+ const field = this.fieldLabel({ title });
+
+ if (isVisible) {
+ await expect(field).toBeVisible();
+ } else {
+ await expect(field).not.toBeVisible();
+ }
+ }
}
diff --git a/tests/playwright/tests/db/views/viewForm.spec.ts b/tests/playwright/tests/db/views/viewForm.spec.ts
index 1719da3ae9..57ffe57a2b 100644
--- a/tests/playwright/tests/db/views/viewForm.spec.ts
+++ b/tests/playwright/tests/db/views/viewForm.spec.ts
@@ -1434,3 +1434,258 @@ test.describe('Form view: field validation', () => {
});
});
});
+
+test.describe('Form view: conditional fields', () => {
+ if (enableQuickRun() || !isEE()) test.skip();
+
+ let dashboard: DashboardPage;
+ let form: FormPage;
+ let context: any;
+ let api: Api;
+
+ test.beforeEach(async ({ page }) => {
+ context = await setup({ page, isEmptyProject: true });
+ dashboard = new DashboardPage(page, context.base);
+ form = dashboard.form;
+ });
+
+ test.afterEach(async () => {
+ await unsetup(context);
+ });
+
+ async function createTable({ tableName }: { tableName: string; type?: 'limitToRange' | 'attachment' }) {
+ api = new Api({
+ baseURL: 'http://localhost:8080/',
+ headers: {
+ 'xc-auth': context.token,
+ },
+ });
+
+ const columns = [
+ {
+ column_name: 'Id',
+ title: 'Id',
+ uidt: UITypes.ID,
+ },
+
+ {
+ column_name: 'Text',
+ title: 'Text',
+ uidt: UITypes.SingleLineText,
+ },
+ {
+ column_name: 'LongText',
+ title: 'LongText',
+ uidt: UITypes.LongText,
+ },
+ {
+ column_name: 'Email',
+ title: 'Email',
+ uidt: UITypes.Email,
+ },
+ {
+ column_name: 'PhoneNumber',
+ title: 'PhoneNumber',
+ uidt: UITypes.PhoneNumber,
+ meta: {
+ validate: true,
+ },
+ },
+ {
+ column_name: 'SingleSelect',
+ title: 'SingleSelect',
+ uidt: 'SingleSelect',
+ dtxp: "'jan','feb', 'mar','apr', 'may','jun','jul','aug','sep','oct','nov','dec'",
+ },
+ {
+ column_name: 'MultiSelect',
+ title: 'MultiSelect',
+ uidt: 'MultiSelect',
+ dtxp: "'jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'",
+ },
+ {
+ column_name: 'Number',
+ title: 'Number',
+ uidt: 'Number',
+ },
+ {
+ column_name: 'Decimal',
+ title: 'Decimal',
+ uidt: 'Decimal',
+ },
+ {
+ column_name: 'Currency',
+ title: 'Currency',
+ uidt: 'Currency',
+ meta: {
+ currency_locale: 'en-GB',
+ currency_code: 'UGX',
+ },
+ },
+ {
+ column_name: 'Percent',
+ title: 'Percent',
+ uidt: 'Percent',
+ },
+ {
+ column_name: 'Duration',
+ title: 'Duration',
+ uidt: 'Duration',
+ meta: {
+ duration: 0,
+ },
+ },
+ ];
+
+ const base = await api.base.read(context.base.id);
+ await api.source.tableCreate(context.base.id, base.sources?.[0].id, {
+ table_name: tableName,
+ title: tableName,
+ columns: columns,
+ });
+
+ await dashboard.rootPage.reload();
+ await dashboard.rootPage.waitForTimeout(100);
+
+ await dashboard.treeView.openTable({ title: tableName });
+
+ await dashboard.rootPage.waitForTimeout(500);
+
+ await dashboard.viewSidebar.createFormView({ title: 'NewForm' });
+ }
+
+ test('Form builder conditional field', async () => {
+ await createTable({ tableName: 'FormConditionalFields' });
+
+ const url = dashboard.rootPage.url();
+
+ await form.configureHeader({
+ title: 'Form conditional fields',
+ subtitle: 'Test form conditional fields',
+ });
+ await form.verifyHeader({
+ title: 'Form conditional fields',
+ subtitle: 'Test form conditional fields',
+ });
+
+ // 1. Verify first field conditions btn is disabled: we can't add conditions on first form field
+ await form.selectVisibleField({ title: 'Text' });
+
+ await form.conditionalFields.verify({ isDisabled: true });
+
+ await form.selectVisibleField({ title: 'Decimal' });
+
+ const fieldConditionsList = [
+ { column: 'Text', op: 'is equal', value: 'Spain', dataType: UITypes.SingleLineText },
+ { column: 'Email', op: 'is not equal', value: 'user@nocodb.com', dataType: UITypes.Email },
+ { column: 'Number', op: '=', value: '22', dataType: UITypes.Number },
+ ];
+
+ await form.conditionalFields.click();
+
+ for (let i = 0; i < fieldConditionsList.length; i++) {
+ await form.toolbar.filter.add({
+ title: fieldConditionsList[i].column,
+ operation: fieldConditionsList[i].op,
+ // subOperation: param.opSubType,
+ value: fieldConditionsList[i].value,
+ locallySaved: false,
+ dataType: fieldConditionsList[i].dataType,
+ openModal: false,
+ skipWaitingResponse: true,
+ });
+ }
+
+ await form.conditionalFields.click();
+
+ await form.conditionalFields.verify({ isDisabled: false, count: '3' });
+
+ await form.conditionalFields.verifyVisibility({ title: 'Decimal', isVisible: false });
+
+ await form.fillForm([{ field: 'Text', value: 'Spain' }]);
+ await form.fillForm([{ field: 'Email', value: 'user1@nocodb.com' }]);
+
+ await form.conditionalFields.verifyVisibility({ title: 'Decimal', isVisible: false });
+
+ await form.fillForm([{ field: 'Number', value: '22' }]);
+
+ await form.conditionalFields.verifyVisibility({ title: 'Decimal', isVisible: true });
+
+ await form.formHeading.scrollIntoViewIfNeeded();
+ await form.formHeading.click();
+ // reorder & verify error
+ await form.reorderFields({
+ sourceField: 'Number',
+ destinationField: 'Currency',
+ });
+
+ await form.verifyFieldConfigError({ title: 'Decimal', hasError: true });
+
+ await form.reorderFields({
+ sourceField: 'Number',
+ destinationField: 'Decimal',
+ });
+
+ await form.verifyFieldConfigError({ title: 'Decimal', hasError: false });
+
+ // hide & verify error
+ await form.formHeading.scrollIntoViewIfNeeded();
+ await form.formHeading.click();
+
+ await form.removeField({ field: 'Text', mode: 'hideField' });
+
+ await form.verifyFieldConfigError({ title: 'Decimal', hasError: true });
+
+ await form.conditionalFields.verifyVisibility({ title: 'Decimal', isVisible: true });
+
+ await form.removeField({ field: 'Text', mode: 'hideField' });
+
+ await form.verifyFieldConfigError({ title: 'Decimal', hasError: false });
+
+ await form.conditionalFields.verifyVisibility({ title: 'Decimal', isVisible: true });
+
+ await form.fillForm([{ field: 'Email', value: 'user@nocodb.com' }]);
+
+ await form.conditionalFields.verifyVisibility({ title: 'Decimal', isVisible: false });
+
+ await dashboard.rootPage.waitForTimeout(5000);
+
+ // Shared form view
+ const formLink = await dashboard.form.topbar.getSharedViewUrl();
+
+ await dashboard.rootPage.goto(formLink);
+ // fix me! kludge@hub; page wasn't getting loaded from previous step
+ await dashboard.rootPage.reload();
+
+ const sharedForm = new SharedFormPage(dashboard.rootPage);
+
+ await sharedForm.verifyField({ title: 'Decimal', isVisible: false });
+
+ await sharedForm.cell.fillText({
+ columnHeader: 'Text',
+ text: 'Spain',
+ type: UITypes.SingleLineText,
+ });
+
+ await sharedForm.cell.fillText({
+ columnHeader: 'Email',
+ text: 'user1@nocodb.com',
+ type: UITypes.Email,
+ });
+
+ await sharedForm.cell.fillText({
+ columnHeader: 'Number',
+ text: '22',
+ type: UITypes.Number,
+ });
+
+ await sharedForm.verifyField({ title: 'Decimal', isVisible: true });
+
+ await sharedForm.submit();
+ await sharedForm.verifySuccessMessage();
+
+ await dashboard.rootPage.goto(url);
+ // kludge- reload
+ await dashboard.rootPage.reload();
+ });
+});
]