Browse Source

Nc fix: Form view bug fixes (#7899)

* fix(nc-gui): show inline form field validation errors

* fix(nc-gui): display inline validation error in shared form and form builder

* fix(nc-gui): shared form default value issue

* fix(nc-gui): limit option spell mistake

* fix(nc-gui): form title update issue when toggle between grid & form view

* fix(nc-gui): form banner & logo display issue on upload

* chore(nc-gui): lint

* fix(nc-gui): show error message on press non numeric keys in numeric field

* fix(nc-gui): add key for form banner and logo

* fix(nc-gui): show currency suffix only in form

* fix(nc-gui): edit column default value input height issue

* fix(nc-gui): form checkbox field enter keypress should navigate to next question in survey form

* fix(nc-gui): escape should blur focus field in survey form

* fix(nc-gui): add currency code suffix in form view currency field

* chore(nc-gui): lint

* fix(nc-gui): add percent suffix in form view percent field

* fix(nc-gui): survey form pw test fail issue

* fix(nc-gui): filter pw test fail issue

* fix(nc-gui): add missing classname in oss

* fix(nc-gui): survey form ui break issue

* fix(nc-gui): update oss survey form file

* fix(nc-gui): in survey form branding text color should be dynamic based on form bg color

* chore(nc-gui): lint

* fix(nc-gui): ai pr review changes

* fix(nc-gui): pr review changes #2555

* fix(nc-gui): use handler instead on ternery condition
pull/7925/head
Ramesh Mane 8 months ago committed by GitHub
parent
commit
34cc8197d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 19
      packages/nc-gui/components/cell/Checkbox.vue
  2. 11
      packages/nc-gui/components/cell/Currency.vue
  3. 21
      packages/nc-gui/components/cell/Email.vue
  4. 15
      packages/nc-gui/components/cell/Integer.vue
  5. 5
      packages/nc-gui/components/cell/MultiSelect.vue
  6. 38
      packages/nc-gui/components/cell/Percent.vue
  7. 22
      packages/nc-gui/components/cell/SingleSelect.vue
  8. 21
      packages/nc-gui/components/cell/Url.vue
  9. 17
      packages/nc-gui/components/cell/attachment/index.vue
  10. 10
      packages/nc-gui/components/general/FormBanner.vue
  11. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  12. 75
      packages/nc-gui/components/smartsheet/Form.vue
  13. 39
      packages/nc-gui/composables/useSharedFormViewStore.ts
  14. 2
      packages/nc-gui/lang/en.json
  15. 19
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  16. 10
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  17. 155
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  18. 2
      packages/nc-gui/utils/dataUtils.ts
  19. 2
      tests/playwright/pages/Dashboard/Form/index.ts
  20. 5
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  21. 1
      tests/playwright/pages/SharedForm/index.ts

19
packages/nc-gui/components/cell/Checkbox.vue

@ -63,7 +63,7 @@ const vModel = computed<boolean | number>({
set: (val: any) => emits('update:modelValue', isMssql(column?.value?.source_id) ? +val : val), set: (val: any) => emits('update:modelValue', isMssql(column?.value?.source_id) ? +val : val),
}) })
function onClick(force?: boolean, event?: MouseEvent) { function onClick(force?: boolean, event?: MouseEvent | KeyboardEvent) {
if ( if (
(event?.target as HTMLElement)?.classList?.contains('nc-checkbox') || (event?.target as HTMLElement)?.classList?.contains('nc-checkbox') ||
(event?.target as HTMLElement)?.closest('.nc-checkbox') (event?.target as HTMLElement)?.closest('.nc-checkbox')
@ -75,6 +75,19 @@ function onClick(force?: boolean, event?: MouseEvent) {
} }
} }
const keydownEnter = (e: KeyboardEvent) => {
if (!isSurveyForm.value) {
onClick(true, e)
e.stopPropagation()
}
}
const keydownSpace = (e: KeyboardEvent) => {
if (isSurveyForm.value) {
onClick(true, e)
e.stopPropagation()
}
}
useSelectedCellKeyupListener(active, (e) => { useSelectedCellKeyupListener(active, (e) => {
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
@ -101,8 +114,8 @@ useSelectedCellKeyupListener(active, (e) => {
}" }"
:tabindex="readOnly ? -1 : 0" :tabindex="readOnly ? -1 : 0"
@click="onClick(false, $event)" @click="onClick(false, $event)"
@keydown.enter.stop="!isSurveyForm ? onClick(true, $event) : undefined" @keydown.enter="keydownEnter"
@keydown.space.stop="isSurveyForm ? onClick(true, $event) : undefined" @keydown.space="keydownSpace($event)"
> >
<div <div
class="flex items-center" class="flex items-center"

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

@ -103,12 +103,21 @@ onMounted(() => {
</script> </script>
<template> <template>
<div
v-if="isForm && !isEditColumn"
class="nc-currency-code h-full !bg-gray-100 border-r border-gray-200 px-3 mr-1 flex items-center"
>
<span>
{{ currencyMeta.currency_code }}
</span>
</div>
<input <input
v-if="!readOnly && editEnabled" v-if="!readOnly && editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
type="number" type="number"
class="nc-cell-field w-full h-full text-sm border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0" class="nc-cell-field h-full text-sm border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur" @blur="onBlur"
@keydown.enter="onKeydownEnter" @keydown.enter="onKeydownEnter"

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

@ -5,7 +5,6 @@ import {
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsFormInj, IsFormInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
inject, inject,
@ -31,12 +30,14 @@ const editEnabled = inject(EditModeInj)!
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
// Used in the logic of when to display error since we are not storing the email if it's not valid // Used in the logic of when to display error since we are not storing the email if it's not valid
const localState = ref(value) const localState = ref(value)
@ -44,7 +45,7 @@ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
localState.value = val localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && validateEmail(val)) || !val || isSurveyForm.value) { if (!parseProp(column.value.meta)?.validate || (val && validateEmail(val)) || !val || isForm.value) {
emit('update:modelValue', val) emit('update:modelValue', val)
} }
}, },
@ -52,17 +53,19 @@ const vModel = computed({
const validEmail = computed(() => vModel.value && validateEmail(vModel.value)) const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch( watch(
() => editEnabled.value, () => editEnabled.value,
() => { () => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !validateEmail(localState.value)) { if (
!isForm.value &&
parseProp(column.value.meta)?.validate &&
!editEnabled.value &&
localState.value &&
!validateEmail(localState.value)
) {
message.error(t('msg.error.invalidEmail')) message.error(t('msg.error.invalidEmail'))
localState.value = undefined localState.value = undefined
return return

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

@ -25,6 +25,10 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const _vModel = useVModel(props, 'modelValue', emits) const _vModel = useVModel(props, 'modelValue', emits)
const displayValue = computed(() => { const displayValue = computed(() => {
@ -42,15 +46,15 @@ const vModel = computed({
// if we clear / empty a cell in sqlite, // if we clear / empty a cell in sqlite,
// the value is considered as '' // the value is considered as ''
_vModel.value = null _vModel.value = null
} else if (isForm.value && !isEditColumn.value) {
_vModel.value = isNaN(Number(value)) ? value : Number(value)
} else { } else {
_vModel.value = value _vModel.value = value
} }
}, },
}) })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const inputType = computed(() => (isForm.value && !isEditColumn.value ? 'text' : 'number'))
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
@ -91,7 +95,7 @@ function onKeyDown(e: any) {
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="nc-cell-field outline-none py-1 border-none w-full h-full text-sm" class="nc-cell-field outline-none py-1 border-none w-full h-full text-sm"
type="number" :type="inputType"
style="letter-spacing: 0.06rem" style="letter-spacing: 0.06rem"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false" @blur="editEnabled = false"
@ -109,7 +113,8 @@ function onKeyDown(e: any) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
input[type='number']:focus { input[type='number']:focus,
input[type='text']:focus {
@apply ring-transparent; @apply ring-transparent;
} }

5
packages/nc-gui/components/cell/MultiSelect.vue

@ -371,6 +371,9 @@ const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') { if (e.key === 'Tab') {
isOpen.value = false isOpen.value = false
return return
} else if (e.key === 'Escape' && isForm.value) {
isOpen.value = false
return
} }
e.stopPropagation() e.stopPropagation()
@ -394,7 +397,7 @@ const onFocus = () => {
@click="toggleMenu" @click="toggleMenu"
> >
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full"> <div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full">
<a-checkbox-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list"> <a-checkbox-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list" @click.stop>
<a-checkbox <a-checkbox
v-for="op of options" v-for="op of options"
:key="op.title" :key="op.title"

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

@ -24,17 +24,6 @@ const _vModel = useVModel(props, 'modelValue', emits)
const wrapperRef = ref<HTMLElement>() const wrapperRef = ref<HTMLElement>()
const vModel = computed({
get: () => _vModel.value,
set: (value) => {
if (value === '') {
_vModel.value = null
} else {
_vModel.value = value
}
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)! const isForm = inject(IsFormInj)!
@ -46,6 +35,23 @@ const cellFocused = ref(false)
const expandedEditEnabled = ref(false) const expandedEditEnabled = ref(false)
const vModel = computed({
get: () => {
return isForm.value && !isEditColumn.value && _vModel.value && !cellFocused.value && !isNaN(Number(_vModel.value))
? `${_vModel.value}%`
: _vModel.value
},
set: (value) => {
if (value === '') {
_vModel.value = null
} else if (isForm.value && !isEditColumn.value) {
_vModel.value = isNaN(Number(value)) ? value : Number(value)
} else {
_vModel.value = value
}
},
})
const percentMeta = computed(() => { const percentMeta = computed(() => {
return { return {
is_progress: false, is_progress: false,
@ -53,6 +59,8 @@ const percentMeta = computed(() => {
} }
}) })
const inputType = computed(() => (isForm.value && !isEditColumn.value ? 'text' : 'number'))
const onBlur = () => { const onBlur = () => {
if (editEnabled) { if (editEnabled) {
editEnabled.value = false editEnabled.value = false
@ -106,7 +114,11 @@ const onTabPress = (e: KeyboardEvent) => {
) )
for (let i = focusesNcCellIndex - 1; i >= 0; i--) { for (let i = focusesNcCellIndex - 1; i >= 0; i--) {
const lastFormItem = nodes[i].querySelector('[tabindex="0"]') as HTMLElement const node = nodes[i]
const lastFormItem = (node.querySelector('[tabindex="0"]') ??
node.querySelector('input') ??
node.querySelector('textarea') ??
node.querySelector('button')) as HTMLElement
if (lastFormItem) { if (lastFormItem) {
lastFormItem.focus() lastFormItem.focus()
break break
@ -132,7 +144,7 @@ const onTabPress = (e: KeyboardEvent) => {
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="nc-cell-field w-full !text-sm !border-none !outline-none focus:ring-0 text-base py-1" class="nc-cell-field w-full !text-sm !border-none !outline-none focus:ring-0 text-base py-1"
type="number" :type="inputType"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"

22
packages/nc-gui/components/cell/SingleSelect.vue

@ -257,12 +257,26 @@ const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
isOpen.value = false isOpen.value = false
if (isForm.value) return
setTimeout(() => { setTimeout(() => {
aselect.value?.$el.querySelector('.ant-select-selection-search > input').focus() aselect.value?.$el.querySelector('.ant-select-selection-search > input').focus()
}, 100) }, 100)
} }
} }
const handleKeyDownList = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft':
// skip
e.stopPropagation()
break
}
}
const onSelect = () => { const onSelect = () => {
isOpen.value = false isOpen.value = false
isEditable.value = false isEditable.value = false
@ -315,7 +329,13 @@ const onFocus = () => {
@keydown.enter.stop.prevent="toggleMenu" @keydown.enter.stop.prevent="toggleMenu"
> >
<div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full"> <div v-if="!isEditColumn && isForm && parseProp(column.meta)?.isList" class="w-full max-w-full">
<a-radio-group v-model:value="vModel" :disabled="readOnly || !editAllowed" class="nc-field-layout-list"> <a-radio-group
v-model:value="vModel"
:disabled="readOnly || !editAllowed"
class="nc-field-layout-list"
@keydown="handleKeyDownList"
@click.stop
>
<a-radio <a-radio
v-for="op of options" v-for="op of options"
:key="op.title" :key="op.title"

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

@ -7,7 +7,6 @@ import {
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsFormInj, IsFormInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
inject, inject,
@ -42,10 +41,12 @@ const disableOverlay = inject(CellUrlDisableOverlayInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined)) const rowHeight = inject(RowHeightInj, ref(undefined))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
// Used in the logic of when to display error since we are not storing the url if it's not valid // Used in the logic of when to display error since we are not storing the url if it's not valid
const localState = ref(value) const localState = ref(value)
@ -53,7 +54,7 @@ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
localState.value = val localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isSurveyForm.value) { if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isForm.value) {
emit('update:modelValue', val) emit('update:modelValue', val)
} }
}, },
@ -72,17 +73,19 @@ const url = computed(() => {
const { cellUrlOptions } = useCellUrlConfig(url) const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
watch( watch(
() => editEnabled.value, () => editEnabled.value,
() => { () => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !isValidURL(localState.value)) { if (
!isForm.value &&
parseProp(column.value.meta)?.validate &&
!editEnabled.value &&
localState.value &&
!isValidURL(localState.value)
) {
message.error(t('msg.error.invalidURL')) message.error(t('msg.error.invalidURL'))
localState.value = undefined localState.value = undefined
return return

17
packages/nc-gui/components/cell/attachment/index.vue

@ -178,6 +178,19 @@ const onImageClick = (item: any) => {
selectedImage.value = item selectedImage.value = item
} }
const keydownEnter = (e: KeyboardEvent) => {
if (!isSurveyForm.value) {
open(e)
e.stopPropagation()
}
}
const keydownSpace = (e: KeyboardEvent) => {
if (isSurveyForm.value) {
open(e)
e.stopPropagation()
}
}
</script> </script>
<template> <template>
@ -211,8 +224,8 @@ const onImageClick = (item: any) => {
data-testid="attachment-cell-file-picker-button" data-testid="attachment-cell-file-picker-button"
tabindex="0" tabindex="0"
@click="open" @click="open"
@keydown.enter="!isSurveyForm ? open($event) : undefined" @keydown.enter="keydownEnter"
@keydown.space="isSurveyForm ? open($event) : undefined" @keydown.space="keydownSpace"
> >
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

10
packages/nc-gui/components/general/FormBanner.vue

@ -7,6 +7,10 @@ interface Props {
const { bannerImageUrl } = defineProps<Props>() const { bannerImageUrl } = defineProps<Props>()
const { getPossibleAttachmentSrc } = useAttachment() const { getPossibleAttachmentSrc } = useAttachment()
const getBannerImageSrc = computed(() => {
return getPossibleAttachmentSrc(parseProp(bannerImageUrl))
})
</script> </script>
<template> <template>
@ -15,11 +19,7 @@ const { getPossibleAttachmentSrc } = useAttachment()
:class="!bannerImageUrl ? 'shadow-sm' : ''" :class="!bannerImageUrl ? 'shadow-sm' : ''"
:style="{ aspectRatio: 4 / 1 }" :style="{ aspectRatio: 4 / 1 }"
> >
<LazyCellAttachmentImage <LazyCellAttachmentImage v-if="bannerImageUrl" :srcs="getBannerImageSrc" class="nc-form-banner-image object-cover w-full" />
v-if="bannerImageUrl"
:srcs="getPossibleAttachmentSrc(parseProp(bannerImageUrl))"
class="nc-form-banner-image object-cover w-full"
/>
<div v-else class="h-full flex items-stretch justify-between bg-white"> <div v-else class="h-full flex items-stretch justify-between bg-white">
<div class="flex -mt-1"> <div class="flex -mt-1">
<img src="~assets/img/form-banner-left.png" alt="form-banner-left'" /> <img src="~assets/img/form-banner-left.png" alt="form-banner-left'" />

2
packages/nc-gui/components/smartsheet/Cell.vue

@ -196,7 +196,7 @@ onUnmounted(() => {
{ {
'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar, 'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar,
'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen, 'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen,
'h-10': isForm && !isSurveyForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual, 'h-10': !isEditColumnMenu && isForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu, 'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm), '!min-h-30': isTextArea(column) && (isForm || isSurveyForm),
}, },

75
packages/nc-gui/components/smartsheet/Form.vue

@ -3,6 +3,8 @@ import Draggable from 'vuedraggable'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import { Pane, Splitpanes } from 'splitpanes' import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css' import 'splitpanes/dist/splitpanes.css'
import type { FormItemProps } from 'ant-design-vue'
import { import {
type AttachmentResType, type AttachmentResType,
ProjectRoles, ProjectRoles,
@ -186,6 +188,8 @@ const { open, onChange: onChangeFile } = useFileDialog({
const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => a.order - b.order)) const visibleColumns = computed(() => localColumns.value.filter((f) => f.show).sort((a, b) => a.order - b.order))
const getFormLogoSrc = computed(() => getPossibleAttachmentSrc(parseProp(formViewData.value.logo_url)))
const updateView = useDebounceFn( const updateView = useDebounceFn(
() => { () => {
updateFormView(formViewData.value) updateFormView(formViewData.value)
@ -546,6 +550,50 @@ const handleOnUploadImage = (data: AttachmentResType = null) => {
updateView() updateView()
} }
const validateFormEmail = async (_rule, value) => {
if (!value) {
return Promise.resolve()
} else if (!validateEmail(value)) {
return Promise.reject(t('msg.error.invalidEmail'))
}
}
const validateFormURL = async (_rule, value) => {
if (!value) {
return Promise.resolve()
} else if (!isValidURL(value)) {
return Promise.reject(t('msg.error.invalidURL'))
}
}
const formElementValidationRules = (element) => {
const rules: FormItemProps['rules'][] = [
{
required: isRequired(element, element.required),
message: t('msg.error.fieldRequired', { value: 'This field' }),
},
]
if (parseProp(element.meta).validate && element.uidt === UITypes.URL) {
rules.push({
validator: validateFormURL,
})
} else if (parseProp(element.meta).validate && element.uidt === UITypes.Email) {
rules.push({
validator: validateFormEmail,
})
}
if ([UITypes.Number, UITypes.Currency, UITypes.Percent].includes(element.uidt)) {
rules.push({
type: 'number',
message: t('msg.plsEnterANumber'),
})
}
return rules
}
onClickOutside(draggableRef, (e) => { onClickOutside(draggableRef, (e) => {
if ( if (
(e.target as HTMLElement)?.closest( (e.target as HTMLElement)?.closest(
@ -736,7 +784,7 @@ useEventListener(
<div class="flex justify-center"> <div class="flex justify-center">
<div class="w-full"> <div class="w-full">
<a-alert class="!my-4 !py-4 text-left !rounded-lg" type="success" outlined> <a-alert class="nc-form-success-msg !my-4 !py-4 text-left !rounded-lg" type="success" outlined>
<template #message> <template #message>
<LazyCellRichText <LazyCellRichText
v-if="formViewData?.success_msg?.trim()" v-if="formViewData?.success_msg?.trim()"
@ -806,7 +854,7 @@ useEventListener(
></GeneralImageCropper> ></GeneralImageCropper>
<!-- cover image --> <!-- cover image -->
<div v-if="!parseProp(formViewData?.meta).hide_banner" class="group relative max-w-[max(33%,688px)] mx-auto"> <div v-if="!parseProp(formViewData?.meta).hide_banner" class="group relative max-w-[max(33%,688px)] mx-auto">
<GeneralFormBanner :banner-image-url="formViewData.banner_image_url" /> <GeneralFormBanner :key="formViewData.banner_image_url?.path" :banner-image-url="formViewData.banner_image_url" />
<div class="absolute bottom-0 right-0 hidden group-hover:block"> <div class="absolute bottom-0 right-0 hidden group-hover:block">
<div class="flex items-center space-x-1 m-2"> <div class="flex items-center space-x-1 m-2">
<NcTooltip :disabled="isEeUI"> <NcTooltip :disabled="isEeUI">
@ -880,7 +928,8 @@ useEventListener(
> >
<LazyCellAttachmentImage <LazyCellAttachmentImage
v-if="formViewData.logo_url" v-if="formViewData.logo_url"
:srcs="getPossibleAttachmentSrc(parseProp(formViewData.logo_url))" :key="formViewData.logo_url?.path"
:srcs="getFormLogoSrc"
class="flex-none nc-form-logo !object-contain object-left max-h-full max-w-full !m-0" class="flex-none nc-form-logo !object-contain object-left max-h-full max-w-full !m-0"
/> />
<div <div
@ -973,8 +1022,7 @@ useEventListener(
:bordered="false" :bordered="false"
:data-testid="NcForm.heading" :data-testid="NcForm.heading"
:data-title="NcForm.heading" :data-title="NcForm.heading"
@blur="updateView" @update:value="updateView"
@keydown.enter="updateView"
/> />
</a-form-item> </a-form-item>
@ -1199,12 +1247,7 @@ useEventListener(
<a-form-item <a-form-item
:name="element.title" :name="element.title"
class="!my-0 nc-input-required-error nc-form-input-item" class="!my-0 nc-input-required-error nc-form-input-item"
:rules="[ :rules="formElementValidationRules(element)"
{
required: isRequired(element, element.required),
message: `${$t('msg.error.fieldRequired', { value: 'This field' })}`,
},
]"
> >
<LazySmartsheetDivDataCell <LazySmartsheetDivDataCell
class="relative" class="relative"
@ -1725,6 +1768,9 @@ useEventListener(
&.nc-cell-geodata { &.nc-cell-geodata {
@apply !py-1; @apply !py-1;
} }
&.nc-cell-currency {
@apply !py-0 !pl-0 flex items-stretch;
}
:deep(input) { :deep(input) {
@apply !px-1; @apply !px-1;
@ -1732,8 +1778,11 @@ useEventListener(
&.nc-cell-longtext { &.nc-cell-longtext {
@apply p-0 h-auto; @apply p-0 h-auto;
} }
&:not(.nc-cell-longtext) { &.nc-cell:not(.nc-cell-longtext) {
@apply px-2 py-2; @apply p-2;
}
&.nc-virtual-cell {
@apply px-2 py-1;
} }
&.nc-cell-json { &.nc-cell-json {

39
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -22,6 +22,7 @@ import {
createEventHook, createEventHook,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isNumericFieldType, isNumericFieldType,
isValidURL,
message, message,
parseProp, parseProp,
provide, provide,
@ -34,6 +35,7 @@ import {
useMetas, useMetas,
useProvideSmartsheetRowStore, useProvideSmartsheetRowStore,
useViewsStore, useViewsStore,
validateEmail,
watch, watch,
} from '#imports' } from '#imports'
import type { SharedViewMeta } from '#imports' import type { SharedViewMeta } from '#imports'
@ -138,7 +140,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
c?.cdf && c?.cdf &&
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf) !/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf)
) { ) {
formState.value[c.title] = c.cdf formState.value[c.title] = typeof c.cdf === 'string' ? c.cdf.replace(/^'|'$/g, '') : c.cdf
} }
return { return {
@ -197,7 +199,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
!isVirtualCol(column) && !isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required) ((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)
) { ) {
obj.localState[column.title!] = { required: fieldRequired() } obj.localState[column.title!] = {
required: fieldRequired(),
}
} else if ( } else if (
isLinksOrLTAR(column) && isLinksOrLTAR(column) &&
column.colOptions && column.colOptions &&
@ -216,6 +220,37 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
required: fieldRequired(), required: fieldRequired(),
} }
} }
if (
!isVirtualCol(column) &&
parseProp(column.meta)?.validate &&
[UITypes.URL, UITypes.Email].includes(column.uidt as UITypes)
) {
if (column.uidt === UITypes.URL) {
obj.localState[column.title!] = {
...(obj.localState[column.title!] || {}),
validateFormURL: helpers.withMessage(t('msg.error.invalidURL'), (value) => {
return value ? isValidURL(value) : true
}),
}
} else if (column.uidt === UITypes.Email) {
obj.localState[column.title!] = {
...(obj.localState[column.title!] || {}),
validateFormEmail: helpers.withMessage(t('msg.error.invalidEmail'), (value) => {
return value ? validateEmail(value) : true
}),
}
}
}
if ([UITypes.Number, UITypes.Currency, UITypes.Percent].includes(column.uidt as UITypes)) {
obj.localState[column.title!] = {
...(obj.localState[column.title!] || {}),
validateFormNumber: helpers.withMessage(t('msg.plsEnterANumber'), (value) => {
return value ? (column.uidt === UITypes.Number ? /^\d+$/.test(value) : /^\d*\.?\d+$/.test(value)) : true
}),
}
}
} }
return obj return obj

2
packages/nc-gui/lang/en.json

@ -699,7 +699,7 @@
"hideNocodbBranding": "Hide NocoDB Branding", "hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on condtions", "showOnConditions": "Show on condtions",
"showFieldOnConditionsMet": "Shows field only when conditions are met", "showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit ptions", "limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options", "limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection" "clearSelection": "Clear selection"
}, },

19
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -28,7 +28,7 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<template> <template>
<div <div
class="scrollbar-thin-dull h-[100vh] overflow-y-auto overflow-x-hidden color-transition p-4 lg:p-10 nc-form-view relative min-h-[600px]" class="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-200 hover-scrollbar-thumb-gray-300 h-[100vh] overflow-y-auto overflow-x-hidden flex flex-col color-transition p-4 lg:p-10 nc-form-view min-h-[600px]"
:class="{ :class="{
'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode, 'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode,
}" }"
@ -62,7 +62,8 @@ p {
} }
} }
.nc-cell { .nc-cell,
.nc-virtual-cell {
@apply bg-white dark:bg-slate-500 appearance-none; @apply bg-white dark:bg-slate-500 appearance-none;
&.nc-cell-checkbox { &.nc-cell-checkbox {
@ -85,7 +86,7 @@ p {
@apply bg-white dark:bg-slate-500; @apply bg-white dark:bg-slate-500;
&.nc-input { &.nc-input {
@apply w-full; @apply w-full h-10;
&:not(.layout-list) { &:not(.layout-list) {
@apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden; @apply rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500 overflow-hidden;
@ -162,9 +163,12 @@ p {
@apply px-3; @apply px-3;
} }
} }
&:not(.nc-cell-longtext) { &.nc-cell:not(.nc-cell-longtext) {
@apply p-2; @apply p-2;
} }
&.nc-virtual-cell {
@apply px-2 py-1;
}
&.nc-cell-json { &.nc-cell-json {
@apply h-auto; @apply h-auto;
@ -177,6 +181,13 @@ p {
input.nc-cell-field { input.nc-cell-field {
@apply !py-0 !px-1; @apply !py-0 !px-1;
} }
&.nc-cell-currency {
@apply !py-0 !pl-0 flex items-stretch;
.nc-currency-code {
@apply !bg-gray-100;
}
}
} }
} }

10
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue

@ -113,7 +113,7 @@ const onDecode = async (scannedCodeValue: string) => {
<template v-else-if="submitted"> <template v-else-if="submitted">
<div class="flex justify-center"> <div class="flex justify-center">
<div v-if="sharedFormView" class="w-full"> <div v-if="sharedFormView" class="w-full">
<a-alert class="!mt-2 !mb-4 !py-4 text-left !rounded-lg" type="success" outlined> <a-alert class="nc-shared-form-success-msg !mt-2 !mb-4 !py-4 text-left !rounded-lg" type="success" outlined>
<template #message> <template #message>
<LazyCellRichText <LazyCellRichText
v-if="sharedFormView?.success_msg?.trim()" v-if="sharedFormView?.success_msg?.trim()"
@ -140,7 +140,6 @@ const onDecode = async (scannedCodeValue: string) => {
v-if="sharedFormView?.submit_another_form" v-if="sharedFormView?.submit_another_form"
type="secondary" type="secondary"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit-another-form"
@click="submitted = false" @click="submitted = false"
> >
{{ $t('activity.submitAnotherForm') }} {{ $t('activity.submitAnotherForm') }}
@ -218,6 +217,11 @@ const onDecode = async (scannedCodeValue: string) => {
: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="
() => {
v$.localState[field.title]?.$validate()
}
"
/> />
<a-button <a-button
v-if="field.enable_scanner" v-if="field.enable_scanner"
@ -232,7 +236,7 @@ const onDecode = async (scannedCodeValue: string) => {
</LazySmartsheetDivDataCell> </LazySmartsheetDivDataCell>
</NcTooltip> </NcTooltip>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-sm mt-2"> <div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-xs mt-2">
<template v-if="isVirtualCol(field)"> <template v-if="isVirtualCol(field)">
<div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500"> <div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500">
{{ error.$message }} {{ error.$message }}

155
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue

@ -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 HTMLInputElement) (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(() => {
// NcButton will only focus if document has already focused element
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 || columnValidationError" :disabled="fieldHasError"
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 || columnValidationError ? 'text-gray-200' : 'text-gray-800'" :class="fieldHasError ? '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 || columnValidationError" :disabled="fieldHasError"
@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 || columnValidationError" :disabled="isLast || fieldHasError"
@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') }}

2
packages/nc-gui/utils/dataUtils.ts

@ -107,7 +107,7 @@ export const rowDefaultData = (columns: ColumnType[] = []) => {
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(col.cdf) !/^\w+\(\)|CURRENT_TIMESTAMP$/.test(col.cdf)
) { ) {
const defaultValue = col.cdf const defaultValue = col.cdf
acc[col.title!] = typeof defaultValue === 'string' ? defaultValue.replace(/^'/, '').replace(/'$/, '') : defaultValue acc[col.title!] = typeof defaultValue === 'string' ? defaultValue.replace(/^'|'$/g, '') : defaultValue
} }
return acc return acc
}, {} as Record<string, any>) }, {} as Record<string, any>)

2
tests/playwright/pages/Dashboard/Form/index.ts

@ -250,6 +250,8 @@ export class FormPage extends BasePage {
} }
async verifyStatePostSubmit(param: { message?: string; submitAnotherForm?: boolean; showBlankForm?: boolean }) { async verifyStatePostSubmit(param: { message?: string; submitAnotherForm?: boolean; showBlankForm?: boolean }) {
await this.rootPage.locator('.nc-form-success-msg').waitFor({ state: 'visible' });
if (undefined !== param.message) { if (undefined !== param.message) {
await expect(this.getFormAfterSubmit()).toContainText(param.message); await expect(this.getFormAfterSubmit()).toContainText(param.message);
} }

5
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -338,7 +338,10 @@ export class ToolbarFilterPage extends BasePage {
break; break;
default: default:
fillFilter = () => this.rootPage.locator('.nc-filter-value-select > input').last().fill(value); fillFilter = async () => {
await this.rootPage.locator('.nc-filter-value-select > input').last().clear({ force: true });
return this.rootPage.locator('.nc-filter-value-select > input').last().fill(value);
};
await this.waitForResponse({ await this.waitForResponse({
uiAction: fillFilter, uiAction: fillFilter,
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],

1
tests/playwright/pages/SharedForm/index.ts

@ -25,6 +25,7 @@ export class SharedFormPage extends BasePage {
} }
async verifySuccessMessage() { async verifySuccessMessage() {
await this.rootPage.locator('.nc-shared-form-success-msg').waitFor({ state: 'visible' });
await expect( await expect(
this.get().locator('.ant-alert-success', { this.get().locator('.ant-alert-success', {
hasText: 'Successfully submitted form data', hasText: 'Successfully submitted form data',

Loading…
Cancel
Save