Browse Source

Merge branch 'develop' into enhancement/expanded-form-comments

pull/5341/head
Wing-Kam Wong 2 years ago
parent
commit
a6baf99725
  1. 15
      packages/nc-gui/app.vue
  2. 11
      packages/nc-gui/assets/css/global.css
  3. 20
      packages/nc-gui/components/account/UsersModal.vue
  4. 43
      packages/nc-gui/components/cell/Email.vue
  5. 1
      packages/nc-gui/components/cell/TimePicker.vue
  6. 7
      packages/nc-gui/components/cell/Url.vue
  7. 13
      packages/nc-gui/components/smartsheet/Cell.vue
  8. 9
      packages/nc-gui/components/smartsheet/Form.vue
  9. 20
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  10. 1
      packages/nc-gui/context/index.ts
  11. 1
      packages/nc-gui/lang/ar.json
  12. 1
      packages/nc-gui/lang/bn_IN.json
  13. 1
      packages/nc-gui/lang/cs.json
  14. 1
      packages/nc-gui/lang/da.json
  15. 1
      packages/nc-gui/lang/de.json
  16. 1
      packages/nc-gui/lang/en.json
  17. 1
      packages/nc-gui/lang/es.json
  18. 1
      packages/nc-gui/lang/eu.json
  19. 1
      packages/nc-gui/lang/fa.json
  20. 1
      packages/nc-gui/lang/fi.json
  21. 1
      packages/nc-gui/lang/fr.json
  22. 1
      packages/nc-gui/lang/he.json
  23. 1
      packages/nc-gui/lang/hi.json
  24. 1
      packages/nc-gui/lang/hr.json
  25. 1
      packages/nc-gui/lang/id.json
  26. 1
      packages/nc-gui/lang/it.json
  27. 1
      packages/nc-gui/lang/ja.json
  28. 1
      packages/nc-gui/lang/ko.json
  29. 1
      packages/nc-gui/lang/lv.json
  30. 1
      packages/nc-gui/lang/nl.json
  31. 1
      packages/nc-gui/lang/no.json
  32. 1
      packages/nc-gui/lang/pl.json
  33. 1
      packages/nc-gui/lang/pt.json
  34. 1
      packages/nc-gui/lang/pt_BR.json
  35. 1
      packages/nc-gui/lang/ru.json
  36. 1
      packages/nc-gui/lang/sk.json
  37. 1
      packages/nc-gui/lang/sl.json
  38. 1
      packages/nc-gui/lang/sv.json
  39. 1
      packages/nc-gui/lang/th.json
  40. 1
      packages/nc-gui/lang/tr.json
  41. 1
      packages/nc-gui/lang/uk.json
  42. 1
      packages/nc-gui/lang/vi.json
  43. 1
      packages/nc-gui/lang/zh-Hans.json
  44. 1
      packages/nc-gui/lang/zh-Hant.json
  45. 1
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  46. 7
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  47. 55
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue
  48. 17
      packages/nc-gui/utils/validation.ts
  49. 4
      packages/nc-gui/utils/viewUtils.ts
  50. 9
      tests/playwright/pages/Dashboard/Form/index.ts
  51. 1
      tests/playwright/pages/Dashboard/SurveyForm/index.ts

15
packages/nc-gui/app.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { applyNonSelectable, computed, useRoute, useTheme } from '#imports' import { computed, useRoute, useTheme } from '#imports'
const route = useRoute() const route = useRoute()
@ -7,7 +7,18 @@ const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || ro
useTheme() useTheme()
applyNonSelectable() useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (cmdOrCtrl) {
switch (e.key.toLowerCase()) {
case 'a':
// prevent Ctrl + A selection for non-editable nodes
if (!['input', 'textarea'].includes((e.target as any).nodeName.toLowerCase())) {
e.preventDefault()
}
}
}
})
// TODO: Remove when https://github.com/vuejs/core/issues/5513 fixed // TODO: Remove when https://github.com/vuejs/core/issues/5513 fixed
const key = ref(0) const key = ref(0)

11
packages/nc-gui/assets/css/global.css

