Browse Source

Nc feat/survey form v2 (#7843)

* fix(nc-gui): survey form v2 setup

* fix(nc-gui): survey form ui updated

* fix(nc-gui): survery form ui changes for oss

* chore(nc-gui): lint

* chore(nc-gui): lint

* chore(nc-gui): revert unrelated changes

* test(nc-gui): update pw test of survey form

* fix(nc-gui): update survey form according to new design

* fix(nc-gui): add survey form slide animation

* fix(nc-gui): hide survey form pagination in first slide

* fix(nc-gui): optimize shared form for mobile screen

* chore(nc-gui): lint

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

* fix(nc-gui): some of the pr review changes

* fix(nc-gui): add placeholder for datetime related fields

* fix(nc-gui): allow upload same file next time

* fix(nc-gui): gallery image display issue nocodb/nocodb/issues/7851

* chore(nc-gui): lint

* fix(nc-gui): survey form ui changes for oss

* fix(nc-gui): use i18n for survey form

* fix(nc-gui): use keydown space for date, datetime fields to open modal in survey form
pull/7857/head
Ramesh Mane 7 months ago committed by GitHub
parent
commit
2cd0a1c74a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      packages/nc-gui/assets/nc-icons/arrow-left.svg
  2. 6
      packages/nc-gui/assets/nc-icons/arrow-right.svg
  3. 4
      packages/nc-gui/assets/nc-icons/circle-check.svg
  4. 6
      packages/nc-gui/components/cell/Checkbox.vue
  5. 27
      packages/nc-gui/components/cell/DatePicker.vue
  6. 28
      packages/nc-gui/components/cell/DateTimePicker.vue
  7. 28
      packages/nc-gui/components/cell/TimePicker.vue
  8. 28
      packages/nc-gui/components/cell/YearPicker.vue
  9. 6
      packages/nc-gui/components/cell/attachment/index.vue
  10. 4
      packages/nc-gui/components/cell/attachment/utils.ts
  11. 2
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  12. 2
      packages/nc-gui/components/nc/Badge.vue
  13. 42
      packages/nc-gui/components/smartsheet/Form.vue
  14. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  15. 6
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  16. 4
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  17. 8
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  18. 4
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  19. 6
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  20. 4
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  21. 6
      packages/nc-gui/composables/useSharedFormViewStore.ts
  22. 5
      packages/nc-gui/lang/en.json
  23. 5
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  24. 31
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  25. 521
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  26. 7
      packages/nc-gui/utils/iconUtils.ts
  27. 42
      tests/playwright/pages/Dashboard/SurveyForm/index.ts
  28. 14
      tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

6
packages/nc-gui/assets/nc-icons/arrow-left.svg

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6666 8H3.33331" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M7.99998 12.6666L3.33331 7.99998L7.99998 3.33331" stroke="currentColor" stroke-width="1.33333"
stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 404 B

6
packages/nc-gui/assets/nc-icons/arrow-right.svg

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33331 8H12.6666" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M8 3.33331L12.6667 7.99998L8 12.6666" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 392 B

4
packages/nc-gui/assets/nc-icons/circle-check.svg

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11.0799V11.9999C21.9988 14.1563 21.3005 16.2545 20.0093 17.9817C18.7182 19.7088 16.9033 20.9723 14.8354 21.5838C12.7674 22.1952 10.5573 22.1218 8.53447 21.3744C6.51168 20.6271 4.78465 19.246 3.61096 17.4369C2.43727 15.6279 1.87979 13.4879 2.02168 11.3362C2.16356 9.18443 2.99721 7.13619 4.39828 5.49694C5.79935 3.85768 7.69279 2.71525 9.79619 2.24001C11.8996 1.76477 14.1003 1.9822 16.07 2.85986" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 4L12 14.01L9 11.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 722 B

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

@ -4,6 +4,7 @@ import {
ColumnInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
getMdiIcon,
inject,
@ -44,6 +45,8 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const rowHeight = inject(RowHeightInj, ref())
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const checkboxMeta = computed(() => {
return {
icon: {
@ -98,7 +101,8 @@ useSelectedCellKeyupListener(active, (e) => {
}"
:tabindex="readOnly ? -1 : 0"
@click="onClick(false, $event)"
@keydown.enter.stop="onClick(true, $event)"
@keydown.enter.stop="!isSurveyForm ? onClick(true, $event) : undefined"
@keydown.space.stop="isSurveyForm ? onClick(true, $event) : undefined"
>
<div
class="flex items-center"

27
packages/nc-gui/components/cell/DatePicker.vue

@ -7,6 +7,7 @@ import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
inject,
@ -45,6 +46,10 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isDateInvalid = ref(false)
const dateFormat = computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD')
@ -98,7 +103,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isDateInvalid.value) {
return dateFormat.value
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -232,6 +239,22 @@ const clickHandler = () => {
}
cellClickHandler()
}
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
</script>
<template>
@ -251,7 +274,7 @@ const clickHandler = () => {
:open="isOpen"
@click="clickHandler"
@update:open="updateOpen"
@keydown.enter="open = !open"
@keydown="handleKeydown"
>
<template #suffixIcon></template>
</a-date-picker>

28
packages/nc-gui/components/cell/DateTimePicker.vue

@ -6,6 +6,8 @@ import {
CellClickHookInj,
ColumnInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
inject,
isDrawerOrModalExist,
@ -35,6 +37,10 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false))
@ -151,7 +157,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isDateInvalid.value) {
return dateTimeFormat.value
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -286,6 +294,22 @@ const clickHandler = () => {
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
</script>
<template>
@ -304,7 +328,7 @@ const isColDisabled = computed(() => {
:open="isOpen"
@click="clickHandler"
@ok="okHandler"
@keydown.enter="open = !open"
@keydown="handleKeydown"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -3,6 +3,8 @@ import dayjs from 'dayjs'
import {
ActiveCellInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
inject,
onClickOutside,
@ -32,6 +34,10 @@ const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const column = inject(ColumnInj)!
const isTimeInvalid = ref(false)
@ -90,7 +96,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isTimeInvalid.value) {
return 'HH:mm'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -107,6 +115,22 @@ const isOpen = computed(() => {
return (readOnly.value || (localState.value && isPk)) && !active.value && !editable.value ? false : open.value
})
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
@ -126,6 +150,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
<template>
<a-time-picker
v-model:value="localState"
:tabindex="0"
:disabled="readOnly"
:show-time="true"
:bordered="false"
@ -138,6 +163,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:input-read-only="true"
:open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@keydown="handleKeydown"
@click="open = (active || editable) && !open"
@ok="open = !open"
>

28
packages/nc-gui/components/cell/YearPicker.vue

@ -3,6 +3,8 @@ import dayjs from 'dayjs'
import {
ActiveCellInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
inject,
@ -31,6 +33,10 @@ const editable = inject(EditModeInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isYearInvalid = ref(false)
const { t } = useI18n()
@ -77,7 +83,9 @@ watch(
)
const placeholder = computed(() => {
if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
if (isForm.value && !isYearInvalid.value) {
return 'YYYY'
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
return t('general.null').toUpperCase()
@ -94,6 +102,22 @@ const isOpen = computed(() => {
return (readOnly.value || (localState.value && isPk)) && !active.value && !editable.value ? false : open.value
})
const handleKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case ' ':
if (isSurveyForm.value) {
open.value = !open.value
}
break
case 'Enter':
if (!isSurveyForm.value) {
open.value = !open.value
}
break
}
}
useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
switch (e.key) {
case 'Enter':
@ -123,10 +147,10 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:input-read-only="true"
:open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@keydown="handleKeydown"
@click="open = (active || editable) && !open"
@change="open = (active || editable) && !open"
@ok="open = !open"
@keydown.enter="open = !open"
>
<template #suffixIcon></template>
</a-date-picker>

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

@ -9,6 +9,7 @@ import {
IsExpandedFormOpenInj,
IsGalleryInj,
IsKanbanInj,
IsSurveyFormInj,
RowHeightInj,
iconMap,
inject,
@ -49,6 +50,8 @@ const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()!
const { isMobileMode } = useGlobal()
@ -208,7 +211,8 @@ const onImageClick = (item: any) => {
data-testid="attachment-cell-file-picker-button"
tabindex="0"
@click="open"
@keydown.enter="open"
@keydown.enter="!isSurveyForm ? open($event) : undefined"
@keydown.space="isSurveyForm ? open($event) : undefined"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

4
packages/nc-gui/components/cell/attachment/utils.ts

@ -63,7 +63,9 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const { api, isLoading } = useApi()
const { files, open } = useFileDialog()
const { files, open } = useFileDialog({
reset: true,
})
const { appInfo } = useGlobal()

2
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -146,7 +146,7 @@ const isTableOpened = computed(() => {
return openedTableId.value === table.value?.id && (activeView.value?.is_default || !activeViewTitleOrId.value)
})
let tableTimeout: number
let tableTimeout: NodeJS.Timeout
watch(openedTableId, () => {
if (tableTimeout) {

2
packages/nc-gui/components/nc/Badge.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
const props = withDefaults(
defineProps<{
color: string
color?: string
border?: boolean
size?: 'sm' | 'md' | 'lg'
}>(),

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

@ -750,27 +750,29 @@ useEventListener(
</template>
</a-alert>
<div v-if="formViewData.show_blank_form" class="text-gray-400 mt-4">
{{
$t('msg.newFormWillBeLoaded', {
seconds: secondsRemain,
})
}}
</div>
<div class="mt-16 w-full flex justify-between items-center gap-3">
<div v-if="formViewData.show_blank_form" class="text-gray-400">
{{
$t('msg.newFormWillBeLoaded', {
seconds: secondsRemain,
})
}}
</div>
<div v-if="formViewData.submit_another_form || !isPublic" class="text-right mt-4">
<NcButton
type="primary"
size="medium"
@click="
() => {
submitted = false
clearForm()
}
"
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
<div v-if="formViewData.submit_another_form || !isPublic" class="flex-1 flex justify-end">
<NcButton
type="primary"
size="small"
@click="
() => {
submitted = false
clearForm()
}
"
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
</div>
</div>
</div>
</div>

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

@ -285,7 +285,7 @@ watch(
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-cover"
class="h-52 !object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
@click="expandFormClick($event, record)"
/>

6
packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue

@ -4,7 +4,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { isRowEmpty } from '~/utils'
const emit = defineEmits(['expandRecord', 'newRecord'])
const emit = defineEmits(['expand-record', 'new-record'])
const meta = inject(MetaInj, ref())
@ -192,7 +192,7 @@ const newRecord = () => {
[calendarRange.value[0].fk_from_col!.title!]: selectedDate.value.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
emit('new-record', record)
}
</script>
@ -221,7 +221,7 @@ const newRecord = () => {
:resize="false"
color="blue"
size="small"
@click="emit('expandRecord', record)"
@click="emit('expand-record', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell

4
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -4,7 +4,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['expandRecord', 'newRecord'])
const emit = defineEmits(['expandRecord', 'new-record'])
const {
// activeCalendarView,
@ -847,7 +847,7 @@ const newRecord = (hour: dayjs.Dayjs) => {
[calendarRange.value[0].fk_from_col!.title!]: hour.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
emit('new-record', record)
}
</script>

8
packages/nc-gui/components/smartsheet/calendar/MonthView.vue

@ -4,7 +4,7 @@ import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['newRecord', 'expandRecord'])
const emit = defineEmits(['new-record', 'expandRecord'])
const {
selectedDate,
@ -635,7 +635,7 @@ const addRecord = (date: dayjs.Dayjs) => {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', newRecord)
emit('new-record', newRecord)
}
</script>
@ -710,7 +710,7 @@ const addRecord = (date: dayjs.Dayjs) => {
[range.fk_from_col!.title!]: dayjs(day).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
emit('new-record', record)
}
"
>
@ -738,7 +738,7 @@ const addRecord = (date: dayjs.Dayjs) => {
[calendarRange[0].fk_from_col!.title!]: (day).format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emit('newRecord', record)
emit('new-record', record)
}
"
>

4
packages/nc-gui/components/smartsheet/calendar/SideMenu.vue

@ -8,7 +8,7 @@ const props = defineProps<{
visible: boolean
}>()
const emit = defineEmits(['expandRecord', 'newRecord'])
const emit = defineEmits(['expand-record', 'newRecord'])
const INFINITY_SCROLL_THRESHOLD = 100
@ -428,7 +428,7 @@ onUnmounted(() => {
"
color="blue"
data-testid="nc-sidebar-record-card"
@click="emit('expandRecord', record)"
@click="emit('expand-record', record)"
@dragstart="dragStart($event, record)"
@dragover.prevent
>

6
packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue

@ -5,7 +5,7 @@ import type { Row } from '~/lib'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord', 'newRecord'])
const emits = defineEmits(['expandRecord'])
const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
useCalendarViewStoreOrThrow()
@ -529,7 +529,7 @@ const addRecord = (date: dayjs.Dayjs) => {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emits('newRecord', newRecord)
emits('new-record', newRecord)
}
</script>
@ -586,7 +586,7 @@ const addRecord = (date: dayjs.Dayjs) => {
:resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id"
color="blue"
@dblclick.stop="emits('expandRecord', record)"
@dblclick.stop="emits('expand-record', record)"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">

4
packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue

@ -5,7 +5,7 @@ import type { Row } from '~/lib'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord', 'newRecord'])
const emits = defineEmits(['expandRecord'])
const {
selectedDateRange,
@ -733,7 +733,7 @@ const addRecord = (date: dayjs.Dayjs) => {
[fromCol.title!]: date.format('YYYY-MM-DD HH:mm:ssZ'),
},
}
emits('newRecord', newRecord)
emits('new-record', newRecord)
}
</script>

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

@ -467,11 +467,12 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
return preFillValue
}
let intvl: NodeJS.Timeout
/** reset form if show_blank_form is true */
watch(submitted, (nextVal) => {
if (nextVal && sharedFormView.value?.show_blank_form) {
secondsRemain.value = 5
const intvl = setInterval(() => {
intvl = setInterval(() => {
secondsRemain.value = secondsRemain.value - 1
if (secondsRemain.value < 0) {
@ -486,6 +487,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
/** reset form state and validation */
if (!nextVal) {
if (sharedFormView.value?.show_blank_form) {
clearInterval(intvl)
}
additionalState.value = {}
formState.value = {}
v$.value?.$reset()

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

@ -429,7 +429,8 @@
"setDefault": "Set Default"
},
"selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here",
"noOptionsFound": "No options found"
"noOptionsFound": "No options found",
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?"
},
"labels": {
"selectYear": "Select Year",
@ -681,7 +682,7 @@
"sourceNameRequired": "Source name is required",
"changeWsName": "Change Workspace Name",
"pressEnter": "Press Enter",
"newFormLoaded": "New form will be loaded after",
"newFormLoaded": "Loading new form in",
"webhook": "Webhook",
"multiField": {
"newField": "New field",

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

@ -28,7 +28,10 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<template>
<div
class="scrollbar-thin-dull h-[100vh] overflow-y-auto overflow-x-hidden flex flex-col color-transition p-4 lg:p-10 nc-form-view relative min-h-[600px]"
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="{
'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode,
}"
:style="{
background: parseProp(sharedFormView?.meta)?.background_color || '#F9F9FA',
}"

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

@ -19,6 +19,8 @@ const {
progress,
} = useSharedFormStoreOrThrow()
const { isMobileMode } = storeToRefs(useConfigStore())
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
if (
@ -125,14 +127,25 @@ const onDecode = async (scannedCodeValue: string) => {
</template>
</a-alert>
<p v-if="sharedFormView.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 my-4">
{{ $t('msg.newFormWillBeLoaded', { seconds: secondsRemain }) }}
</p>
<div
v-if="sharedFormView.show_blank_form || sharedFormView.submit_another_form"
class="mt-16 w-full flex justify-between items-center flex-wrap gap-3"
>
<p v-if="sharedFormView?.show_blank_form" class="text-sm text-gray-500 dark:text-slate-300 m-0">
{{ $t('labels.newFormLoaded') }} {{ secondsRemain }} {{ $t('general.seconds').toLowerCase() }}
</p>
<div v-if="sharedFormView.submit_another_form" class="text-right">
<NcButton type="primary" size="medium" @click="submitted = false">
{{ $t('activity.submitAnotherForm') }}
</NcButton>
<div class="flex-1 self-end flex justify-end">
<NcButton
v-if="sharedFormView?.submit_another_form"
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit-another-form"
@click="submitted = false"
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
</div>
</div>
</div>
</div>
@ -239,7 +252,7 @@ const onDecode = async (scannedCodeValue: string) => {
<NcButton
html-type="reset"
type="secondary"
size="small"
:size="isMobileMode ? 'medium' : 'small'"
:disabled="isLoading"
class="nc-shared-form-button shared-form-clear-button"
data-testid="shared-form-clear-button"
@ -252,7 +265,7 @@ const onDecode = async (scannedCodeValue: string) => {
html-type="submit"
:disabled="progress"
type="primary"
size="small"
:size="isMobileMode ? 'medium' : 'small'"
class="nc-shared-form-button shared-form-submit-button"
data-testid="shared-form-submit-button"
@click="submitForm"

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

@ -6,7 +6,6 @@ import {
DropZoneRef,
IsSurveyFormInj,
computed,
iconMap,
isValidURL,
onKeyStroke,
onMounted,
@ -39,6 +38,8 @@ const { v$, formState, formColumns, submitForm, submitted, secondsRemain, shared
const { t } = useI18n()
const { isMobileMode } = storeToRefs(useConfigStore())
const isTransitioning = ref(false)
const transitionName = ref<TransitionDirection>(TransitionDirection.Left)
@ -47,13 +48,19 @@ const animationTarget = ref<AnimationTarget>(AnimationTarget.ArrowRight)
const isAnimating = ref(false)
const isStarted = ref(false)
const dialogShow = ref(false)
const el = ref<HTMLDivElement>()
const activeCell = ref<HTMLElement>()
provide(DropZoneRef, el)
provide(IsSurveyFormInj, ref(true))
const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 50)
const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 100)
const steps = computed(() => {
if (!formColumns.value) return []
@ -139,7 +146,7 @@ async function validateColumn() {
async function goNext(animationTarget?: AnimationTarget) {
columnValidationError.value = false
if (isLast.value || submitted.value) return
if (isLast.value || !isStarted.value || submitted.value) return
if (!field.value || !field.value.title) return
@ -165,7 +172,9 @@ async function goNext(animationTarget?: AnimationTarget) {
}
async function goPrevious(animationTarget?: AnimationTarget) {
if (isFirst.value || submitted.value) return
if (isFirst.value || !isStarted.value || submitted.value) return
columnValidationError.value = false
animate(animationTarget || AnimationTarget.ArrowLeft)
@ -178,11 +187,13 @@ function focusInput() {
if (document && typeof document !== 'undefined') {
const inputEl =
(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)
if (inputEl) {
inputEl.select()
inputEl.focus()
activeCell.value = inputEl
inputEl?.select?.()
inputEl?.focus?.()
}
}
}
@ -190,17 +201,33 @@ function focusInput() {
function resetForm() {
v$.value.$reset()
submitted.value = false
isStarted.value = false
transition(TransitionDirection.Right)
goTo(steps.value[0])
}
async function submit() {
if (submitted.value || !(await validateColumn())) return
dialogShow.value = false
submitForm()
}
onReset(resetForm)
const onStart = () => {
isStarted.value = true
setTimeout(() => {
focusInput()
}, 100)
}
const handleFocus = () => {
if (document?.activeElement !== activeCell.value) {
focusInput()
}
}
onKeyStroke(['ArrowLeft', 'ArrowDown'], () => {
goPrevious(AnimationTarget.ArrowLeft)
})
@ -208,16 +235,34 @@ onKeyStroke(['ArrowRight', 'ArrowUp'], () => {
goNext(AnimationTarget.ArrowRight)
})
onKeyStroke(['Enter', 'Space'], () => {
if (isLast.value) {
submit()
} else {
goNext(AnimationTarget.OkButton, true)
if (submitted.value) return
if (!isStarted.value && !submitted.value) {
onStart()
} else if (isStarted.value) {
if (isLast.value) {
if (dialogShow.value) {
submit()
} else {
dialogShow.value = true
}
} else {
const activeElement = document.activeElement as HTMLElement
if (activeElement?.classList && activeElement.classList.contains('nc-survey-form__btn-next')) return
goNext(AnimationTarget.OkButton, true)
}
}
})
onMounted(() => {
focusInput()
onKeyStroke('Escape', () => {
if (document) {
;(document.activeElement as HTMLElement)?.blur?.()
}
})
onMounted(() => {
if (!md.value) {
const { direction } = usePointerSwipe(el, {
onSwipe: () => {
@ -232,162 +277,40 @@ onMounted(() => {
})
}
})
watch(
formState,
() => {
columnValidationError.value = false
},
{
deep: true,
},
)
</script>
<template>
<div ref="el" class="survey pt-8 md:p-0 w-full h-full flex flex-col">
<div
v-if="sharedFormView"
style="height: max(40vh, 225px); min-height: 225px"
class="w-full max-w-[max(33%,600px)] mx-auto flex flex-col justify-end"
>
<div class="px-4 md:px-0 flex flex-col justify-end">
<h1 class="text-2xl font-bold text-gray-900 self-center text-center my-4" data-testid="nc-survey-form__heading">
{{ sharedFormView.heading }}
</h1>
<div v-if="sharedFormView.subheading?.trim()" class="w-full">
<LazyCellRichText
:value="sharedFormView.subheading"
class="font-medium text-base text-gray-500 dark:text-slate-300 !h-auto mb-4 -ml-1"
is-form-field
read-only
sync-value-change
data-testid="nc-survey-form__sub-heading"
<div class="h-full">
<div class="survey md:p-0 w-full h-full flex flex-col max-w-[max(33%,688px)] mx-auto mb-4rem lg:mb-10rem">
<div v-if="sharedFormView" class="my-auto">
<template v-if="!isStarted || submitted">
<GeneralFormBanner
v-if="sharedFormView && !parseProp(sharedFormView?.meta).hide_banner"
:banner-image-url="sharedFormView.banner_image_url"
class="flex-none mb-4"
/>
</div>
</div>
</div>
<div class="h-full w-full flex items-center px-4 md:px-0">
<Transition :name="`slide-${transitionName}`" :duration="transitionDuration" mode="out-in">
<div
ref="el"
:key="field?.title"
class="color-transition h-full flex flex-col mt-6 gap-4 w-full max-w-[max(33%,600px)] m-auto"
>
<div v-if="field && !submitted" class="flex flex-col gap-2">
<div class="nc-form-column-label text-sm font-semibold text-gray-800" data-testid="nc-form-column-label">
<span>
{{ field.label || field.title }}
</span>
<span v-if="isRequired(field, field.required)" class="text-red-500 text-base leading-[18px]">&nbsp;*</span>
</div>
<div
v-if="field?.description"
class="nc-form-column-description text-gray-500 text-sm"
data-testid="nc-survey-form__field-description"
>
<LazyCellRichText :value="field?.description" class="!h-auto -ml-1" is-form-field read-only sync-value-change />
</div>
<NcTooltip :disabled="!field?.read_only">
<template #title> {{ $t('activity.preFilledFields.lockedFieldTooltip') }} </template>
<LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
v-model="formState[field.title]"
class="mt-0 nc-input h-auto"
:class="{
readonly: field?.read_only,
}"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:read-only="field?.read_only"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input h-auto"
:class="{ 'layout-list': parseProp(field?.meta)?.isList, 'readonly': field?.read_only }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="!field?.read_only"
:read-only="field?.read_only"
/>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>
<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') }}
<MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> {{ $t('msg.makeLineBreak') }}
</div>
</div>
</LazySmartsheetDivDataCell>
</NcTooltip>
</div>
<div class="ml-1 mt-4 flex w-full text-lg">
<div class="flex-1 flex justify-center">
<div v-if="isLast && !submitted && !v$.$invalid" class="text-center my-4">
<NcButton
:class="
animationTarget === AnimationTarget.SubmitButton && isAnimating
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
: ''
"
html-type="submit"
data-testid="nc-survey-form__btn-submit"
@click="submit"
>
{{ $t('general.submit') }}
</NcButton>
</div>
<div v-else-if="!submitted" class="flex items-center gap-3 flex-col">
<a-tooltip
:title="
v$.localState[field.title]?.$error ? v$.localState[field.title].$errors[0].$message : $t('msg.info.goToNext')
"
:mouse-enter-delay="0.25"
:mouse-leave-delay="0"
>
<!-- Ok button for question -->
<NcButton
data-testid="nc-survey-form__btn-next"
:class="[
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)'
: '',
]"
@click="goNext()"
>
<div class="flex items-center gap-1">
<Transition name="fade">
<span v-if="!v$.localState[field.title]?.$error" class="uppercase text-white">Ok</span>
</Transition>
<Transition name="slide-right" mode="out-in">
<component
:is="iconMap.closeCircle"
v-if="v$.localState[field.title]?.$error || columnValidationError"
class="text-red-500 md:text-md"
/>
<component :is="iconMap.check" v-else class="text-white md:text-md" />
</Transition>
</div>
</NcButton>
</a-tooltip>
<div class="hidden md:flex text-sm text-gray-500 items-center gap-1">
{{ $t('labels.pressEnter') }} <MaterialSymbolsKeyboardReturn class="text-primary" />
</div>
</div>
</div>
</div>
<div class="rounded-3xl border-1 border-gray-200 p-6 lg:p-12 bg-white">
<h1 class="text-2xl font-bold text-gray-900 mb-4" data-testid="nc-survey-form__heading">
{{ sharedFormView.heading }}
</h1>
<Transition name="slide-left">
<div v-if="submitted" class="flex flex-col justify-center items-center text-center">
<a-alert
class="!my-4 !py-4 !rounded-lg text-left w-full"
class="nc-survey-form__success-msg !p-4 !rounded-lg text-left w-full !bg-white !border-gray-200 !items-start"
type="success"
data-testid="nc-survey-form__success-msg"
outlined
show-icon
>
<template #message>
<LazyCellRichText
@ -405,85 +328,237 @@ onMounted(() => {
<template v-if="!sharedFormView?.success_msg?.trim()" #description>
{{ $t('msg.info.submittedFormData') }}
</template>
<template #icon>
<div>
<GeneralIcon icon="circleCheck2" class="text-[#27D665]"></GeneralIcon>
</div>
</template>
</a-alert>
<div v-if="sharedFormView" class="mt-3 w-full">
<p v-if="sharedFormView?.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-left my-4">
{{ $t('labels.newFormLoaded') }} {{ secondsRemain }} {{ $t('general.seconds') }}
<div
v-if="sharedFormView.show_blank_form || sharedFormView.submit_another_form"
class="mt-16 w-full flex justify-between items-center flex-wrap gap-3"
>
<p v-if="sharedFormView?.show_blank_form" class="text-sm text-gray-500 dark:text-slate-300 m-0">
{{ $t('labels.newFormLoaded') }} {{ secondsRemain }} {{ $t('general.seconds').toLowerCase() }}
</p>
<div v-if="sharedFormView?.submit_another_form" class="text-right">
<NcButton type="primary" size="medium" data-testid="nc-survey-form__btn-submit-another-form" @click="resetForm">
<div class="flex-1 self-end flex justify-end">
<NcButton
v-if="sharedFormView?.submit_another_form"
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit-another-form"
@click="resetForm"
>
{{ $t('activity.submitAnotherForm') }}
</NcButton>
</div>
</div>
</div>
</Transition>
</div>
</Transition>
</div>
<template v-else-if="!isStarted">
<div v-if="sharedFormView.subheading?.trim()">
<LazyCellRichText
:value="sharedFormView.subheading"
class="font-medium text-base text-gray-500 dark:text-slate-300 !h-auto mb-4 -ml-1"
is-form-field
read-only
sync-value-change
data-testid="nc-survey-form__sub-heading"
/>
</div>
<template v-if="!submitted">
<div class="mb-24 md:my-4 select-none text-center text-gray-500 dark:text-slate-200" data-testid="nc-survey-form__footer">
{{ index + 1 }} / {{ formColumns?.length }}
<div class="flex justify-end mt-12">
<div class="flex items-center gap-3">
<div class="hidden md:flex text-sm items-center gap-1 text-gray-800">
<span>
{{ $t('labels.pressEnter') }}
</span>
<NcBadge class="pl-4 pr-1 h-[21px] text-gray-600"> </NcBadge>
</div>
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__fill-form-btn"
@click="onStart()"
>
Fill Form
</NcButton>
</div>
</div>
</template>
</div>
</template>
<template v-if="isStarted && !submitted">
<Transition :name="`slide-${transitionName}`" :duration="transitionDuration" mode="out-in">
<div
ref="el"
:key="field?.title"
class="flex flex-col gap-4 w-full m-auto rounded-xl border-1 border-gray-200 bg-white p-6 lg:p-12"
>
<div class="select-none text-gray-500 mb-4 md:mb-2" data-testid="nc-survey-form__footer">
{{ index + 1 }} / {{ formColumns?.length }}
</div>
<div v-if="field" class="flex flex-col gap-2">
<div class="nc-form-column-label text-sm font-semibold text-gray-800" data-testid="nc-form-column-label">
<span>
{{ field.label || field.title }}
</span>
<span v-if="isRequired(field, field.required)" class="text-red-500 text-base leading-[18px]">&nbsp;*</span>
</div>
<div
v-if="field?.description"
class="nc-form-column-description text-gray-500 text-sm"
data-testid="nc-survey-form__field-description"
>
<LazyCellRichText :value="field?.description" class="!h-auto -ml-1" is-form-field read-only sync-value-change />
</div>
<NcTooltip :disabled="!field?.read_only">
<template #title> {{ $t('activity.preFilledFields.lockedFieldTooltip') }} </template>
<SmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell" @click.stop="handleFocus">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
v-model="formState[field.title]"
class="mt-0 nc-input h-auto"
:class="{
readonly: field?.read_only,
}"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:read-only="field?.read_only"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input h-auto"
:class="{ 'layout-list': parseProp(field?.meta)?.isList, 'readonly': field?.read_only }"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="!field?.read_only"
:read-only="field?.read_only"
/>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }}
</div>
<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') }}
<MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> {{ $t('msg.info.makeLineBreak') }}
</div>
</div>
</SmartsheetDivDataCell>
</NcTooltip>
</div>
<div class="ml-1 mt-4 flex w-full text-lg">
<div class="flex-1 flex justify-end">
<div v-if="isLast && !v$.$invalid">
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
:class="
animationTarget === AnimationTarget.SubmitButton && isAnimating
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
: ''
"
:disabled="v$.localState[field.title]?.$error || columnValidationError"
data-testid="nc-survey-form__btn-submit-confirm"
@click="dialogShow = true"
>
{{ $t('general.submit') }} form
</NcButton>
</div>
<div v-else class="flex items-center gap-3">
<div
class="hidden md:flex text-sm items-center gap-1"
:class="v$.localState[field.title]?.$error || columnValidationError ? 'text-gray-200' : 'text-gray-800'"
>
<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>
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-next"
class="nc-survey-form__btn-next"
:class="[
animationTarget === AnimationTarget.OkButton && isAnimating
? 'transform translate-y-[2px] translate-x-[2px] after:(!ring !ring-accent !ring-opacity-100)'
: '',
]"
:disabled="v$.localState[field.title]?.$error || columnValidationError"
@click="goNext()"
>
{{ $t('labels.next') }}
</NcButton>
</div>
</div>
</div>
</div>
</Transition>
</template>
</div>
</template>
<div class="relative flex w-full items-end">
<Transition name="fade">
<div
v-if="!submitted"
class="color-transition shadow-sm absolute bottom-18 right-1/2 transform translate-x-[50%] md:bottom-4 md:(right-12 transform-none) flex items-center bg-white border dark:bg-slate-500 rounded divide-x-1"
>
<a-tooltip :title="isFirst ? '' : $t('msg.info.goToPrevious')" :mouse-enter-delay="0.25" :mouse-leave-delay="0">
<button
:class="
animationTarget === AnimationTarget.ArrowLeft && isAnimating
? 'transform translate-y-[1px] translate-x-[1px] text-primary'
: ''
"
class="p-0.5 flex items-center group color-transition"
<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-center">
<GeneralFormBranding class="inline-flex mx-auto" />
</div>
<div v-if="isStarted && !submitted" class="flex items-center gap-3">
<NcButton
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__icon-prev"
:disabled="isFirst || v$.localState[field.title]?.$error"
@click="goPrevious()"
>
<component
:is="iconMap.chevronLeft"
:class="isFirst ? 'text-gray-300' : 'group-hover:text-accent'"
class="text-2xl md:text-md"
/>
</button>
</a-tooltip>
<a-tooltip
:title="v$.localState[field.title]?.$error ? '' : $t('msg.info.goToNext')"
:mouse-enter-delay="0.25"
:mouse-leave-delay="0"
>
<button
:class="
animationTarget === AnimationTarget.ArrowRight && isAnimating
? 'transform translate-y-[1px] translate-x-[-1px] text-primary'
: ''
"
class="p-0.5 flex items-center group color-transition"
<GeneralIcon icon="ncArrowLeft"
/></NcButton>
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
type="secondary"
data-testid="nc-survey-form__icon-next"
:disabled="isLast || v$.localState[field.title]?.$error || columnValidationError"
@click="goNext()"
>
<component
:is="iconMap.chevronRight"
:class="[isLast || v$.localState[field.title]?.$error ? 'text-gray-300' : 'group-hover:text-accent']"
class="text-2xl md:text-md"
/>
</button>
</a-tooltip>
<GeneralIcon icon="ncArrowRight" />
</NcButton>
</div>
</div>
</Transition>
<div class="w-full flex justify-center">
<GeneralFormBranding class="inline-flex mx-auto" />
</div>
</div>
<NcModal v-model:visible="dialogShow" size="small" class="nc-survery-form__confirmation_modal">
<div>
<div class="text-lg font-bold">{{ $t('general.submit') }} {{ $t('objects.viewType.form') }}</div>
<div class="mt-1 text-sm">{{ $t('title.surveyFormSubmitConfirmMsg') }}</div>
<div class="flex justify-end mt-7 gap-x-2">
<NcButton type="secondary" :size="isMobileMode ? 'medium' : 'small'" @click="dialogShow = false">{{
$t('general.back')
}}</NcButton>
<NcButton
type="primary"
:size="isMobileMode ? 'medium' : 'small'"
data-testid="nc-survey-form__btn-submit"
@click="submit"
>
{{ $t('general.submit') }}
</NcButton>
</div>
</div>
</NcModal>
</div>
</template>
@ -494,10 +569,6 @@ onMounted(() => {
.survey {
.nc-form-column-label {
> * {
@apply !prose-lg;
}
.nc-icon {
@apply mr-2;
}
@ -514,5 +585,11 @@ onMounted(() => {
@apply !border-none;
}
}
.nc-survey-form__success-msg {
.ant-alert-icon {
@apply flex items-start;
}
}
}
</style>

7
packages/nc-gui/utils/iconUtils.ts

@ -120,6 +120,8 @@ import NcCopy from '~icons/nc-icons/copy'
import NcPaste from '~icons/nc-icons/paste'
import NcArrowUp from '~icons/nc-icons/arrow-up'
import NcArrowDown from '~icons/nc-icons/arrow-down'
import NcArrowLeft from '~icons/nc-icons/arrow-left'
import NcArrowRight from '~icons/nc-icons/arrow-right'
import NcUpload from '~icons/nc-icons/upload'
import NcDownload from '~icons/nc-icons/download'
// import NcProjectGray from '~icons/nc-icons/project-gray'
@ -163,6 +165,8 @@ import NcCellSystemUser from '~icons/nc-icons/system-user'
import NcCellSystemText from '~icons/nc-icons/system-text'
import NcCellAttachment from '~icons/nc-icons/cell-attachment'
import NcCircleCheck from '~icons/nc-icons/circle-check'
// keep it for reference
// todo: remove it after all icons are migrated
/* export const iconMapOld = {
@ -555,11 +559,14 @@ export const iconMap = {
ncEdit: NcEdit,
ncArrowUp: NcArrowUp,
ncArrowDown: NcArrowDown,
ncArrowLeft: NcArrowLeft,
ncArrowRight: NcArrowRight,
underline: NcUnderline,
bold: NcBold,
italic: NcItalic,
phoneCall: NcPhoneCall,
crop: NcCrop,
circleCheck2: NcCircleCheck,
}
export const getMdiIcon = (type: string): any => {

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

@ -5,6 +5,8 @@ import { getTextExcludeIconText } from '../../../tests/utils/general';
export class SurveyFormPage extends BasePage {
readonly formHeading: Locator;
readonly formSubHeading: Locator;
readonly fillFormButton: Locator;
readonly submitConfirmationButton: Locator;
readonly submitButton: Locator;
readonly nextButton: Locator;
readonly nextSlideButton: Locator;
@ -16,7 +18,11 @@ export class SurveyFormPage extends BasePage {
super(rootPage);
this.formHeading = this.get().locator('[data-testid="nc-survey-form__heading"]');
this.formSubHeading = this.get().locator('[data-testid="nc-survey-form__sub-heading"]');
this.submitButton = this.get().locator('[data-testid="nc-survey-form__btn-submit"]');
this.fillFormButton = this.get().locator('[data-testid="nc-survey-form__fill-form-btn"]');
this.submitConfirmationButton = this.get().locator('[data-testid="nc-survey-form__btn-submit-confirm"]');
this.submitButton = this.rootPage.locator(
'.nc-survery-form__confirmation_modal [data-testid="nc-survey-form__btn-submit"]'
);
this.nextButton = this.get().locator('[data-testid="nc-survey-form__btn-next"]');
this.nextSlideButton = this.get().locator('[data-testid="nc-survey-form__icon-next"]');
this.prevSlideButton = this.get().locator('[data-testid="nc-survey-form__icon-prev"]');
@ -28,20 +34,15 @@ export class SurveyFormPage extends BasePage {
return this.rootPage.locator('html >> .nc-form-view');
}
async validate({
heading,
subHeading,
fieldLabel,
footer,
}: {
heading: string;
subHeading: string;
fieldLabel: string;
footer: string;
}) {
async validateHeaders({ heading, subHeading }: { heading: string; subHeading: string }) {
await expect(this.get()).toBeVisible();
await expect(this.formHeading).toHaveText(heading);
await expect(this.formSubHeading).toHaveText(subHeading);
}
async validate({ fieldLabel, footer }: { fieldLabel: string; footer: string }) {
await expect(this.get()).toBeVisible();
await expect(this.formFooter).toHaveText(footer);
const locator = this.get().locator(`[data-testid="nc-form-column-label"]`);
@ -62,7 +63,7 @@ export class SurveyFormPage extends BasePage {
isLastSlide = true;
}
if (isLastSlide) {
await expect(this.submitButton).toBeVisible();
await expect(this.submitConfirmationButton).toBeVisible();
} else {
await expect(this.nextButton).toBeVisible();
}
@ -88,6 +89,8 @@ export class SurveyFormPage extends BasePage {
}
async validateSuccessMessage(param: { message: string; showAnotherForm?: boolean }) {
await this.get().locator('[data-testid="nc-survey-form__success-msg"]').waitFor({ state: 'visible' });
await expect(
this.get().locator(`[data-testid="nc-survey-form__success-msg"]:has-text("${param.message}")`)
).toBeVisible();
@ -96,4 +99,17 @@ export class SurveyFormPage extends BasePage {
await expect(this.get().locator(`button:has-text("Submit Another Form")`)).toBeVisible();
}
}
async clickFillForm() {
await this.fillFormButton.click();
}
async confirmAndSubmit() {
await this.submitConfirmationButton.click();
await this.submitButton.waitFor({ state: 'visible' });
await this.submitButton.click();
await this.submitButton.waitFor({ state: 'hidden' });
}
}

14
tests/playwright/tests/db/views/viewFormShareSurvey.spec.ts

@ -46,9 +46,15 @@ test.describe('Share form', () => {
await dashboard.rootPage.waitForTimeout(2000);
surveyForm = new SurveyFormPage(dashboard.rootPage);
await surveyForm.validate({
await surveyForm.validateHeaders({
heading: 'Country Title',
subHeading: 'Country Form Subtitle',
});
await surveyForm.clickFillForm();
await surveyForm.validate({
fieldLabel: 'Country *',
footer: '1 / 3',
});
@ -59,8 +65,6 @@ test.describe('Share form', () => {
});
await surveyForm.validate({
heading: 'Country Title',
subHeading: 'Country Form Subtitle',
fieldLabel: 'LastUpdate',
footer: '2 / 3',
});
@ -70,12 +74,10 @@ test.describe('Share form', () => {
});
await surveyForm.validate({
heading: 'Country Title',
subHeading: 'Country Form Subtitle',
fieldLabel: 'Cities',
footer: '3 / 3',
});
await surveyForm.submitButton.click();
await surveyForm.confirmAndSubmit();
// validate post submit data
await surveyForm.validateSuccessMessage({

Loading…
Cancel
Save