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. 13
      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">
import { applyNonSelectable, computed, useRoute, useTheme } from '#imports'
import { computed, useRoute, useTheme } from '#imports'
const route = useRoute()
@ -7,7 +7,18 @@ const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || ro
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
const key = ref(0)

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

@ -30,15 +30,4 @@ For Drag and Drop
*/
.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 {
Form,
computed,
emailValidator,
extractSdkResponseErrorMsg,
message,
ref,
@ -11,7 +12,6 @@ import {
useDashboard,
useI18n,
useNuxtApp,
validateEmail,
} from '#imports'
import type { User } from '~/lib'
import { Role } from '~/lib'
@ -44,24 +44,10 @@ const usersData = $ref<Users>({ emails: '', role: Role.OrgLevelViewer, invitatio
const formRef = ref()
const useForm = Form.useForm
const validators = computed(() => {
return {
emails: [
{
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()
}
},
},
],
emails: [emailValidator],
}
})

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

@ -1,28 +1,53 @@
<script lang="ts" setup>
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 {
modelValue: string | null | undefined
}
interface Emits {
(event: 'update:modelValue', model: string): void
}
const { modelValue: value } = defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const emits = defineEmits<Emits>()
const { t } = useI18n()
const { showNull } = useGlobal()
const editEnabled = inject(EditModeInj)
const editEnabled = inject(EditModeInj)!
const column = inject(ColumnInj)!
// 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 vModel = useVModel(props, 'modelValue', emits)
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 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>
<template>
@ -30,7 +55,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="outline-none text-sm px-2"
class="w-full outline-none text-sm px-2"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop

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

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

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

@ -4,6 +4,7 @@ import {
CellUrlDisableOverlayInj,
ColumnInj,
EditModeInj,
IsSurveyFormInj,
computed,
inject,
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
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 && isValidURL(val)) || !val) {
if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isSurveyForm.value) {
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">
<a-tooltip placement="top">
<template #title> Invalid URL </template>
<template #title> {{ t('msg.error.invalidURL') }} </template>
<div class="flex flex-row items-center">
<MiCircleWarning class="text-red-400 h-4" />
</div>

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

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

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

@ -97,6 +97,8 @@ const submitted = ref(false)
const activeRow = ref('')
const editEnabled = ref<boolean[]>([])
const { t } = useI18n()
const { betaFeatureToggleState } = useBetaFeatureToggle()
@ -283,6 +285,8 @@ function setFormData() {
.sort((a, b) => a.order - b.order)
.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)
hiddenColumns.value = col.filter(
@ -727,7 +731,10 @@ watch(view, (nextView) => {
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
: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
/>
</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 {
Form,
computed,
emailValidator,
extractSdkResponseErrorMsg,
message,
onMounted,
@ -17,7 +18,6 @@ import {
useI18n,
useNuxtApp,
useProject,
validateEmail,
} from '#imports'
import type { User } from '~/lib'
import { ProjectRole } from '~/lib'
@ -54,24 +54,10 @@ let usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invit
const formRef = ref()
const useForm = Form.useForm
const validators = computed(() => {
return {
emails: [
{
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()
}
},
},
],
emails: [emailValidator],
}
})

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

@ -14,6 +14,7 @@ export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['pag
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 IsSurveyFormInj: InjectionKey<Ref<boolean>> = Symbol('is-survey-form-injection')
export const IsGridInj: InjectionKey<Ref<boolean>> = Symbol('is-grid-injection')
export const IsGalleryInj: InjectionKey<Ref<boolean>> = Symbol('is-gallery-injection')
export const IsKanbanInj: InjectionKey<Ref<boolean>> = Symbol('is-kanban-injection')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Sallittujen erikoismerkkien luettelo"
},
"invalidURL": "Virheellinen URL-osoite",
"invalidEmail": "Invalid Email",
"internalError": "Tapahtui jokin sisäinen virhe",
"templateGeneratorNotFound": "Template Generatoria ei löydy!",
"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"
},
"invalidURL": "URL invalide",
"invalidEmail": "Invalid Email",
"internalError": "Une erreur interne est survenue",
"templateGeneratorNotFound": "Le générateur de modèles est introuvable !",
"fileUploadFailed": "Échec du téléversement du fichier",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -698,6 +698,7 @@
"allowedSpecialCharList": "Dozwolona lista znaków specjalnych"
},
"invalidURL": "Nieprawidłowy adres URL",
"invalidEmail": "Invalid Email",
"internalError": "Wystąpił błąd wewnętrzny",
"templateGeneratorNotFound": "Nie można znaleźć generatora szablonów!",
"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"
},
"invalidURL": "URL inválido",
"invalidEmail": "Invalid Email",
"internalError": "Ocorreu algum erro interno",
"templateGeneratorNotFound": "O Gerador de Modelos não pode ser encontrado!",
"fileUploadFailed": "Falha no carregamento do ficheiro",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import type { TableType } from 'nocodb-sdk'
import {
TabType,
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 editEnabled = ref<boolean[]>([])
const onLoaded = async () => {
scannerIsReady.value = true
}
@ -161,7 +163,10 @@ const onDecode = async (scannedCodeValue: string) => {
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
: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
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 {
DropZoneRef,
IsSurveyFormInj,
computed,
isValidURL,
onKeyStroke,
onMounted,
provide,
ref,
useBreakpoints,
useI18n,
usePointerSwipe,
useSharedFormStoreOrThrow,
useStepper,
validateEmail,
} from '#imports'
enum TransitionDirection {
@ -32,6 +36,8 @@ const { md } = useBreakpoints(breakpointsTailwind)
const { v$, formState, formColumns, submitForm, submitted, secondsRemain, sharedFormView, sharedViewMeta, onReset } =
useSharedFormStoreOrThrow()
const { t } = useI18n()
const isTransitioning = ref(false)
const transitionName = ref<TransitionDirection>(TransitionDirection.Left)
@ -40,10 +46,14 @@ const animationTarget = ref<AnimationTarget>(AnimationTarget.ArrowRight)
const isAnimating = ref(false)
const editEnabled = ref<boolean[]>([])
const el = ref<HTMLDivElement>()
provide(DropZoneRef, el)
provide(IsSurveyFormInj, ref(true))
const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 50)
const steps = computed(() => {
@ -64,6 +74,8 @@ const { index, goToPrevious, goToNext, isFirst, isLast, goTo } = useStepper(step
const field = computed(() => formColumns.value?.[index.value])
const columnValidationError = ref(false)
function isRequired(column: ColumnType, required = false) {
let columnObj = column
if (
@ -105,7 +117,29 @@ function animate(target: AnimationTarget) {
}, 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) {
columnValidationError.value = false
if (isLast.value || submitted.value) return
if (!field.value || !field.value.title) return
@ -117,6 +151,8 @@ async function goNext(animationTarget?: AnimationTarget) {
if (!isValid) return
}
if (!(await validateColumn())) return
animate(animationTarget || AnimationTarget.ArrowRight)
setTimeout(
@ -159,9 +195,8 @@ function resetForm() {
goTo(steps.value[0])
}
function submit() {
if (submitted.value) return
async function submit() {
if (submitted.value || !(await validateColumn())) return
submitForm()
}
@ -177,7 +212,7 @@ onKeyStroke(['Enter', 'Space'], () => {
if (isLast.value) {
submit()
} else {
goNext(AnimationTarget.OkButton)
goNext(AnimationTarget.OkButton, true)
}
})
@ -263,7 +298,10 @@ onMounted(() => {
class="nc-input"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
: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">
@ -315,7 +353,7 @@ onMounted(() => {
class="bg-opacity-100 scaling-btn flex items-center gap-1"
data-testid="nc-survey-form__btn-next"
: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
? 'transform translate-y-[2px] translate-x-[2px] after:(!ring !ring-accent !ring-opacity-100)'
: '',
@ -327,7 +365,10 @@ onMounted(() => {
</Transition>
<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" />
</Transition>
</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
}
export function applyNonSelectable() {
document.body.classList.add('non-selectable')
}
export const getViewIcon = (key?: string | number) => {
if (!key) return

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

@ -64,7 +64,7 @@ export class FormPage extends BasePage {
}
getFormFieldsRequired() {
return this.get().locator('[data-testid="nc-form-input-required"]');
return this.get().locator('[data-testid="nc-form-input-required"] + button');
}
getFormFieldsInputLabel() {
@ -153,6 +153,9 @@ export class FormPage extends BasePage {
async fillForm(param: { field: string; value: string }[]) {
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()
.locator(`[data-testid="nc-form-input-${param[i].field.replace(' ', '')}"] >> input`)
.fill(param[i].value);
@ -177,9 +180,7 @@ export class FormPage extends BasePage {
await this.getFormFieldsInputLabel().fill(label);
await this.getFormFieldsInputHelpText().fill(helpText);
if (required) {
await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`)
.click();
await this.getFormFieldsRequired().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
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).press('Enter');
} 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');
await expect(modal).toBeVisible();
await modal.locator('.ant-picker-now-btn').click();

Loading…
Cancel
Save