@ -31,14 +31,3 @@ For Drag and Drop
.grabbing * { .grabbing * {
cursor: grabbing; cursor: grabbing;
} }
/*
Prevent Ctrl + A selection
*/
.non-selectable {
-webkit-user-select: none;
-webkit-touch-callout: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

20
packages/nc-gui/components/account/UsersModal.vue

@ -4,6 +4,7 @@ import type { OrgUserReqType } from 'nocodb-sdk'
import { import {
Form, Form,
computed, computed,
emailValidator,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
ref, ref,
@ -11,7 +12,6 @@ import {
useDashboard, useDashboard,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
validateEmail,
} from '#imports' } from '#imports'
import type { User } from '~/lib' import type { User } from '~/lib'
import { Role } from '~/lib' import { Role } from '~/lib'
@ -44,24 +44,10 @@ const usersData = $ref<Users>({ emails: '', role: Role.OrgLevelViewer, invitatio
const formRef = ref() const formRef = ref()
const useForm = Form.useForm const useForm = Form.useForm
const validators = computed(() => { const validators = computed(() => {
return { return {
emails: [ emails: [emailValidator],
{
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => {
if (!value || value.length === 0) {
callback('Email is required')
return
}
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !validateEmail(e))
if (invalidEmails.length > 0) {
callback(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `)
} else {
callback()
}
},
},
],
} }
}) })

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

@ -1,28 +1,53 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, computed, inject, useVModel, validateEmail } from '#imports' import { EditModeInj, IsSurveyFormInj, computed, inject, useI18n, validateEmail } from '#imports'
interface Props { interface Props {
modelValue: string | null | undefined modelValue: string | null | undefined
} }
interface Emits { const { modelValue: value } = defineProps<Props>()
(event: 'update:modelValue', model: string): void
}
const props = defineProps<Props>() const emit = defineEmits(['update:modelValue'])
const emits = defineEmits<Emits>() const { t } = useI18n()
const { showNull } = useGlobal() const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)!
const column = inject(ColumnInj)!
const vModel = useVModel(props, 'modelValue', emits) // 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 isSurveyForm = inject(IsSurveyFormInj, ref(false))
const vModel = computed({
get: () => value,
set: (val) => {
localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && validateEmail(val)) || !val || isSurveyForm.value) {
emit('update:modelValue', val)
}
},
})
const validEmail = computed(() => vModel.value && validateEmail(vModel.value)) const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
() => {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !validateEmail(localState.value)) {
message.error(t('msg.error.invalidEmail'))
localState.value = undefined
return
}
localState.value = value
},
)
</script> </script>
<template> <template>
@ -30,7 +55,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none text-sm px-2" class="w-full outline-none text-sm px-2"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown.down.stop @keydown.down.stop
@keydown.left.stop @keydown.left.stop

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

@ -95,7 +95,6 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
<template> <template>
<a-time-picker <a-time-picker
v-model:value="localState" v-model:value="localState"
autofocus
:show-time="true" :show-time="true"
:bordered="false" :bordered="false"
use12-hours use12-hours

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

@ -4,6 +4,7 @@ import {
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
ColumnInj, ColumnInj,
EditModeInj, EditModeInj,
IsSurveyFormInj,
computed, computed,
inject, inject,
isValidURL, isValidURL,
@ -36,11 +37,13 @@ const disableOverlay = inject(CellUrlDisableOverlayInj, ref(false))
// 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)
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const vModel = computed({ 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) { if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isSurveyForm.value) {
emit('update:modelValue', val) emit('update:modelValue', val)
} }
}, },
@ -119,7 +122,7 @@ watch(
<div v-if="column.meta?.validate && !isValid && value?.length && !editEnabled" class="mr-1 w-1/10"> <div v-if="column.meta?.validate && !isValid && value?.length && !editEnabled" class="mr-1 w-1/10">
<a-tooltip placement="top"> <a-tooltip placement="top">
<template #title> Invalid URL </template> <template #title> {{ t('msg.error.invalidURL') }} </template>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<MiCircleWarning class="text-red-400 h-4" /> <MiCircleWarning class="text-red-400 h-4" />
</div> </div>

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

@ -8,6 +8,7 @@ import {
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
IsSurveyFormInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
inject, inject,
@ -66,7 +67,7 @@ const column = toRef(props, 'column')
const active = toRef(props, 'active', false) const active = toRef(props, 'active', false)
const readOnly = toRef(props, 'readOnly', undefined) const readOnly = toRef(props, 'readOnly', false)
provide(ColumnInj, column) provide(ColumnInj, column)
@ -84,6 +85,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { currentRow } = useSmartsheetRowStoreOrThrow() const { currentRow } = useSmartsheetRowStoreOrThrow()
const { sqlUis } = storeToRefs(useProject()) const { sqlUis } = storeToRefs(useProject())
@ -118,11 +121,10 @@ const vModel = computed({
}, },
}) })
const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => { const navigate = (dir: NavigateDir, e: KeyboardEvent) => {
if (isJSON(column.value)) return if (isJSON(column.value)) return
if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) { if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) {
emit('save')
currentRow.value.rowMeta.changed = false currentRow.value.rowMeta.changed = false
} }
emit('navigate', dir) emit('navigate', dir)
@ -158,9 +160,10 @@ const onContextmenu = (e: MouseEvent) => {
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`, `nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{ 'text-blue-600': isPrimary(column) && !props.virtual && !isForm }, { 'text-blue-600': isPrimary(column) && !props.virtual && !isForm },
{ 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField }, { 'nc-grid-numeric-cell': isGrid && !isForm && isNumericField },
{ 'h-[40px]': !props.editEnabled && isForm && !isSurveyForm },
]" ]"
@keydown.enter.exact="syncAndNavigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="navigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="syncAndNavigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"
@contextmenu="onContextmenu" @contextmenu="onContextmenu"
> >
<template v-if="column"> <template v-if="column">

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

@ -97,6 +97,8 @@ const submitted = ref(false)
const activeRow = ref('') const activeRow = ref('')
const editEnabled = ref<boolean[]>([])
const { t } = useI18n() const { t } = useI18n()
const { betaFeatureToggleState } = useBetaFeatureToggle() const { betaFeatureToggleState } = useBetaFeatureToggle()
@ -283,6 +285,8 @@ function setFormData() {
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
.map((c) => ({ ...c, required: !!c.required })) .map((c) => ({ ...c, required: !!c.required }))
editEnabled.value = new Array(localColumns.value.length).fill(false)
systemFieldsIds.value = getSystemColumns(col).map((c) => c.fk_column_id) systemFieldsIds.value = getSystemColumns(col).map((c) => c.fk_column_id)
hiddenColumns.value = col.filter( hiddenColumns.value = col.filter(
@ -727,7 +731,10 @@ watch(view, (nextView) => {
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`" :class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`" :data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element" :column="element"
:edit-enabled="true" :edit-enabled="editEnabled[index]"
@click="editEnabled[index] = true"
@cancel="editEnabled[index] = false"
@update:edit-enabled="editEnabled[index] = $event"
@click.stop.prevent @click.stop.prevent
/> />
</a-form-item> </a-form-item>

20
packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue

@ -4,6 +4,7 @@ import type { ProjectUserReqType } from 'nocodb-sdk'
import { import {
Form, Form,
computed, computed,
emailValidator,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
onMounted, onMounted,
@ -17,7 +18,6 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
validateEmail,
} from '#imports' } from '#imports'
import type { User } from '~/lib' import type { User } from '~/lib'
import { ProjectRole } from '~/lib' import { ProjectRole } from '~/lib'
@ -54,24 +54,10 @@ let usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invit
const formRef = ref() const formRef = ref()
const useForm = Form.useForm const useForm = Form.useForm
const validators = computed(() => { const validators = computed(() => {
return { return {
emails: [ emails: [emailValidator],
{
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => {
if (!value || value.length === 0) {
callback('Email is required')
return
}
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !validateEmail(e))
if (invalidEmails.length > 0) {
callback(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `)
} else {
callback()
}
},
},
],
} }
}) })

1
packages/nc-gui/context/index.ts

@ -14,6 +14,7 @@ export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['pag
Symbol('pagination-data-injection') Symbol('pagination-data-injection')
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection') export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection')
export const IsFormInj: InjectionKey<Ref<boolean>> = Symbol('is-form-injection') export const IsFormInj: InjectionKey<Ref<boolean>> = Symbol('is-form-injection')
export const IsSurveyFormInj: InjectionKey<Ref<boolean>> = Symbol('is-survey-form-injection')
export const IsGridInj: InjectionKey<Ref<boolean>> = Symbol('is-grid-injection') export const IsGridInj: InjectionKey<Ref<boolean>> = Symbol('is-grid-injection')
export const IsGalleryInj: InjectionKey<Ref<boolean>> = Symbol('is-gallery-injection') export const IsGalleryInj: InjectionKey<Ref<boolean>> = Symbol('is-gallery-injection')
export const IsKanbanInj: InjectionKey<Ref<boolean>> = Symbol('is-kanban-injection') export const IsKanbanInj: InjectionKey<Ref<boolean>> = Symbol('is-kanban-injection')

1
packages/nc-gui/lang/ar.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "قائمة الأحرف الخاصة المسموح بها" "allowedSpecialCharList": "قائمة الأحرف الخاصة المسموح بها"
}, },
"invalidURL": "رابط غير صالح", "invalidURL": "رابط غير صالح",
"invalidEmail": "Invalid Email",
"internalError": "حدث خطأ داخلي", "internalError": "حدث خطأ داخلي",
"templateGeneratorNotFound": "لا يمكن العثور على مولد القالب!", "templateGeneratorNotFound": "لا يمكن العثور على مولد القالب!",
"fileUploadFailed": "فشل في رفع الملف", "fileUploadFailed": "فشل في رفع الملف",

1
packages/nc-gui/lang/bn_IN.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/cs.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Seznam povolených speciálních znaků" "allowedSpecialCharList": "Seznam povolených speciálních znaků"
}, },
"invalidURL": "Neplatná adresa URL", "invalidURL": "Neplatná adresa URL",
"invalidEmail": "Invalid Email",
"internalError": "Došlo k nějaké interní chybě", "internalError": "Došlo k nějaké interní chybě",
"templateGeneratorNotFound": "Generátor šablon nelze najít!", "templateGeneratorNotFound": "Generátor šablon nelze najít!",
"fileUploadFailed": "Nepodařilo se nahrát soubor", "fileUploadFailed": "Nepodařilo se nahrát soubor",

1
packages/nc-gui/lang/da.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Liste over tilladte specialtegn" "allowedSpecialCharList": "Liste over tilladte specialtegn"
}, },
"invalidURL": "Ugyldig URL", "invalidURL": "Ugyldig URL",
"invalidEmail": "Invalid Email",
"internalError": "Der er opstået en intern fejl", "internalError": "Der er opstået en intern fejl",
"templateGeneratorNotFound": "Template Generator kan ikke findes!", "templateGeneratorNotFound": "Template Generator kan ikke findes!",
"fileUploadFailed": "Det er ikke lykkedes at uploade filen", "fileUploadFailed": "Det er ikke lykkedes at uploade filen",

1
packages/nc-gui/lang/de.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Erlaubte Sonderzeichenliste" "allowedSpecialCharList": "Erlaubte Sonderzeichenliste"
}, },
"invalidURL": "Ungültige URL", "invalidURL": "Ungültige URL",
"invalidEmail": "Invalid Email",
"internalError": "Interner Fehler aufgetreten", "internalError": "Interner Fehler aufgetreten",
"templateGeneratorNotFound": "Template-Generator kann nicht gefunden werden!", "templateGeneratorNotFound": "Template-Generator kann nicht gefunden werden!",
"fileUploadFailed": "Fehler beim Hochladen der Datei", "fileUploadFailed": "Fehler beim Hochladen der Datei",

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

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/es.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Lista de caracteres especiales permitidos" "allowedSpecialCharList": "Lista de caracteres especiales permitidos"
}, },
"invalidURL": "URL no válida", "invalidURL": "URL no válida",
"invalidEmail": "Invalid Email",
"internalError": "Se ha producido algún error interno", "internalError": "Se ha producido algún error interno",
"templateGeneratorNotFound": "¡No se encuentra el generador de plantillas!", "templateGeneratorNotFound": "¡No se encuentra el generador de plantillas!",
"fileUploadFailed": "Fallo al cargar el archivo", "fileUploadFailed": "Fallo al cargar el archivo",

1
packages/nc-gui/lang/eu.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/fa.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/fi.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Sallittujen erikoismerkkien luettelo" "allowedSpecialCharList": "Sallittujen erikoismerkkien luettelo"
}, },
"invalidURL": "Virheellinen URL-osoite", "invalidURL": "Virheellinen URL-osoite",
"invalidEmail": "Invalid Email",
"internalError": "Tapahtui jokin sisäinen virhe", "internalError": "Tapahtui jokin sisäinen virhe",
"templateGeneratorNotFound": "Template Generatoria ei löydy!", "templateGeneratorNotFound": "Template Generatoria ei löydy!",
"fileUploadFailed": "Tiedoston lataaminen epäonnistui", "fileUploadFailed": "Tiedoston lataaminen epäonnistui",

1
packages/nc-gui/lang/fr.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Liste des caractères spéciaux autorisés" "allowedSpecialCharList": "Liste des caractères spéciaux autorisés"
}, },
"invalidURL": "URL invalide", "invalidURL": "URL invalide",
"invalidEmail": "Invalid Email",
"internalError": "Une erreur interne est survenue", "internalError": "Une erreur interne est survenue",
"templateGeneratorNotFound": "Le générateur de modèles est introuvable !", "templateGeneratorNotFound": "Le générateur de modèles est introuvable !",
"fileUploadFailed": "Échec du téléversement du fichier", "fileUploadFailed": "Échec du téléversement du fichier",

1
packages/nc-gui/lang/he.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/hi.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/hr.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/id.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Daftar karakter khusus yang diizinkan" "allowedSpecialCharList": "Daftar karakter khusus yang diizinkan"
}, },
"invalidURL": "URL tidak valid", "invalidURL": "URL tidak valid",
"invalidEmail": "Invalid Email",
"internalError": "Beberapa kesalahan internal terjadi", "internalError": "Beberapa kesalahan internal terjadi",
"templateGeneratorNotFound": "Pembuat Templat tidak dapat ditemukan!", "templateGeneratorNotFound": "Pembuat Templat tidak dapat ditemukan!",
"fileUploadFailed": "Gagal mengunggah file", "fileUploadFailed": "Gagal mengunggah file",

1
packages/nc-gui/lang/it.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Elenco dei caratteri speciali consentiti" "allowedSpecialCharList": "Elenco dei caratteri speciali consentiti"
}, },
"invalidURL": "URL non valido", "invalidURL": "URL non valido",
"invalidEmail": "Invalid Email",
"internalError": "Si è verificato un errore interno", "internalError": "Si è verificato un errore interno",
"templateGeneratorNotFound": "Il generatore di modelli non può essere trovato!", "templateGeneratorNotFound": "Il generatore di modelli non può essere trovato!",
"fileUploadFailed": "Non è riuscito a caricare il file", "fileUploadFailed": "Non è riuscito a caricare il file",

1
packages/nc-gui/lang/ja.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "利用できる記号の一覧" "allowedSpecialCharList": "利用できる記号の一覧"
}, },
"invalidURL": "無効なURL", "invalidURL": "無効なURL",
"invalidEmail": "Invalid Email",
"internalError": "内部エラーが発生しました", "internalError": "内部エラーが発生しました",
"templateGeneratorNotFound": "テンプレートジェネレーターが見つかりません!", "templateGeneratorNotFound": "テンプレートジェネレーターが見つかりません!",
"fileUploadFailed": "ファイルのアップロードに失敗しました", "fileUploadFailed": "ファイルのアップロードに失敗しました",

1
packages/nc-gui/lang/ko.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/lv.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Atļauto īpašo rakstzīmju saraksts" "allowedSpecialCharList": "Atļauto īpašo rakstzīmju saraksts"
}, },
"invalidURL": "Nederīgs URL", "invalidURL": "Nederīgs URL",
"invalidEmail": "Invalid Email",
"internalError": "Notika kāda iekšēja kļūda", "internalError": "Notika kāda iekšēja kļūda",
"templateGeneratorNotFound": "Šablonu ģenerators nav atrodams!", "templateGeneratorNotFound": "Šablonu ģenerators nav atrodams!",
"fileUploadFailed": "Fail to upload file", "fileUploadFailed": "Fail to upload file",

1
packages/nc-gui/lang/nl.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Lijst met toegestane speciale tekens" "allowedSpecialCharList": "Lijst met toegestane speciale tekens"
}, },
"invalidURL": "Ongeldige URL", "invalidURL": "Ongeldige URL",
"invalidEmail": "Invalid Email",
"internalError": "Er is een interne fout opgetreden", "internalError": "Er is een interne fout opgetreden",
"templateGeneratorNotFound": "Sjabloongenerator kan niet worden gevonden!", "templateGeneratorNotFound": "Sjabloongenerator kan niet worden gevonden!",
"fileUploadFailed": "Bestand niet geüpload", "fileUploadFailed": "Bestand niet geüpload",

1
packages/nc-gui/lang/no.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/pl.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Dozwolona lista znaków specjalnych" "allowedSpecialCharList": "Dozwolona lista znaków specjalnych"
}, },
"invalidURL": "Nieprawidłowy adres URL", "invalidURL": "Nieprawidłowy adres URL",
"invalidEmail": "Invalid Email",
"internalError": "Wystąpił błąd wewnętrzny", "internalError": "Wystąpił błąd wewnętrzny",
"templateGeneratorNotFound": "Nie można znaleźć generatora szablonów!", "templateGeneratorNotFound": "Nie można znaleźć generatora szablonów!",
"fileUploadFailed": "Nie udało się przesłać pliku", "fileUploadFailed": "Nie udało się przesłać pliku",

1
packages/nc-gui/lang/pt.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Lista de caracteres especiais permitidos" "allowedSpecialCharList": "Lista de caracteres especiais permitidos"
}, },
"invalidURL": "URL inválido", "invalidURL": "URL inválido",
"invalidEmail": "Invalid Email",
"internalError": "Ocorreu algum erro interno", "internalError": "Ocorreu algum erro interno",
"templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!", "templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!",
"fileUploadFailed": "Falha no carregamento do ficheiro", "fileUploadFailed": "Falha no carregamento do ficheiro",

1
packages/nc-gui/lang/pt_BR.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Lista de caracteres especiais permitidos" "allowedSpecialCharList": "Lista de caracteres especiais permitidos"
}, },
"invalidURL": "URL inválido", "invalidURL": "URL inválido",
"invalidEmail": "Invalid Email",
"internalError": "Ocorreu algum erro interno", "internalError": "Ocorreu algum erro interno",
"templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!", "templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!",
"fileUploadFailed": "Falha no carregamento do ficheiro", "fileUploadFailed": "Falha no carregamento do ficheiro",

1
packages/nc-gui/lang/ru.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Список разрешенных специальных символов" "allowedSpecialCharList": "Список разрешенных специальных символов"
}, },
"invalidURL": "Неверный URL", "invalidURL": "Неверный URL",
"invalidEmail": "Invalid Email",
"internalError": "Произошла какая-то внутренняя ошибка", "internalError": "Произошла какая-то внутренняя ошибка",
"templateGeneratorNotFound": "Генератор шаблонов не найден!", "templateGeneratorNotFound": "Генератор шаблонов не найден!",
"fileUploadFailed": "Не удалось загрузить файл", "fileUploadFailed": "Не удалось загрузить файл",

1
packages/nc-gui/lang/sk.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Zoznam povolených špeciálnych znakov" "allowedSpecialCharList": "Zoznam povolených špeciálnych znakov"
}, },
"invalidURL": "Neplatná adresa URL", "invalidURL": "Neplatná adresa URL",
"invalidEmail": "Invalid Email",
"internalError": "Vyskytla sa nejaká vnútorná chyba", "internalError": "Vyskytla sa nejaká vnútorná chyba",
"templateGeneratorNotFound": "Generátor šablón nemožno nájsť!", "templateGeneratorNotFound": "Generátor šablón nemožno nájsť!",
"fileUploadFailed": "Nepodarilo sa nahrať súbor", "fileUploadFailed": "Nepodarilo sa nahrať súbor",

1
packages/nc-gui/lang/sl.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Seznam dovoljenih posebnih znakov" "allowedSpecialCharList": "Seznam dovoljenih posebnih znakov"
}, },
"invalidURL": "Nepravilen URL", "invalidURL": "Nepravilen URL",
"invalidEmail": "Invalid Email",
"internalError": "Zgodila se je neka notranja napaka", "internalError": "Zgodila se je neka notranja napaka",
"templateGeneratorNotFound": "Generatorja predlog ni mogoče najti!", "templateGeneratorNotFound": "Generatorja predlog ni mogoče najti!",
"fileUploadFailed": "Ni uspelo naložiti datoteke", "fileUploadFailed": "Ni uspelo naložiti datoteke",

1
packages/nc-gui/lang/sv.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Förteckning över tillåtna specialtecken" "allowedSpecialCharList": "Förteckning över tillåtna specialtecken"
}, },
"invalidURL": "Ogiltig URL", "invalidURL": "Ogiltig URL",
"invalidEmail": "Invalid Email",
"internalError": "Ett internt fel har uppstått.", "internalError": "Ett internt fel har uppstått.",
"templateGeneratorNotFound": "Mallgeneratorn kan inte hittas!", "templateGeneratorNotFound": "Mallgeneratorn kan inte hittas!",
"fileUploadFailed": "Uppladdning av filen misslyckades", "fileUploadFailed": "Uppladdning av filen misslyckades",

1
packages/nc-gui/lang/th.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/tr.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "İzin verilen özel karakter listesi" "allowedSpecialCharList": "İzin verilen özel karakter listesi"
}, },
"invalidURL": "Geçersiz URL", "invalidURL": "Geçersiz URL",
"invalidEmail": "Invalid Email",
"internalError": "Bazı dahili hatalar oluştu", "internalError": "Bazı dahili hatalar oluştu",
"templateGeneratorNotFound": "Şablon Oluşturucu bulunamıyor!", "templateGeneratorNotFound": "Şablon Oluşturucu bulunamıyor!",
"fileUploadFailed": "Dosya yüklenemedi", "fileUploadFailed": "Dosya yüklenemedi",

1
packages/nc-gui/lang/uk.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Дозволений список спеціальних символів" "allowedSpecialCharList": "Дозволений список спеціальних символів"
}, },
"invalidURL": "Неправильна URL-адреса", "invalidURL": "Неправильна URL-адреса",
"invalidEmail": "Invalid Email",
"internalError": "Сталась внутрішня помилка", "internalError": "Сталась внутрішня помилка",
"templateGeneratorNotFound": "Генератор шаблонів не знайдено!", "templateGeneratorNotFound": "Генератор шаблонів не знайдено!",
"fileUploadFailed": "Не вдалося завантажити файл", "fileUploadFailed": "Не вдалося завантажити файл",

1
packages/nc-gui/lang/vi.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Allowed special character list" "allowedSpecialCharList": "Allowed special character list"
}, },
"invalidURL": "Invalid URL", "invalidURL": "Invalid URL",
"invalidEmail": "Invalid Email",
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",

1
packages/nc-gui/lang/zh-Hans.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "允许的特殊字符列表" "allowedSpecialCharList": "允许的特殊字符列表"
}, },
"invalidURL": "无效的 URL", "invalidURL": "无效的 URL",
"invalidEmail": "Invalid Email",
"internalError": "发生了一些内部错误", "internalError": "发生了一些内部错误",
"templateGeneratorNotFound": "模板生成器无法找到!", "templateGeneratorNotFound": "模板生成器无法找到!",
"fileUploadFailed": "文件上传失败", "fileUploadFailed": "文件上传失败",

1
packages/nc-gui/lang/zh-Hant.json

@ -698,6 +698,7 @@
"allowedSpecialCharList": "允許特殊字元列表" "allowedSpecialCharList": "允許特殊字元列表"
}, },
"invalidURL": "無效的連結", "invalidURL": "無效的連結",
"invalidEmail": "Invalid Email",
"internalError": "發生內部錯誤", "internalError": "發生內部錯誤",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "上傳文件失敗", "fileUploadFailed": "上傳文件失敗",

1
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import type { TableType } from 'nocodb-sdk'
import { import {
TabType, TabType,
computed, computed,

7
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -27,6 +27,8 @@ const scannerIsReady = ref(false)
const showCodeScannerOverlay = ref(false) const showCodeScannerOverlay = ref(false)
const editEnabled = ref<boolean[]>([])
const onLoaded = async () => { const onLoaded = async () => {
scannerIsReady.value = true scannerIsReady.value = true
} }
@ -161,7 +163,10 @@ const onDecode = async (scannedCodeValue: string) => {
:data-testid="`nc-form-input-cell-${field.label || field.title}`" :data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`" :class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field" :column="field"
:edit-enabled="true" :edit-enabled="editEnabled[index]"
@click="editEnabled[index] = true"
@cancel="editEnabled[index] = false"
@update:edit-enabled="editEnabled[index] = $event"
/> />
<a-button <a-button
v-if="field.enable_scanner" v-if="field.enable_scanner"

55
packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

@ -4,15 +4,19 @@ import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { SwipeDirection, breakpointsTailwind } from '@vueuse/core' import { SwipeDirection, breakpointsTailwind } from '@vueuse/core'
import { import {
DropZoneRef, DropZoneRef,
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 {
@ -32,6 +36,8 @@ 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 isTransitioning = ref(false) const isTransitioning = ref(false)
const transitionName = ref<TransitionDirection>(TransitionDirection.Left) const transitionName = ref<TransitionDirection>(TransitionDirection.Left)
@ -40,10 +46,14 @@ const animationTarget = ref<AnimationTarget>(AnimationTarget.ArrowRight)
const isAnimating = ref(false) const isAnimating = ref(false)
const editEnabled = ref<boolean[]>([])
const el = ref<HTMLDivElement>() const el = ref<HTMLDivElement>()
provide(DropZoneRef, el) provide(DropZoneRef, el)
provide(IsSurveyFormInj, ref(true))
const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 50) const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 50)
const steps = computed(() => { const steps = computed(() => {
@ -64,6 +74,8 @@ 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)
function isRequired(column: ColumnType, required = false) { function isRequired(column: ColumnType, required = false) {
let columnObj = column let columnObj = column
if ( if (
@ -105,7 +117,29 @@ function animate(target: AnimationTarget) {
}, transitionDuration.value / 2) }, transitionDuration.value / 2)
} }
async function validateColumn() {
const f = field.value!
if (parseProp(f.meta)?.validate && formState.value[f.title!]) {
if (f.uidt === UITypes.Email) {
if (!validateEmail(formState.value[f.title!])) {
columnValidationError.value = true
message.error(t('msg.error.invalidEmail'))
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 || submitted.value) return if (isLast.value || submitted.value) return
if (!field.value || !field.value.title) return if (!field.value || !field.value.title) return
@ -117,6 +151,8 @@ async function goNext(animationTarget?: AnimationTarget) {
if (!isValid) return if (!isValid) return
} }
if (!(await validateColumn())) return
animate(animationTarget || AnimationTarget.ArrowRight) animate(animationTarget || AnimationTarget.ArrowRight)
setTimeout( setTimeout(
@ -159,9 +195,8 @@ function resetForm() {
goTo(steps.value[0]) goTo(steps.value[0])
} }
function submit() { async function submit() {
if (submitted.value) return if (submitted.value || !(await validateColumn())) return
submitForm() submitForm()
} }
@ -177,7 +212,7 @@ onKeyStroke(['Enter', 'Space'], () => {
if (isLast.value) { if (isLast.value) {
submit() submit()
} else { } else {
goNext(AnimationTarget.OkButton) goNext(AnimationTarget.OkButton, true)
} }
}) })
@ -263,7 +298,10 @@ onMounted(() => {
class="nc-input" class="nc-input"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`" :data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field" :column="field"
:edit-enabled="true" :edit-enabled="editEnabled[index]"
@click="editEnabled[index] = true"
@cancel="editEnabled[index] = false"
@update:edit-enabled="editEnabled[index] = $event"
/> />
<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-[0.75rem] my-2 px-1">
@ -315,7 +353,7 @@ onMounted(() => {
class="bg-opacity-100 scaling-btn flex items-center gap-1" class="bg-opacity-100 scaling-btn flex items-center gap-1"
data-testid="nc-survey-form__btn-next" data-testid="nc-survey-form__btn-next"
:class="[ :class="[
v$.localState[field.title]?.$error ? 'after:!bg-gray-100 after:!ring-red-500' : '', v$.localState[field.title]?.$error || columnValidationError ? 'after:!bg-gray-100 after:!ring-red-500' : '',
animationTarget === AnimationTarget.OkButton && isAnimating animationTarget === AnimationTarget.OkButton && isAnimating
? '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)'
: '', : '',
@ -327,7 +365,10 @@ onMounted(() => {
</Transition> </Transition>
<Transition name="slide-right" mode="out-in"> <Transition name="slide-right" mode="out-in">
<MdiCloseCircleOutline v-if="v$.localState[field.title]?.$error" class="text-red-500 md:text-md" /> <MdiCloseCircleOutline
v-if="v$.localState[field.title]?.$error || columnValidationError"
class="text-red-500 md:text-md"
/>
<MdiCheck v-else class="text-white md:text-md" /> <MdiCheck v-else class="text-white md:text-md" />
</Transition> </Transition>
</button> </button>

17
packages/nc-gui/utils/validation.ts

@ -173,3 +173,20 @@ export const extraParameterValidator = {
}) })
}, },
} }
export const emailValidator = {
validator: (_: unknown, value: string) => {
return new Promise((resolve, reject) => {
if (!value || value.length === 0) {
return reject(new Error('Email is required'))
}
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !validateEmail(e))
if (invalidEmails.length > 0) {
return reject(
new Error(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `),
)
}
return resolve(true)
})
},
}

4
packages/nc-gui/utils/viewUtils.ts

@ -41,10 +41,6 @@ export function applyLanguageDirection(dir: typeof rtl | typeof ltr) {
document.body.style.direction = dir document.body.style.direction = dir
} }
export function applyNonSelectable() {
document.body.classList.add('non-selectable')
}
export const getViewIcon = (key?: string | number) => { export const getViewIcon = (key?: string | number) => {
if (!key) return if (!key) return

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

@ -64,7 +64,7 @@ export class FormPage extends BasePage {
} }
getFormFieldsRequired() { getFormFieldsRequired() {
return this.get().locator('[data-testid="nc-form-input-required"]'); return this.get().locator('[data-testid="nc-form-input-required"] + button');
} }
getFormFieldsInputLabel() { getFormFieldsInputLabel() {
@ -153,6 +153,9 @@ export class FormPage extends BasePage {
async fillForm(param: { field: string; value: string }[]) { async fillForm(param: { field: string; value: string }[]) {
for (let i = 0; i < param.length; i++) { for (let i = 0; i < param.length; i++) {
await this.get()
.locator(`[data-testid="nc-form-input-${param[i].field.replace(' ', '')}"]`)
.click();
await this.get() await this.get()
.locator(`[data-testid="nc-form-input-${param[i].field.replace(' ', '')}"] >> input`) .locator(`[data-testid="nc-form-input-${param[i].field.replace(' ', '')}"] >> input`)
.fill(param[i].value); .fill(param[i].value);
@ -177,9 +180,7 @@ export class FormPage extends BasePage {
await this.getFormFieldsInputLabel().fill(label); await this.getFormFieldsInputLabel().fill(label);
await this.getFormFieldsInputHelpText().fill(helpText); await this.getFormFieldsInputHelpText().fill(helpText);
if (required) { if (required) {
await this.get() await this.getFormFieldsRequired().click();
.locator(`.nc-form-drag-${field.replace(' ', '')}`)
.click();
} }
await this.formHeading.click(); await this.formHeading.click();
} }

1
tests/playwright/pages/Dashboard/SurveyForm/index.ts

@ -66,6 +66,7 @@ export class SurveyFormPage extends BasePage {
// press enter key // press enter key
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).press('Enter'); await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).press('Enter');
} else if (param.type === 'DateTime') { } else if (param.type === 'DateTime') {
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).click();
const modal = await this.rootPage.locator('.nc-picker-datetime'); const modal = await this.rootPage.locator('.nc-picker-datetime');
await expect(modal).toBeVisible(); await expect(modal).toBeVisible();
await modal.locator('.ant-picker-now-btn').click(); await modal.locator('.ant-picker-now-btn').click();

Loading…
Cancel
Save