@ -2,21 +2,19 @@
import type { ColumnType , LinkToAnotherRecordType } from 'nocodb-sdk'
import type { ColumnType , LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes , UITypes , isVirtualCol } from 'nocodb-sdk'
import { RelationTypes , UITypes , isVirtualCol } from 'nocodb-sdk'
import { breakpointsTailwind } from '@vueuse/core'
import { breakpointsTailwind } from '@vueuse/core'
import tinycolor from 'tinycolor2'
import {
import {
DropZoneRef ,
DropZoneRef ,
IsSurveyFormInj ,
IsSurveyFormInj ,
computed ,
computed ,
isValidURL ,
onKeyStroke ,
onKeyStroke ,
onMounted ,
onMounted ,
provide ,
provide ,
ref ,
ref ,
useBreakpoints ,
useBreakpoints ,
useI18n ,
usePointerSwipe ,
usePointerSwipe ,
useSharedFormStoreOrThrow ,
useSharedFormStoreOrThrow ,
useStepper ,
useStepper ,
validateEmail ,
} from '#imports'
} from '#imports'
enum TransitionDirection {
enum TransitionDirection {
@ -36,8 +34,6 @@ const { md } = useBreakpoints(breakpointsTailwind)
const { v$ , formState , formColumns , submitForm , submitted , secondsRemain , sharedFormView , sharedViewMeta , onReset } =
const { v$ , formState , formColumns , submitForm , submitted , secondsRemain , sharedFormView , sharedViewMeta , onReset } =
useSharedFormStoreOrThrow ( )
useSharedFormStoreOrThrow ( )
const { t } = useI18n ( )
const { isMobileMode } = storeToRefs ( useConfigStore ( ) )
const { isMobileMode } = storeToRefs ( useConfigStore ( ) )
const isTransitioning = ref ( false )
const isTransitioning = ref ( false )
@ -80,7 +76,17 @@ const { index, goToPrevious, goToNext, isFirst, isLast, goTo } = useStepper(step
const field = computed ( ( ) => formColumns . value ? . [ index . value ] )
const field = computed ( ( ) => formColumns . value ? . [ index . value ] )
const columnValidationError = ref ( false )
const fieldHasError = computed ( ( ) => {
if ( field . value ? . title ) {
if ( isVirtualCol ( field . value ) ) {
return v$ . value . virtual [ field . value . title ] ? . $error
} else {
return v$ . value . localState [ field . value . title ] ? . $error
}
}
return false
} )
function isRequired ( column : ColumnType , required = false ) {
function isRequired ( column : ColumnType , required = false ) {
let columnObj = column
let columnObj = column
@ -123,41 +129,20 @@ function animate(target: AnimationTarget) {
} , transitionDuration . value / 2 )
} , transitionDuration . value / 2 )
}
}
async function validateColumn ( ) {
const validateField = async ( title : string , type : 'cell' | 'virtual' ) => {
const f = field . value !
const validationField = type === 'cell' ? v$ . value . localState [ title ] : v$ . value . virtual [ title ]
if ( parseProp ( f . meta ) ? . validate && formState . value [ f . title ! ] ) {
if ( f . uidt === UITypes . Email ) {
if ( validationField ) {
if ( ! validateEmail ( formState . value [ f . title ! ] ) ) {
return await validationField . $validate ( )
columnValidationError . value = true
} else {
message . error ( t ( 'msg.error.invalidEmail' ) )
return true
return false
}
} else if ( f . uidt === UITypes . URL ) {
if ( ! isValidURL ( formState . value [ f . title ! ] ) ) {
columnValidationError . value = true
message . error ( t ( 'msg.error.invalidURL' ) )
return false
}
}
}
}
return true
}
}
async function goNext ( animationTarget ? : AnimationTarget ) {
async function goNext ( animationTarget ? : AnimationTarget ) {
columnValidationError . value = false
if ( isLast . value || ! isStarted . value || submitted . value || dialogShow . value || ! field . value || ! field . value . title ) return
if ( isLast . value || ! isStarted . value || submitted . value ) return
if ( field . value ? . title && ! ( await validateField ( field . value . title , isVirtualCol ( field . value ) ? 'virtual' : 'cell' ) ) ) return
if ( ! field . value || ! field . value . title ) return
const validationField = v$ . value . localState [ field . value . title ]
if ( validationField ) {
const isValid = await validationField . $validate ( )
if ( ! isValid ) return
}
if ( ! ( await validateColumn ( ) ) ) return
animate ( animationTarget || AnimationTarget . ArrowRight )
animate ( animationTarget || AnimationTarget . ArrowRight )
@ -172,9 +157,7 @@ async function goNext(animationTarget?: AnimationTarget) {
}
}
async function goPrevious ( animationTarget ? : AnimationTarget ) {
async function goPrevious ( animationTarget ? : AnimationTarget ) {
if ( isFirst . value || ! isStarted . value || submitted . value ) return
if ( isFirst . value || ! isStarted . value || submitted . value || dialogShow . value ) return
columnValidationError . value = false
animate ( animationTarget || AnimationTarget . ArrowLeft )
animate ( animationTarget || AnimationTarget . ArrowLeft )
@ -188,7 +171,7 @@ function focusInput() {
const inputEl =
const inputEl =
( document . querySelector ( '.nc-cell input' ) as HTMLInputElement ) ||
( document . querySelector ( '.nc-cell input' ) as HTMLInputElement ) ||
( document . querySelector ( '.nc-cell textarea' ) as HTMLTextAreaElement ) ||
( document . querySelector ( '.nc-cell textarea' ) as HTMLTextAreaElement ) ||
( document . querySelector ( '.nc-cell [tabindex="0"]' ) as HTMLInput Element )
( document . querySelector ( '.nc-cell [tabindex="0"]' ) as HTMLElement )
if ( inputEl ) {
if ( inputEl ) {
activeCell . value = inputEl
activeCell . value = inputEl
@ -207,7 +190,7 @@ function resetForm() {
}
}
async function submit ( ) {
async function submit ( ) {
if ( submitted . value || ! ( await validateColumn ( ) ) ) return
if ( submitted . value ) return
dialogShow . value = false
dialogShow . value = false
submitForm ( )
submitForm ( )
}
}
@ -228,13 +211,27 @@ const handleFocus = () => {
}
}
}
}
const showSubmitConfirmModal = async ( ) => {
if ( field . value ? . title && ! ( await validateField ( field . value . title , isVirtualCol ( field . value ) ? 'virtual' : 'cell' ) ) ) {
return
}
dialogShow . value = true
setTimeout ( ( ) => {
/ / N c B u t t o n w i l l o n l y f o c u s i f d o c u m e n t h a s a l r e a d y f o c u s e d e l e m e n t
document . querySelector ( '.nc-survery-form__confirmation_modal div[tabindex="0"]' ) ? . focus ( )
document . querySelector ( '.nc-survey-form-btn-submit.nc-button' ) ? . focus ( )
} , 50 )
}
onKeyStroke ( [ 'ArrowLeft' , 'ArrowDown' ] , ( ) => {
onKeyStroke ( [ 'ArrowLeft' , 'ArrowDown' ] , ( ) => {
goPrevious ( AnimationTarget . ArrowLeft )
goPrevious ( AnimationTarget . ArrowLeft )
} )
} )
onKeyStroke ( [ 'ArrowRight' , 'ArrowUp' ] , ( ) => {
onKeyStroke ( [ 'ArrowRight' , 'ArrowUp' ] , ( ) => {
goNext ( AnimationTarget . ArrowRight )
goNext ( AnimationTarget . ArrowRight )
} )
} )
onKeyStroke ( [ 'Enter' , 'Space' ] , ( ) => {
onKeyStroke ( [ 'Enter' ] , async ( e ) => {
if ( submitted . value ) return
if ( submitted . value ) return
if ( ! isStarted . value && ! submitted . value ) {
if ( ! isStarted . value && ! submitted . value ) {
@ -244,7 +241,8 @@ onKeyStroke(['Enter', 'Space'], () => {
if ( dialogShow . value ) {
if ( dialogShow . value ) {
submit ( )
submit ( )
} else {
} else {
dialogShow . value = true
e . preventDefault ( )
showSubmitConfirmModal ( )
}
}
} else {
} else {
const activeElement = document . activeElement as HTMLElement
const activeElement = document . activeElement as HTMLElement
@ -277,16 +275,6 @@ onMounted(() => {
} )
} )
}
}
} )
} )
watch (
formState ,
( ) => {
columnValidationError . value = false
} ,
{
deep : true ,
} ,
)
< / script >
< / script >
< template >
< template >
@ -372,10 +360,7 @@ watch(
< div class = "flex justify-end mt-12" >
< div class = "flex justify-end mt-12" >
< div class = "flex items-center gap-3" >
< div class = "flex items-center gap-3" >
< div class = "hidden md:flex text-sm items-center gap-1 text-gray-800" >
< div class = "hidden md:flex text-sm items-center gap-1 text-gray-800" >
< span >
< span > { { $t ( 'labels.pressEnter' ) } } ↵ < / span >
{ { $t ( 'labels.pressEnter' ) } }
< / span >
< NcBadge class = "pl-4 pr-1 h-[21px] text-gray-600" > ↵ < / NcBadge >
< / div >
< / div >
< NcButton
< NcButton
: size = "isMobileMode ? 'medium' : 'small'"
: size = "isMobileMode ? 'medium' : 'small'"
@ -429,6 +414,7 @@ watch(
: data - testid = "`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
: data - testid = "`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
: column = "field"
: column = "field"
: read - only = "field?.read_only"
: read - only = "field?.read_only"
@ update : model - value = "validateField(field.title, 'virtual')"
/ >
/ >
< LazySmartsheetCell
< LazySmartsheetCell
@ -440,12 +426,20 @@ watch(
: column = "field"
: column = "field"
: edit - enabled = "!field?.read_only"
: edit - enabled = "!field?.read_only"
: read - only = "field?.read_only"
: read - only = "field?.read_only"
@ update : model - value = "validateField(field.title, 'cell')"
/ >
/ >
< div class = "flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1" >
< div class = "flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-xs my-2 px-1" >
< div v-for ="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500" >
< template v-if ="isVirtualCol(field)" >
{ { error . $message } }
< div v-for ="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500" >
< / div >
{ { error . $message } }
< / div >
< / template >
< template v-else >
< div v-for ="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500" >
{ { error . $message } }
< / div >
< / template >
< div v-if ="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center" >
< div v-if ="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center" >
{ { $t ( 'general.shift' ) } } < MdiAppleKeyboardShift class = "mx-1 text-primary" / > + { { $t ( 'general.enter' ) } }
{ { $t ( 'general.shift' ) } } < MdiAppleKeyboardShift class = "mx-1 text-primary" / > + { { $t ( 'general.enter' ) } }
@ -458,7 +452,7 @@ watch(
< div class = "ml-1 mt-4 flex w-full text-lg" >
< div class = "ml-1 mt-4 flex w-full text-lg" >
< div class = "flex-1 flex justify-end" >
< div class = "flex-1 flex justify-end" >
< div v-if ="isLast && !v$.$invalid " >
< div v-if ="isLast" >
< NcButton
< NcButton
: size = "isMobileMode ? 'medium' : 'small'"
: size = "isMobileMode ? 'medium' : 'small'"
: class = "
: class = "
@ -466,9 +460,9 @@ watch(
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
: ''
: ''
"
"
: disabled = "v$.localState[field.title]?.$error || columnValidation Error"
: disabled = "fieldHas Error"
data - testid = "nc-survey-form__btn-submit-confirm"
data - testid = "nc-survey-form__btn-submit-confirm"
@ click = "dialogShow = true "
@ click = "showSubmitConfirmModal "
>
>
{ { $t ( 'general.submit' ) } } form
{ { $t ( 'general.submit' ) } } form
< / NcButton >
< / NcButton >
@ -477,17 +471,9 @@ watch(
< div v -else class = "flex items-center gap-3" >
< div v -else class = "flex items-center gap-3" >
< div
< div
class = "hidden md:flex text-sm items-center gap-1"
class = "hidden md:flex text-sm items-center gap-1"
: class = "v$.localState[field.title]?.$error || columnValidation Error ? 'text-gray-200' : 'text-gray-800'"
: class = "fieldHas Error ? 'text-gray-200' : 'text-gray-800'"
>
>
< span >
< span > { { $t ( 'labels.pressEnter' ) } } ↵ < / span >
{ { $t ( 'labels.pressEnter' ) } }
< / span >
< NcBadge
class = "pl-4 pr-1 h-[21px]"
: class = "v$.localState[field.title]?.$error || columnValidationError ? 'text-gray-200' : 'text-gray-600'"
>
↵
< / NcBadge >
< / div >
< / div >
< NcButton
< NcButton
: size = "isMobileMode ? 'medium' : 'small'"
: size = "isMobileMode ? 'medium' : 'small'"
@ -498,7 +484,7 @@ watch(
? 'transform translate-y-[2px] translate-x-[2px] after:(!ring !ring-accent !ring-opacity-100)'
? 'transform translate-y-[2px] translate-x-[2px] after:(!ring !ring-accent !ring-opacity-100)'
: '' ,
: '' ,
] "
] "
: disabled = "v$.localState[field.title]?.$error || columnValidation Error"
: disabled = "fieldHas Error"
@ click = "goNext()"
@ click = "goNext()"
>
>
{ { $t ( 'labels.next' ) } }
{ { $t ( 'labels.next' ) } }
@ -513,14 +499,26 @@ watch(
< div class = "md:(absolute bottom-0 left-0 right-0 px-4 pb-4) lg:px-10 lg:pb-10" >
< div class = "md:(absolute bottom-0 left-0 right-0 px-4 pb-4) lg:px-10 lg:pb-10" >
< div class = "flex justify-end items-center gap-4" >
< div class = "flex justify-end items-center gap-4" >
< div class = "flex justify-center" >
< div class = "flex justify-center" >
< GeneralFormBranding class = "inline-flex mx-auto" / >
< GeneralFormBranding
class = "inline-flex mx-auto"
: style = " {
color : tinycolor . isReadable ( parseProp ( sharedFormView ? . meta ) ? . background _color || '#F9F9FA' , '#D5D5D9' , {
level : 'AA' ,
size : 'large' ,
} )
? '#fff'
: tinycolor
. mostReadable ( parseProp ( sharedFormView ? . meta ) ? . background _color || '#F9F9FA' , [ '#374151' , '#D5D5D9' ] )
. toHex8String ( ) ,
} "
/ >
< / div >
< / div >
< div v-if ="isStarted && !submitted" class="flex items-center gap-3" >
< div v-if ="isStarted && !submitted" class="flex items-center gap-3" >
< NcButton
< NcButton
type = "secondary"
type = "secondary"
: size = "isMobileMode ? 'medium' : 'small'"
: size = "isMobileMode ? 'medium' : 'small'"
data - testid = "nc-survey-form__icon-prev"
data - testid = "nc-survey-form__icon-prev"
: disabled = "isFirst || v$.localState[field.title]?.$error"
: disabled = "isFirst"
@ click = "goPrevious()"
@ click = "goPrevious()"
>
>
< GeneralIcon icon = "ncArrowLeft"
< GeneralIcon icon = "ncArrowLeft"
@ -530,7 +528,7 @@ watch(
: size = "isMobileMode ? 'medium' : 'small'"
: size = "isMobileMode ? 'medium' : 'small'"
type = "secondary"
type = "secondary"
data - testid = "nc-survey-form__icon-next"
data - testid = "nc-survey-form__icon-next"
: disabled = "isLast || v$.localState[field.title]?.$error || columnValidation Error"
: disabled = "isLast || fieldHas Error"
@ click = "goNext()"
@ click = "goNext()"
>
>
< GeneralIcon icon = "ncArrowRight" / >
< GeneralIcon icon = "ncArrowRight" / >
@ -552,6 +550,7 @@ watch(
type = "primary"
type = "primary"
: size = "isMobileMode ? 'medium' : 'small'"
: size = "isMobileMode ? 'medium' : 'small'"
data - testid = "nc-survey-form__btn-submit"
data - testid = "nc-survey-form__btn-submit"
class = "nc-survey-form-btn-submit"
@ click = "submit"
@ click = "submit"
>
>
{ { $t ( 'general.submit' ) } }
{ { $t ( 'general.submit' ) } }