Browse Source

Merge pull request #7664 from nocodb/nc-feat/form-view-v2

Nc feat/form view v2
pull/7696/head
Raju Udava 9 months ago committed by GitHub
parent
commit
029f2b846f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 20
      packages/nc-gui/assets/img/brand/nocodb-logo.svg
  2. BIN
      packages/nc-gui/assets/img/form-banner-left.png
  3. BIN
      packages/nc-gui/assets/img/form-banner-right.png
  4. 2
      packages/nc-gui/assets/style.scss
  5. 1
      packages/nc-gui/components/cell/Json.vue
  6. 9
      packages/nc-gui/components/cell/TextArea.vue
  7. 2
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  8. 46
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  9. 33
      packages/nc-gui/components/general/ColorPicker.vue
  10. 31
      packages/nc-gui/components/general/FormBanner.vue
  11. 13
      packages/nc-gui/components/general/FormBranding.vue
  12. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  13. 1502
      packages/nc-gui/components/smartsheet/Form.vue
  14. 1
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  15. 2
      packages/nc-gui/components/tabs/Smartsheet.vue
  16. 19
      packages/nc-gui/composables/useSharedFormViewStore.ts
  17. 1
      packages/nc-gui/composables/useViewColumns.ts
  18. 16
      packages/nc-gui/composables/useViewData.ts
  19. 27
      packages/nc-gui/lang/en.json
  20. 1
      packages/nc-gui/layouts/shared-view.vue
  21. 2
      packages/nc-gui/pages/index/[typeOrId]/[baseId]/index.vue
  22. 72
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  23. 145
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/index.vue
  24. 127
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index/survey.vue
  25. 2
      packages/nc-gui/pages/projects/index/list.vue
  26. 1
      packages/nc-gui/utils/iconUtils.ts
  27. 2
      packages/nc-gui/windi.config.ts
  28. 41
      tests/playwright/pages/Dashboard/Form/index.ts
  29. 18
      tests/playwright/tests/db/views/viewForm.spec.ts

20
packages/nc-gui/assets/img/brand/nocodb-logo.svg

@ -0,0 +1,20 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6 10.8676L8.75329 13.6226V17.9842H6V10.8676ZM17.5645 5.01046V17.535C17.5645 17.7921 17.3548 18 17.0977 18C16.9744 18 16.8563 17.9525 16.7683 17.8644L6 8.15303V5.40504C6 5.14785 6.20787 4.94 6.46505 4.94H6.48972C6.61303 4.94 6.7328 4.98933 6.81911 5.07564L14.8094 12.009V5.01046H17.5645Z"
fill="url(#paint0_linear_231_10218)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M3 0C1.34315 0 0 1.34315 0 3V21C0 22.6569 1.34315 24 3 24H21C22.6569 24 24 22.6569 24 21V3C24 1.34315 22.6569 0 21 0H3ZM3.63333 2.13333C2.8049 2.13333 2.13333 2.8049 2.13333 3.63333V20.3667C2.13333 21.1951 2.8049 21.8667 3.63333 21.8667H20.3667C21.1951 21.8667 21.8667 21.1951 21.8667 20.3667V3.63333C21.8667 2.8049 21.1951 2.13333 20.3667 2.13333H3.63333Z"
fill="url(#paint1_linear_231_10218)" />
<defs>
<linearGradient id="paint0_linear_231_10218" x1="11.7814" y1="0.543214" x2="11.7814" y2="21.6612"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
<linearGradient id="paint1_linear_231_10218" x1="4.82267" y1="19.1431" x2="26.7035" y2="-2.6331"
gradientUnits="userSpaceOnUse">
<stop stop-color="#4351E8" />
<stop offset="1" stop-color="#2A1EA5" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
packages/nc-gui/assets/img/form-banner-left.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
packages/nc-gui/assets/img/form-banner-right.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

2
packages/nc-gui/assets/style.scss

@ -613,7 +613,7 @@ a {
} }
.nc-click-transition { .nc-click-transition {
@apply transform transition-transform transition-color !text-gray-400 !hover:(scale-130 !text-gray-500) !active:(scale-100 !text-gray-500); @apply transform transition-transform transition-colors !text-gray-400 !hover:(scale-130 !text-gray-500) !active:(scale-100 !text-gray-500);
} }
.nc-click-transition-1 { .nc-click-transition-1 {

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

@ -185,6 +185,7 @@ watch(isExpanded, () => {
:hide-minimap="true" :hide-minimap="true"
:disable-deep-compare="true" :disable-deep-compare="true"
@update:model-value="localValue = $event" @update:model-value="localValue = $event"
@keydown.enter.stop
/> />
<span v-if="error" class="nc-cell-field text-xs w-full py-1 text-red-500"> <span v-if="error" class="nc-cell-field text-xs w-full py-1 text-red-500">

9
packages/nc-gui/components/cell/TextArea.vue

@ -61,7 +61,7 @@ const mousePosition = ref<
const isDragging = ref(false) const isDragging = ref(false)
const focus: VNodeRef = (el) => const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && isForm.value && (el as HTMLTextAreaElement)?.focus() !isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLTextAreaElement)?.focus()
const height = computed(() => { const height = computed(() => {
if (isExpandedFormOpen.value) return 36 * 4 if (isExpandedFormOpen.value) return 36 * 4
@ -226,7 +226,7 @@ watch(inputWrapperRef, () => {
v-else-if="editEnabled && !isVisible" v-else-if="editEnabled && !isVisible"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
rows="4" :rows="isForm ? 5 : 4"
class="h-full w-full outline-none border-none nc-scrollbar-lg" class="h-full w-full outline-none border-none nc-scrollbar-lg"
:class="{ :class="{
'p-2': editEnabled, 'p-2': editEnabled,
@ -234,7 +234,7 @@ watch(inputWrapperRef, () => {
'px-2': isExpandedFormOpen, 'px-2': isExpandedFormOpen,
}" }"
:style="{ :style="{
minHeight: `${height}px`, minHeight: isForm ? '117px' : `${height}px`,
}" }"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
:disabled="readOnly" :disabled="readOnly"
@ -270,7 +270,7 @@ watch(inputWrapperRef, () => {
<NcTooltip <NcTooltip
v-if="!isVisible" v-if="!isVisible"
placement="bottom" placement="bottom"
class="!absolute right-0 hidden nc-text-area-expand-btn group-hover:block z-3" class="!absolute right-1 hidden nc-text-area-expand-btn group-hover:block z-3"
:class="isExpandedFormOpen || isForm ? 'top-1' : 'bottom-1'" :class="isExpandedFormOpen || isForm ? 'top-1' : 'bottom-1'"
> >
<template #title>{{ $t('title.expand') }}</template> <template #title>{{ $t('title.expand') }}</template>
@ -296,6 +296,7 @@ watch(inputWrapperRef, () => {
:class="{ :class="{
'cursor-move': isDragging, 'cursor-move': isDragging,
}" }"
@keydown.enter.stop
> >
<div <div
v-if="column" v-if="column"

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

@ -235,7 +235,7 @@ watch(isDropdownOpen, async () => {
v-if="isEditing" v-if="isEditing"
:ref="focusInput" :ref="focusInput"
v-model:value="_title" v-model:value="_title"
class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent" class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent !pl-0"
:class="{ :class="{
'font-medium': activeView?.id === vModel.id, 'font-medium': activeView?.id === vModel.id,
}" }"

46
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType, KanbanType, ViewType } from 'nocodb-sdk' import type { ColumnType, KanbanType, ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
import { useMetas } from '#imports' import { useMetas } from '#imports'
const { view: _view, $api } = useSmartsheetStoreOrThrow() const { view: _view, $api } = useSmartsheetStoreOrThrow()
@ -255,22 +254,6 @@ async function updateSharedView() {
return true return true
} }
function onChangeTheme(color: string) {
if (!activeView.value?.meta) return
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
const complement = tcolor.complement()
activeView.value.meta.theme = {
primaryColor: color,
accentColor: complement.toHex8String(),
}
saveTheme()
}
}
const isPublicShared = computed(() => { const isPublicShared = computed(() => {
return !!activeView.value?.uuid return !!activeView.value?.uuid
}) })
@ -351,7 +334,7 @@ const isPublicShared = computed(() => {
> >
</a-switch> </a-switch>
</div> </div>
<div v-if="activeView?.type === ViewTypes.FORM && isEeUI" class="flex flex-row justify-between"> <div v-if="activeView?.type === ViewTypes.FORM && !isEeUI" class="flex flex-row justify-between">
<div class="text-black">{{ $t('activity.rtlOrientation') }}</div> <div class="text-black">{{ $t('activity.rtlOrientation') }}</div>
<a-switch <a-switch
v-model:checked="withRTL" v-model:checked="withRTL"
@ -360,33 +343,6 @@ const isPublicShared = computed(() => {
> >
</a-switch> </a-switch>
</div> </div>
<div v-if="activeView?.type === ViewTypes.FORM" class="flex flex-col justify-between gap-y-1 bg-gray-50 rounded-md">
<div class="flex flex-row justify-between">
<div class="text-black">{{ $t('activity.useTheme') }}</div>
<a-switch
v-e="['c:share:view:theme:toggle']"
:checked="viewTheme"
:loading="isUpdating.password"
class="share-theme-toggle !mt-0.25"
data-testid="share-theme-toggle"
@click="viewTheme = !viewTheme"
/>
</div>
<Transition mode="out-in" name="layout">
<div v-if="viewTheme" class="flex -ml-1">
<LazyGeneralColorPicker
:advanced="false"
:colors="baseThemeColors"
:model-value="activeView?.meta?.theme?.primaryColor"
:row-size="9"
class="!p-0 !bg-inherit"
data-testid="nc-modal-share-view__theme-picker"
@input="onChangeTheme"
/>
</div>
</Transition>
</div>
</div> </div>
</template> </template>
</div> </div>

33
packages/nc-gui/components/general/ColorPicker.vue

@ -7,6 +7,8 @@ interface Props {
rowSize?: number rowSize?: number
advanced?: boolean advanced?: boolean
pickButton?: boolean pickButton?: boolean
borders?: string[]
isNewDesign?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -15,6 +17,7 @@ const props = withDefaults(defineProps<Props>(), {
rowSize: 10, rowSize: 10,
advanced: true, advanced: true,
pickButton: false, pickButton: false,
isNewDesign: false,
}) })
const emit = defineEmits(['input', 'closeModal']) const emit = defineEmits(['input', 'closeModal'])
@ -52,19 +55,21 @@ watch(picked, (n, _o) => {
v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)" v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)"
:key="`color-${colId}-${i}`" :key="`color-${colId}-${i}`"
class="color-selector" class="color-selector"
:class="compare(picked, color) ? 'selected' : ''" :class="{ 'selected': compare(picked, color), 'new-design': isNewDesign }"
:style="{ 'background-color': `${color}` }" :style="{
'background-color': `${color}`,
'border': borders?.length && borders[i] ? `1px solid ${borders[i]}` : undefined,
}"
@click="selectColor(color, true)" @click="selectColor(color, true)"
> >
{{ compare(picked, color) ? '&#10003;' : '' }} {{ compare(picked, color) && !isNewDesign ? '&#10003;' : '' }}
</button> </button>
<button <button class="h-6 w-6 mt-2.7 ml-1 border-1 border-[grey] rounded-md" @click="isPickerOn = !isPickerOn">
class="h-6 w-6 mt-2.7 ml-1 border-1 border-[grey] rounded-md flex items-center justify-center"
@click="isPickerOn = !isPickerOn"
>
<GeneralTooltip> <GeneralTooltip>
<template #title>{{ $t('activity.moreColors') }}</template> <template #title>{{ $t('activity.moreColors') }}</template>
<GeneralIcon class="mt-1.5" :icon="isPickerOn ? 'minus' : 'plus'" /> <div class="flex items-center justify-center">
<GeneralIcon :icon="isPickerOn ? 'minus' : 'plus'" class="w-4 h-4" />
</div>
</GeneralTooltip> </GeneralTooltip>
</button> </button>
</div> </div>
@ -99,14 +104,22 @@ watch(picked, (n, _o) => {
-webkit-text-stroke-width: 1px; -webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white; -webkit-text-stroke-color: white;
} }
.color-selector:hover { .color-selector:not(.new-design):hover {
filter: brightness(90%); filter: brightness(90%);
-webkit-filter: brightness(90%); -webkit-filter: brightness(90%);
} }
.color-selector.selected {
.color-selector.new-design:hover {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
.color-selector.selected:not(.new-design) {
filter: brightness(90%); filter: brightness(90%);
-webkit-filter: brightness(90%); -webkit-filter: brightness(90%);
} }
.color-selector.selected.new-design {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
:deep(.vc-chrome-toggle-icon) { :deep(.vc-chrome-toggle-icon) {
@apply ml-3!important; @apply ml-3!important;
} }

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

@ -0,0 +1,31 @@
<script lang="ts" setup>
interface Props {
bannerImageUrl?: string | null
}
const { bannerImageUrl } = defineProps<Props>()
</script>
<template>
<div
class="w-full max-w-screen-xl mx-auto bg-white border-1 border-gray-200 rounded-3xl overflow-hidden"
:style="
bannerImageUrl
? { 'background-image': `url(${bannerImageUrl})`, 'background-size': 'cover', 'background-position': 'center' }
: {}
"
>
<!-- Todo: aspect ratio and cover image uploader and image cropper to crop image in fixed aspect ratio -->
<div v-if="!bannerImageUrl" class="h-full flex items-stretch justify-between">
<div class="flex">
<img src="~assets/img/form-banner-left.png" alt="form-banner-left'" />
</div>
<div class="w-[91px] flex justify-center">
<img class="max-h-full self-center" src="~assets/img/icons/256x256.png" alt="form-banner-logo'" />
</div>
<div class="flex justify-end">
<img src="~assets/img/form-banner-right.png" alt="form-banner-left'" />
</div>
</div>
</div>
</template>

13
packages/nc-gui/components/general/FormBranding.vue

@ -0,0 +1,13 @@
<script lang="ts" setup>
import { openLink } from '#imports'
</script>
<template>
<div
class="flex items-center gap-3 cursor-pointer text-gray-700 dark:text-slate-300"
@click="openLink('https://github.com/nocodb/nocodb')"
>
<img src="~assets/img/brand/nocodb-logo.svg" alt="NocoDB" class="flex-none w-6 h-6" />
<div class="text-sm">NocoDB Forms</div>
</div>
</template>

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

@ -204,7 +204,7 @@ onUnmounted(() => {
'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen, 'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen,
'h-10': isForm && !isSurveyForm && !isAttachment(column) && !props.virtual, 'h-10': isForm && !isSurveyForm && !isAttachment(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu, 'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30 resize-y': isTextArea(column) && (isForm || isSurveyForm), '!min-h-30': isTextArea(column) && (isForm || isSurveyForm),
}, },
]" ]"
class="nc-cell w-full h-full relative" class="nc-cell w-full h-full relative"

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

File diff suppressed because it is too large Load Diff

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

@ -458,6 +458,7 @@ const onResizeEnd = () => {
document.removeEventListener('mousemove', onResize) document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', onResizeEnd) document.removeEventListener('mouseup', onResizeEnd)
} }
const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: Row) => { const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return if (!isUIAllowed('dataEdit')) return
resizeInProgress.value = true resizeInProgress.value = true

2
packages/nc-gui/components/tabs/Smartsheet.vue

@ -171,7 +171,7 @@ watch([activeViewTitleOrId, activeTableId], () => {
<div style="height: calc(100% - var(--topbar-height))"> <div style="height: calc(100% - var(--topbar-height))">
<div v-if="openedViewsTab === 'view'" class="flex flex-col h-full flex-1 min-w-0"> <div v-if="openedViewsTab === 'view'" class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar v-if="!isForm" /> <LazySmartsheetToolbar v-if="!isForm" />
<div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))"> <div class="flex flex-row w-full" :style="{ height: isForm ? '100%' : 'calc(100% - var(--topbar-height))' }">
<Transition name="layout" mode="out-in"> <Transition name="layout" mode="out-in">
<div v-if="openedViewsTab === 'view'" class="flex flex-1 min-h-0 w-3/4"> <div v-if="openedViewsTab === 'view'" class="flex flex-1 min-h-0 w-3/4">
<div class="h-full flex-1 min-w-0 min-h-0 bg-white"> <div class="h-full flex-1 min-w-0 min-h-0 bg-white">

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

@ -76,7 +76,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
}), }),
) )
const fieldRequired = (fieldName = 'Value') => helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required) const fieldRequired = (fieldName = 'This field') =>
helpers.withMessage(t('msg.error.fieldRequired', { value: fieldName }), required)
const formColumns = computed(() => const formColumns = computed(() =>
columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || isLinksOrLTAR(col.uidt)), columns.value?.filter((c) => c.show).filter((col) => !isVirtualCol(col) || isLinksOrLTAR(col.uidt)),
@ -158,7 +159,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
!isVirtualCol(column) && !isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required) ((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)
) { ) {
obj.localState[column.title!] = { required: fieldRequired(column.label || column.title) } obj.localState[column.title!] = { required: fieldRequired() }
} else if ( } else if (
isLinksOrLTAR(column) && isLinksOrLTAR(column) &&
column.colOptions && column.colOptions &&
@ -168,13 +169,13 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
if ((col && col.rqd && !col.cdf) || column.required) { if ((col && col.rqd && !col.cdf) || column.required) {
if (col) { if (col) {
obj.virtual[column.title!] = { required: fieldRequired(column.label || column.title) } obj.virtual[column.title!] = { required: fieldRequired() }
} }
} }
} else if (isVirtualCol(column) && column.required) { } else if (isVirtualCol(column) && column.required) {
obj.virtual[column.title!] = { obj.virtual[column.title!] = {
minLength: minLength(1), minLength: minLength(1),
required: fieldRequired(column.label || column.title), required: fieldRequired(),
} }
} }
} }
@ -190,6 +191,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const submitForm = async () => { const submitForm = async () => {
try { try {
if (!(await v$.value?.$validate())) { if (!(await v$.value?.$validate())) {
message.error(t('msg.error.someOfTheRequiredFieldsAreEmpty'))
return return
} }
@ -226,6 +228,14 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
progress.value = false progress.value = false
} }
const clearForm = async () => {
formResetHook.trigger()
additionalState.value = {}
formState.value = {}
v$.value?.$reset()
}
/** reset form if show_blank_form is true */ /** reset form if show_blank_form is true */
watch(submitted, (nextVal) => { watch(submitted, (nextVal) => {
if (nextVal && sharedFormView.value?.show_blank_form) { if (nextVal && sharedFormView.value?.show_blank_form) {
@ -261,6 +271,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
loadSharedView, loadSharedView,
columns, columns,
submitForm, submitForm,
clearForm,
progress, progress,
meta, meta,
validators, validators,

1
packages/nc-gui/composables/useViewColumns.ts

@ -54,7 +54,6 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
const loadViewColumns = async () => { const loadViewColumns = async () => {
if (!meta || !view) return if (!meta || !view) return
let order = 1 let order = 1
if (view.value?.id) { if (view.value?.id) {

16
packages/nc-gui/composables/useViewData.ts

@ -278,16 +278,16 @@ export function useViewData(
if (!viewMeta?.value?.id) return if (!viewMeta?.value?.id) return
try { try {
const { columns, ...view } = await $api.dbView.formRead(viewMeta.value.id) const { columns, ...view } = await $api.dbView.formRead(viewMeta.value.id)
let order = 1
const fieldById = (columns || []).reduce( const fieldById = (columns || []).reduce((o: Record<string, any>, f: Record<string, any>) => {
(o: Record<string, any>, f: Record<string, any>) => ({ if (order < f.order) {
order = f.order
}
return {
...o, ...o,
[f.fk_column_id]: f, [f.fk_column_id]: f,
}), }
{} as Record<string, FormColumnType>, }, {} as Record<string, FormColumnType>)
)
let order = 1
formViewData.value = view formViewData.value = view

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

@ -421,7 +421,8 @@
"restrict": "Restrict", "restrict": "Restrict",
"setNull": "Set NULL", "setNull": "Set NULL",
"setDefault": "Set Default" "setDefault": "Set Default"
} },
"selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here"
}, },
"labels": { "labels": {
"selectYear": "Select Year", "selectYear": "Select Year",
@ -683,7 +684,12 @@
"incompleteConfiguration": "Incomplete configuration", "incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field", "selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list" "selectFieldLabel": "Make changes to field properties by selecting a field from the list"
} },
"appearanceSettings":"Appearance Settings",
"backgroundColor":"Background Color",
"hideNocodbBranding":"Hide NocoDB Branding",
"showOnConditions": "Show on condtions",
"showFieldOnConditionsMet":"Shows field only when conditions are met"
}, },
"activity": { "activity": {
"goToToday": "Go to Today", "goToToday": "Go to Today",
@ -907,7 +913,10 @@
"openInOpenStreetMap": "OSM" "openInOpenStreetMap": "OSM"
}, },
"toggleMobileMode": "Toggle Mobile Mode", "toggleMobileMode": "Toggle Mobile Mode",
"startCommenting": "Start commenting!" "startCommenting": "Start commenting!",
"clearForm": "Clear Form",
"addFieldFromFormView": "Add Field",
"selectAllFields": "Select all fields"
}, },
"tooltip": { "tooltip": {
"reachedSourceLimit": "Limited to only one data source for the moment", "reachedSourceLimit": "Limited to only one data source for the moment",
@ -1166,14 +1175,14 @@
"afterEnablePwd": "Access is password restricted", "afterEnablePwd": "Access is password restricted",
"privateLink": "This view is shared via a private link", "privateLink": "This view is shared via a private link",
"privateLinkAdditionalInfo": "People with private link can only see cells visible in this view", "privateLinkAdditionalInfo": "People with private link can only see cells visible in this view",
"afterFormSubmitted": "After form is submitted", "postFormSubmissionSettings": "Post Form Submission Settings",
"apiOptions": "Access Base via", "apiOptions": "Access Base via",
"submitAnotherForm": "Show 'Submit Another Form' button", "submitAnotherForm": "Show 'Submit Another Form' button",
"showBlankForm": "Show a blank form after 5 seconds", "showBlankForm": "Show a blank form after 5 seconds",
"emailForm": "E-mail me at", "emailForm": "E-mail me at",
"showSysFields": "Show system fields", "showSysFields": "Show system fields",
"filterAutoApply": "Auto apply", "filterAutoApply": "Auto apply",
"showMessage": "Show this message", "formDisplayMessage": "Display Message",
"viewNotShared": "Current view is not shared!", "viewNotShared": "Current view is not shared!",
"showAllViews": "Show all shared views of this table", "showAllViews": "Show all shared views of this table",
"collabView": "Collaborators with edit permissions or higher can change the view configuration.", "collabView": "Collaborators with edit permissions or higher can change the view configuration.",
@ -1283,7 +1292,8 @@
"editingSystemKeyNotSupported": "Editing system key not supported", "editingSystemKeyNotSupported": "Editing system key not supported",
"notAvailableAtTheMoment": "Not available at the moment", "notAvailableAtTheMoment": "Not available at the moment",
"groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column", "groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column",
"groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column" "groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column",
"upgradeToEnterpriseEdition": "Upgrade to Enterprise Edition {extraInfo}"
}, },
"error": { "error": {
"scopesRequired": "Scopes required", "scopesRequired": "Scopes required",
@ -1369,14 +1379,15 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots", "theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.", "fieldRequired": "{value} cannot be empty",
"projectNotAccessible": "Base not accessible", "projectNotAccessible": "Base not accessible",
"copyToClipboardError": "Failed to copy to clipboard", "copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard", "pasteFromClipboardError": "Failed to paste from clipboard",
"multiFieldSaveValidation": "Please complete the configuration of all fields before saving", "multiFieldSaveValidation": "Please complete the configuration of all fields before saving",
"somethingWentWrong": "Something went wrong", "somethingWentWrong": "Something went wrong",
"draggedContentIsNotTypeOfImage": "Dragged content is not type of image", "draggedContentIsNotTypeOfImage": "Dragged content is not type of image",
"fieldToParseImageData": "Field to parse image data" "fieldToParseImageData": "Field to parse image data",
"someOfTheRequiredFieldsAreEmpty": "Some of the required fields are empty"
}, },
"toast": { "toast": {
"exportMetadata": "Base metadata exported successfully", "exportMetadata": "Base metadata exported successfully",

1
packages/nc-gui/layouts/shared-view.vue

@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { iconMap, onMounted, useEventListener, useGlobal, useRouter, useSharedView } from '#imports' import { iconMap, onMounted, useEventListener, useGlobal, useRouter, useSharedView } from '#imports'
const { isLoading, appInfo } = useGlobal() const { isLoading, appInfo } = useGlobal()

2
packages/nc-gui/pages/index/[typeOrId]/[baseId]/index.vue

@ -133,7 +133,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
.nc-left-sidebar { .nc-left-sidebar {
.nc-sidebar-left-toggle-icon { .nc-sidebar-left-toggle-icon {
@apply opacity-0 transition-opactity duration-200 transition-color text-gray-500/80 hover:text-gray-500/100; @apply opacity-0 transition-opacity duration-200 transition-colors text-gray-500/80 hover:text-gray-500/100;
.nc-left-sidebar { .nc-left-sidebar {
@apply !border-r-0; @apply !border-r-0;

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

@ -1,27 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { navigateTo, useDark, useRoute, useRouter, useSharedFormStoreOrThrow, useTheme, watch } from '#imports' import { navigateTo, useDark, useRoute, useRouter, useSharedFormStoreOrThrow } from '#imports'
const { sharedViewMeta } = useSharedFormStoreOrThrow() const { sharedViewMeta, sharedFormView } = useSharedFormStoreOrThrow()
const isDark = useDark() const isDark = useDark()
const { setTheme } = useTheme()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
watch( // For now dark theme is disabled
() => sharedViewMeta.value.withTheme, // const onClick = () => {
(hasTheme) => { // isDark.value = !isDark.value
if (hasTheme && sharedViewMeta.value.theme) setTheme(sharedViewMeta.value.theme) // }
},
{ immediate: true },
)
const onClick = () => { onMounted(() => {
isDark.value = !isDark.value isDark.value = false
} })
const shouldRedirect = (to: string) => { const shouldRedirect = (to: string) => {
if (sharedViewMeta.value.surveyMode) { if (sharedViewMeta.value.surveyMode) {
@ -38,11 +33,14 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<template> <template>
<div <div
class="scrollbar-thin-dull overflow-y-auto overflow-x-hidden flex flex-col color-transition nc-form-view relative bg-primary bg-opacity-10 dark:(bg-slate-900) h-[100vh] min-h-[600px] py-4" 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]"
:style="{
background: parseProp(sharedFormView?.meta)?.background_color || '#F9F9FA',
}"
> >
<NuxtPage /> <NuxtPage />
<div <!-- <div
class="color-transition flex items-center justify-center cursor-pointer absolute top-4 md:top-15 right-4 md:right-15 rounded-full p-2 bg-white dark:(bg-slate-600) shadow hover:(ring-1 ring-accent ring-opacity-100)" class="color-transition flex items-center justify-center cursor-pointer absolute top-4 md:top-15 right-4 md:right-15 rounded-full p-2 bg-white dark:(bg-slate-600) shadow hover:(ring-1 ring-accent ring-opacity-100)"
@click="onClick" @click="onClick"
> >
@ -50,7 +48,7 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<MaterialSymbolsDarkModeOutline v-if="isDark" /> <MaterialSymbolsDarkModeOutline v-if="isDark" />
<MaterialSymbolsLightModeOutline v-else /> <MaterialSymbolsLightModeOutline v-else />
</Transition> </Transition>
</div> </div> -->
</div> </div>
</template> </template>
@ -68,10 +66,6 @@ p {
} }
.nc-form-view { .nc-form-view {
.nc-data-cell {
@apply border-solid border-1 !border-gray-300 dark:!border-slate-200;
}
.nc-cell { .nc-cell {
@apply bg-white dark:bg-slate-500; @apply bg-white dark:bg-slate-500;
@ -95,15 +89,7 @@ p {
@apply bg-white dark:bg-slate-500; @apply bg-white dark:bg-slate-500;
&.nc-input { &.nc-input {
@apply w-full px-3 min-h-[40px] flex items-center; @apply w-full rounded p-2 min-h-[40px] flex items-center;
&.nc-cell-longtext {
@apply !px-1;
}
&.nc-cell-json {
@apply !h-auto;
}
.duration-cell-wrapper { .duration-cell-wrapper {
@apply w-full; @apply w-full;
@ -132,11 +118,37 @@ p {
} }
} }
&.nc-cell-longtext {
@apply p-0 h-auto;
& > div {
@apply w-full;
}
:deep(textarea) {
@apply !p-2;
&:focus {
box-shadow: none !important;
}
}
}
&:not(.nc-cell-longtext) {
@apply px-2 py-2;
}
textarea { textarea {
@apply px-4 py-2 rounded;
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
} }
} }
&.nc-cell-json {
@apply h-auto;
& > div {
@apply w-full;
}
}
} }
} }

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

@ -3,10 +3,21 @@ import type { ColumnType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { ref } from 'vue' import { ref } from 'vue'
import { StreamBarcodeReader } from 'vue-barcode-reader' import { StreamBarcodeReader } from 'vue-barcode-reader'
import { iconMap, useGlobal, useSharedFormStoreOrThrow } from '#imports' import { iconMap, useSharedFormStoreOrThrow } from '#imports'
const { sharedFormView, submitForm, v$, formState, notFound, formColumns, submitted, secondsRemain, isLoading } = const {
useSharedFormStoreOrThrow() sharedFormView,
submitForm,
clearForm,
v$,
formState,
notFound,
formColumns,
submitted,
secondsRemain,
isLoading,
progress,
} = useSharedFormStoreOrThrow()
function isRequired(_columnObj: Record<string, any>, required = false) { function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj let columnObj = _columnObj
@ -21,8 +32,6 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
return !!(required || (columnObj && columnObj.rqd && !columnObj.cdf)) return !!(required || (columnObj && columnObj.rqd && !columnObj.cdf))
} }
const { isMobileMode } = useGlobal()
const fieldTitleForCurrentScan = ref('') const fieldTitleForCurrentScan = ref('')
const scannerIsReady = ref(false) const scannerIsReady = ref(false)
@ -70,32 +79,37 @@ const onDecode = async (scannedCodeValue: string) => {
</script> </script>
<template> <template>
<div class="h-full flex flex-col items-center" :class="isMobileMode ? 'mobile' : 'desktop'"> <div class="h-full flex flex-col items-center">
<GeneralFormBanner
v-if="sharedFormView"
:banner-image-url="sharedFormView.banner_image_url"
class="flex-none dark:border-none"
/>
<div <div
class="color-transition flex flex-col justify-center gap-2 w-full max-w-[max(33%,600px)] m-auto py-4 pb-8 px-16 md:(bg-white dark:bg-slate-700 rounded-lg border-1 border-gray-200 shadow-xl)" class="transition-all duration-300 ease-in relative flex flex-col justify-center gap-2 w-full max-w-[max(33%,688px)] mx-auto my-6 bg-white dark:bg-transparent rounded-3xl border-1 border-gray-200 px-4 py-8 lg:p-12 md:(p-8 dark:bg-slate-700)"
> >
<template v-if="sharedFormView"> <template v-if="sharedFormView">
<h1 class="prose-2xl font-bold self-center my-4 break-words"> <div class="mb-4">
{{ sharedFormView.heading }} <h1 class="text-2xl font-bold text-gray-900 mb-4">
</h1> {{ sharedFormView.heading }}
</h1>
<h2 <h2 v-if="sharedFormView.subheading" class="font-medium text-base text-gray-500 dark:text-slate-300 mb-4">
v-if="sharedFormView.subheading" {{ sharedFormView.subheading }}
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6 break-words" </h2>
> </div>
{{ sharedFormView.subheading }}
</h2>
<a-alert v-if="notFound" type="warning" class="my-4 text-center" :message="$t('general.notFound')" /> <a-alert v-if="notFound" type="warning" class="my-4 text-center" message="Not found" />
<template v-else-if="submitted"> <template v-else-if="submitted">
<div class="flex justify-center"> <div class="flex justify-center">
<div v-if="sharedFormView" class="min-w-350px mt-3"> <div v-if="sharedFormView" class="w-full lg:w-[95%]">
<a-alert <a-alert
type="success" type="success"
class="my-4 text-center" class="!my-4 text-center !rounded-lg"
outlined outlined
:message="sharedFormView.success_msg || $t('msg.successfullySubmittedFormData')" :message="sharedFormView.success_msg || 'Successfully submitted form data'"
/> />
<p v-if="sharedFormView.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4"> <p v-if="sharedFormView.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4">
@ -103,7 +117,9 @@ const onDecode = async (scannedCodeValue: string) => {
</p> </p>
<div v-if="sharedFormView.submit_another_form" class="text-center"> <div v-if="sharedFormView.submit_another_form" class="text-center">
<a-button type="primary" @click="submitted = false"> {{ $t('activity.submitAnotherForm') }}</a-button> <NcButton type="primary" size="medium" @click="submitted = false">
{{ $t('activity.submitAnotherForm') }}</NcButton
>
</div> </div>
</div> </div>
</div> </div>
@ -124,7 +140,7 @@ const onDecode = async (scannedCodeValue: string) => {
<StreamBarcodeReader v-show="scannerIsReady" @decode="onDecode" @loaded="onLoaded"> </StreamBarcodeReader> <StreamBarcodeReader v-show="scannerIsReady" @decode="onDecode" @loaded="onLoaded"> </StreamBarcodeReader>
</div> </div>
</a-modal> </a-modal>
<GeneralOverlay class="bg-gray-400/75" :model-value="isLoading" inline transition> <GeneralOverlay class="bg-gray-50/75 rounded-3xl" :model-value="isLoading" inline transition>
<div class="w-full h-full flex items-center justify-center"> <div class="w-full h-full flex items-center justify-center">
<a-spin size="large" /> <a-spin size="large" />
</div> </div>
@ -132,22 +148,16 @@ const onDecode = async (scannedCodeValue: string) => {
<div class="nc-form-wrapper"> <div class="nc-form-wrapper">
<div class="nc-form h-full"> <div class="nc-form h-full">
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-3 md:gap-6">
<div v-for="(field, index) in formColumns" :key="index" class="flex flex-col gap-2"> <div v-for="(field, index) in formColumns" :key="index" class="flex flex-col gap-2">
<div class="flex nc-form-column-label"> <div class="nc-form-column-label text-sm font-semibold text-gray-800">
<LazySmartsheetHeaderVirtualCell <span>
v-if="isVirtualCol(field)" {{ field.label || field.title }}
:column="{ ...field, title: field.label || field.title }" </span>
:required="isRequired(field, field.required)" <span v-if="isRequired(field, field.required)" class="text-red-500 text-base leading-[18px]">&nbsp;*</span>
:hide-menu="true" </div>
/> <div v-if="field?.description" class="nc-form-column-description text-gray-500 text-sm">
{{ field?.description }}
<LazySmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
</div> </div>
<div> <div>
@ -182,34 +192,59 @@ const onDecode = async (scannedCodeValue: string) => {
</a-button> </a-button>
</LazySmartsheetDivDataCell> </LazySmartsheetDivDataCell>
<div <div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-sm mt-2">
class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1 leading-[18px]" <template v-if="isVirtualCol(field)">
style="word-break: break-word" <div v-for="error of v$.virtual[field.title]?.$errors" :key="`${error}virtual`" class="text-red-500">
> {{ error.$message }}
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500"> </div>
{{ error.$message }} </template>
</div> <template v-else>
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ field.description }} {{ error.$message }}
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center mt-4"> <div class="flex justify-between items-center mt-6">
<NcButton type="primary" html-type="submit" data-testid="shared-form-submit-button" @click="submitForm"> <NcButton
html-type="reset"
type="secondary"
size="small"
:disabled="isLoading"
class="nc-shared-form-button shared-form-clear-button"
data-testid="shared-form-clear-button"
@click="clearForm"
>
{{ $t('activity.clearForm') }}
</NcButton>
<NcButton
html-type="submit"
:disabled="progress"
type="primary"
size="small"
class="nc-shared-form-button shared-form-submit-button"
data-testid="shared-form-submit-button"
@click="submitForm"
>
{{ $t('general.submit') }} {{ $t('general.submit') }}
</NcButton> </NcButton>
</div> </div>
</div> </div>
</div> </div>
<div>
<a-divider class="!my-6 !md:my-8" />
<div class="inline-block">
<GeneralFormBranding />
</div>
</div>
</template> </template>
</template> </template>
</div> </div>
<div>&nbsp;</div>
<div class="flex items-end">
<GeneralPoweredBy />
</div>
</div> </div>
</template> </template>
@ -221,4 +256,10 @@ const onDecode = async (scannedCodeValue: string) => {
@apply h-auto; @apply h-auto;
@apply ml-1; @apply ml-1;
} }
.nc-shared-form-button {
&.nc-button.ant-btn:focus {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
}
</style> </style>

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

@ -242,13 +242,13 @@ onMounted(() => {
class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end" class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end"
> >
<div class="px-4 md:px-0 flex flex-col justify-end"> <div class="px-4 md:px-0 flex flex-col justify-end">
<h1 class="prose-2xl font-bold self-center my-4" data-testid="nc-survey-form__heading"> <h1 class="text-2xl font-bold text-gray-900 self-center my-4" data-testid="nc-survey-form__heading">
{{ sharedFormView.heading }} {{ sharedFormView.heading }}
</h1> </h1>
<h2 <h2
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''" v-if="sharedFormView.subheading && sharedFormView.subheading !== ''"
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6" class="font-medium text-base text-gray-500 dark:text-gray-300 self-center mb-4"
data-testid="nc-survey-form__sub-heading" data-testid="nc-survey-form__sub-heading"
> >
{{ sharedFormView?.subheading }} {{ sharedFormView?.subheading }}
@ -264,24 +264,21 @@ onMounted(() => {
class="color-transition h-full flex flex-col mt-6 gap-4 w-full max-w-[max(33%,600px)] m-auto" 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 v-if="field && !submitted" class="flex flex-col gap-2">
<div class="flex nc-form-column-label" data-testid="nc-form-column-label"> <div class="nc-form-column-label text-sm font-semibold text-gray-800" data-testid="nc-form-column-label">
<LazySmartsheetHeaderVirtualCell <span>
v-if="isVirtualCol(field)" {{ field.label || field.title }}
:column="{ ...field, title: field.label || field.title }" </span>
:required="isRequired(field, field.required)" <span v-if="isRequired(field, field.required)" class="text-red-500 text-base leading-[18px]">&nbsp;*</span>
:hide-menu="true" </div>
/> <div
v-if="field?.description"
<LazySmartsheetHeaderCell class="nc-form-column-description text-gray-500 text-sm"
v-else data-testid="nc-survey-form__field-description"
:class="field.uidt === UITypes.Checkbox ? 'nc-form-column-label__checkbox' : ''" >
:column="{ meta: {}, ...field, title: field.label || field.title }" {{ field?.description }}
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
</div> </div>
<LazySmartsheetDivDataCell v-if="field.title" class="relative"> <LazySmartsheetDivDataCell v-if="field.title" class="relative nc-form-data-cell">
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(field)" v-if="isVirtualCol(field)"
v-model="formState[field.title]" v-model="formState[field.title]"
@ -304,13 +301,6 @@ onMounted(() => {
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500"> <div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
{{ error.$message }} {{ error.$message }}
</div> </div>
<div
class="block text-[14px]"
:class="field.uidt === UITypes.Checkbox ? 'text-center' : ''"
data-testid="nc-survey-form__field-description"
>
{{ field.description }}
</div>
<div v-if="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center"> <div v-if="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center">
{{ $t('general.shift') }} <MdiAppleKeyboardShift class="mx-1 text-primary" /> + {{ $t('general.enter') }} {{ $t('general.shift') }} <MdiAppleKeyboardShift class="mx-1 text-primary" /> + {{ $t('general.enter') }}
@ -323,19 +313,18 @@ onMounted(() => {
<div class="ml-1 mt-4 flex w-full text-lg"> <div class="ml-1 mt-4 flex w-full text-lg">
<div class="flex-1 flex justify-center"> <div class="flex-1 flex justify-center">
<div v-if="isLast && !submitted && !v$.$invalid" class="text-center my-4"> <div v-if="isLast && !submitted && !v$.$invalid" class="text-center my-4">
<button <NcButton
:class=" :class="
animationTarget === AnimationTarget.SubmitButton && isAnimating animationTarget === AnimationTarget.SubmitButton && isAnimating
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100' ? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
: '' : ''
" "
type="submit" html-type="submit"
class="uppercase scaling-btn prose-sm"
data-testid="nc-survey-form__btn-submit" data-testid="nc-survey-form__btn-submit"
@click="submit" @click="submit"
> >
{{ $t('general.submit') }} {{ $t('general.submit') }}
</button> </NcButton>
</div> </div>
<div v-else-if="!submitted" class="flex items-center gap-3 flex-col"> <div v-else-if="!submitted" class="flex items-center gap-3 flex-col">
@ -357,18 +346,20 @@ onMounted(() => {
]" ]"
@click="goNext()" @click="goNext()"
> >
<Transition name="fade"> <div class="flex items-center gap-1">
<span v-if="!v$.localState[field.title]?.$error" class="uppercase text-white">{{ $t('general.ok') }}</span> <Transition name="fade">
</Transition> <span v-if="!v$.localState[field.title]?.$error" class="uppercase text-white">Ok</span>
</Transition>
<Transition name="slide-right" mode="out-in">
<component <Transition name="slide-right" mode="out-in">
:is="iconMap.closeCircle" <component
v-if="v$.localState[field.title]?.$error || columnValidationError" :is="iconMap.closeCircle"
class="text-red-500 md:text-md" 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> <component :is="iconMap.check" v-else class="text-white md:text-md" />
</Transition>
</div>
</NcButton> </NcButton>
</a-tooltip> </a-tooltip>
@ -381,19 +372,14 @@ onMounted(() => {
<Transition name="slide-left"> <Transition name="slide-left">
<div v-if="submitted" class="flex flex-col justify-center items-center text-center"> <div v-if="submitted" class="flex flex-col justify-center items-center text-center">
<div class="text-lg px-6 py-3 bg-green-300 text-gray-700 rounded" data-testid="nc-survey-form__success-msg"> <a-alert
<template v-if="sharedFormView?.success_msg"> type="success"
{{ sharedFormView?.success_msg }} class="!my-4 !py-4 text-center !rounded-lg"
</template> data-testid="nc-survey-form__success-msg"
outlined
<template v-else> :message="sharedFormView?.success_msg || $t('msg.info.thankYou')"
<div class="flex flex-col gap-1"> :description="sharedFormView?.success_msg ? undefined : $t('msg.info.submittedFormData')"
<div>{{ $t('msg.info.thankYou') }}</div> />
<div>{{ $t('msg.info.submittedFormData') }}</div>
</div>
</template>
</div>
<div v-if="sharedFormView" class="mt-3"> <div v-if="sharedFormView" class="mt-3">
<p v-if="sharedFormView?.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4"> <p v-if="sharedFormView?.show_blank_form" class="text-xs text-slate-500 dark:text-slate-300 text-center my-4">
@ -401,7 +387,7 @@ onMounted(() => {
</p> </p>
<div v-if="sharedFormView?.submit_another_form" class="text-center"> <div v-if="sharedFormView?.submit_another_form" class="text-center">
<NcButton type="primary" data-testid="nc-survey-form__btn-submit-another-form" @click="resetForm"> <NcButton type="primary" size="medium" data-testid="nc-survey-form__btn-submit-another-form" @click="resetForm">
{{ $t('activity.submitAnotherForm') }} {{ $t('activity.submitAnotherForm') }}
</NcButton> </NcButton>
</div> </div>
@ -468,7 +454,9 @@ onMounted(() => {
</div> </div>
</Transition> </Transition>
<GeneralPoweredBy /> <div class="w-full flex justify-center">
<GeneralFormBranding class="inline-flex mx-auto" />
</div>
</div> </div>
</div> </div>
</template> </template>
@ -494,16 +482,37 @@ onMounted(() => {
} }
.nc-input { .nc-input {
@apply appearance-none w-full rounded px-2 py-2 my-2 border-solid border-1 border-primary border-opacity-50; @apply appearance-none w-full !bg-white !rounded-lg border-solid border-1 border-gray-200 focus-within:border-brand-500;
&.nc-cell-rating,
&.nc-cell-geodata {
@apply !py-1;
}
:deep(input) {
@apply !px-1;
}
&.nc-cell-longtext {
@apply p-0 h-auto overflow-hidden;
}
&:not(.nc-cell-longtext) {
@apply px-2 py-2;
:deep(textarea) {
@apply !p-2;
}
}
&.nc-cell-checkbox { &.nc-cell-checkbox {
> * { > * {
@apply justify-center flex items-center; @apply justify-center flex items-center;
} }
} }
}
.nc-form-data-cell.nc-data-cell {
@apply !border-none rounded-none;
input { &:focus-within {
@apply !py-1 !px-1; @apply !border-none;
} }
} }
} }

2
packages/nc-gui/pages/projects/index/list.vue

@ -30,7 +30,7 @@ const openProject = async (base: BaseType) => {
<template v-for="base of bases" :key="base.id"> <template v-for="base of bases" :key="base.id">
<div <div
class="cursor-pointer grid grid-cols-3 gap-2 prose-md hover:(bg-gray-300/30) p-2 transition-color ease-in duration-100" class="cursor-pointer grid grid-cols-3 gap-2 prose-md hover:(bg-gray-300/30) p-2 transition-colors ease-in duration-100"
@click="openProject(base)" @click="openProject(base)"
> >
<div class="font-semibold capitalize">{{ base.title || 'Untitled' }}</div> <div class="font-semibold capitalize">{{ base.title || 'Untitled' }}</div>

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

@ -121,6 +121,7 @@ import NcArrowUp from '~icons/nc-icons/arrow-up'
import NcArrowDown from '~icons/nc-icons/arrow-down' import NcArrowDown from '~icons/nc-icons/arrow-down'
import NcUpload from '~icons/nc-icons/upload' import NcUpload from '~icons/nc-icons/upload'
import NcDownload from '~icons/nc-icons/download' import NcDownload from '~icons/nc-icons/download'
// import NcProjectGray from '~icons/nc-icons/project-gray'
import NcPhoneCall from '~icons/nc-icons/phone-call' import NcPhoneCall from '~icons/nc-icons/phone-call'
// keep it for reference // keep it for reference

2
packages/nc-gui/windi.config.ts

@ -43,7 +43,7 @@ export default defineConfig({
}, },
shortcuts: { shortcuts: {
'color-transition': 'transition-color duration-100 ease-in', 'color-transition': 'transition-colors duration-100 ease-in',
'scrollbar-thin-primary': 'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white', 'scrollbar-thin-primary': 'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white',
'scrollbar-thin-dull': 'scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-100 scrollbar-track-white', 'scrollbar-thin-dull': 'scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-100 scrollbar-track-white',
}, },

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

@ -22,14 +22,16 @@ export class FormPage extends BasePage {
readonly formSubHeading: Locator; readonly formSubHeading: Locator;
readonly afterSubmitMsg: Locator; readonly afterSubmitMsg: Locator;
readonly formFields: Locator;
constructor(dashboard: DashboardPage) { constructor(dashboard: DashboardPage) {
super(dashboard.rootPage); super(dashboard.rootPage);
this.dashboard = dashboard; this.dashboard = dashboard;
this.toolbar = new ToolbarPage(this); this.toolbar = new ToolbarPage(this);
this.topbar = new TopbarPage(this); this.topbar = new TopbarPage(this);
this.addAllButton = dashboard.get().locator('[data-testid="nc-form-add-all"]'); this.addAllButton = dashboard.get().locator('[data-testid="nc-form-show-all-fields"]').locator('.nc-switch');
this.removeAllButton = dashboard.get().locator('[data-testid="nc-form-remove-all"]'); this.removeAllButton = dashboard.get().locator('[data-testid="nc-form-show-all-fields"]').locator('.nc-switch');
this.submitButton = dashboard.get().locator('[data-testid="nc-form-submit"]'); this.submitButton = dashboard.get().locator('[data-testid="nc-form-submit"]');
this.showAnotherFormRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-submit-another-form"]'); this.showAnotherFormRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-submit-another-form"]');
@ -40,6 +42,8 @@ export class FormPage extends BasePage {
this.formHeading = dashboard.get().locator('[data-testid="nc-form-heading"]'); this.formHeading = dashboard.get().locator('[data-testid="nc-form-heading"]');
this.formSubHeading = dashboard.get().locator('[data-testid="nc-form-sub-heading"]'); this.formSubHeading = dashboard.get().locator('[data-testid="nc-form-sub-heading"]');
this.afterSubmitMsg = dashboard.get().locator('[data-testid="nc-form-after-submit-msg"]'); this.afterSubmitMsg = dashboard.get().locator('[data-testid="nc-form-after-submit-msg"]');
this.formFields = dashboard.get().locator('.nc-form-fields-list');
} }
get() { get() {
@ -50,32 +54,20 @@ export class FormPage extends BasePage {
return this.dashboard.get().locator('[data-testid="nc-form-wrapper-submit"]'); return this.dashboard.get().locator('[data-testid="nc-form-wrapper-submit"]');
} }
getFormHiddenColumn() {
return this.get().locator('[data-testid="nc-form-hidden-column"]');
}
getFormFields() { getFormFields() {
return this.get().locator('[data-testid="nc-form-fields"]'); return this.get().locator('[data-testid="nc-form-fields"]');
} }
getDragNDropToHide() {
return this.get().locator('[data-testid="nc-drag-n-drop-to-hide"]');
}
getFormFieldsRemoveIcon() {
return this.get().locator('[data-testid="nc-field-remove-icon"]');
}
getFormFieldsRequired() { getFormFieldsRequired() {
return this.get().locator('[data-testid="nc-form-input-required"] + button'); return this.get().locator('[data-testid="nc-form-input-required"] + button');
} }
getFormFieldsInputLabel() { getFormFieldsInputLabel() {
return this.get().locator('input[data-testid="nc-form-input-label"]:visible'); return this.get().locator('textarea[data-testid="nc-form-input-label"]:visible');
} }
getFormFieldsInputHelpText() { getFormFieldsInputHelpText() {
return this.get().locator('input[data-testid="nc-form-input-help-text"]:visible'); return this.get().locator('textarea[data-testid="nc-form-input-help-text"]:visible');
} }
async verifyFormFieldLabel({ index, label }: { index: number; label: string }) { async verifyFormFieldLabel({ index, label }: { index: number; label: string }) {
@ -83,9 +75,7 @@ export class FormPage extends BasePage {
} }
async verifyFormFieldHelpText({ index, helpText }: { index: number; helpText: string }) { async verifyFormFieldHelpText({ index, helpText }: { index: number; helpText: string }) {
await expect( await expect(this.getFormFields().nth(index).locator('[data-testid="nc-form-help-text"]')).toContainText(helpText);
this.getFormFields().nth(index).locator('[data-testid="nc-form-input-help-text-label"]')
).toContainText(helpText);
} }
async verifyFieldsIsEditable({ index }: { index: number }) { async verifyFieldsIsEditable({ index }: { index: number }) {
@ -124,8 +114,8 @@ export class FormPage extends BasePage {
const dst = this.get().locator(`[data-testid="nc-drag-n-drop-to-hide"]`); const dst = this.get().locator(`[data-testid="nc-drag-n-drop-to-hide"]`);
await src.dragTo(dst); await src.dragTo(dst);
} else if (mode === 'hideField') { } else if (mode === 'hideField') {
const src = this.get().locator(`.nc-form-drag-${field.replace(' ', '')}`); // in form-v2, hide field will be using right sidebar
await src.locator(`[data-testid="nc-field-remove-icon"]`).click(); await this.formFields.locator(`[data-testid="nc-form-field-item-${field}"]`).locator('.nc-switch').click();
} }
} }
@ -141,15 +131,11 @@ export class FormPage extends BasePage {
await src.dragTo(dst, { trial: true }); await src.dragTo(dst, { trial: true });
await src.dragTo(dst); await src.dragTo(dst);
} else if (mode === 'clickField') { } else if (mode === 'clickField') {
const src = this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`); await this.formFields.locator(`[data-testid="nc-form-field-item-${field}"]`).locator('.nc-switch').click();
await src.click();
} }
} }
async removeAllFields() { async removeAllFields() {
// TODO: Otherwise form input boxes are not visible sometimes
await this.rootPage.waitForTimeout(1000);
await this.removeAllButton.click(); await this.removeAllButton.click();
} }
@ -214,8 +200,9 @@ export class FormPage extends BasePage {
await this.get() await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`) .locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-testid="nc-form-input-label"]') .locator('[data-testid="nc-form-input-label"]')
.click(); .click();
await waitForResponse(() => this.getFormFieldsInputLabel().fill(label)); await waitForResponse(() => this.getFormFieldsInputLabel().fill(label));
await waitForResponse(() => this.getFormFieldsInputHelpText().fill(helpText)); await waitForResponse(() => this.getFormFieldsInputHelpText().fill(helpText));
if (required) { if (required) {

18
tests/playwright/tests/db/views/viewForm.spec.ts

@ -49,18 +49,6 @@ test.describe('Form view', () => {
fields: ['LastUpdate', 'Country', 'Cities'], fields: ['LastUpdate', 'Country', 'Cities'],
}); });
// remove & verify (drag-drop)
await form.removeField({ field: 'Cities', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country'],
});
// add & verify (drag-drop)
await form.addField({ field: 'Cities', mode: 'dragDrop' });
await form.verifyFormViewFieldsOrder({
fields: ['LastUpdate', 'Country', 'Cities'],
});
// remove & verify (hide field button) // remove & verify (hide field button)
await form.removeField({ field: 'Cities', mode: 'hideField' }); await form.removeField({ field: 'Cities', mode: 'hideField' });
await form.verifyFormViewFieldsOrder({ await form.verifyFormViewFieldsOrder({
@ -88,7 +76,7 @@ test.describe('Form view', () => {
}); });
}); });
test('Form elements validation', async ({ page }) => { test('Form elements validation', async () => {
// close 'Team & Auth' tab // close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' }); await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Country' }); await dashboard.treeView.openTable({ title: 'Country' });
@ -182,7 +170,7 @@ test.describe('Form view', () => {
await dashboard.verifyToast({ await dashboard.verifyToast({
message: 'Please activate SMTP plugin in App store for enabling email notification', message: 'Please activate SMTP plugin in App store for enabling email notification',
}); });
const url = dashboard.rootPage.url(); // const url = dashboard.rootPage.url();
// activate SMTP plugin // activate SMTP plugin
// await accountAppStorePage.goto(); // await accountAppStorePage.goto();
@ -254,7 +242,6 @@ test.describe('Form view with LTAR', () => {
let dashboard: DashboardPage; let dashboard: DashboardPage;
let loginPage: LoginPage; let loginPage: LoginPage;
let wsPage: WorkspacePage;
let context: any; let context: any;
let api: Api<any>; let api: Api<any>;
@ -264,7 +251,6 @@ test.describe('Form view with LTAR', () => {
context = await setup({ page, isEmptyProject: true }); context = await setup({ page, isEmptyProject: true });
dashboard = new DashboardPage(page, context.base); dashboard = new DashboardPage(page, context.base);
loginPage = new LoginPage(page); loginPage = new LoginPage(page);
wsPage = new WorkspacePage(page);
api = new Api({ api = new Api({
baseURL: `http://localhost:8080/`, baseURL: `http://localhost:8080/`,

Loading…
Cancel
Save