Browse Source

Merge pull request #3980 from nocodb/feat/survey-form-animation

feat(nc-gui): add animation to survey form on keypress
pull/3994/head
Raju Udava 2 years ago committed by GitHub
parent
commit
96eb0c3fa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/components.d.ts
  2. 94
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  3. 1
      packages/nc-gui/lib/types.ts
  4. 112
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

2
packages/nc-gui/components.d.ts vendored

@ -103,6 +103,7 @@ declare module '@vue/runtime-core' {
MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default'] MaterialSymbolsDarkModeOutline: typeof import('~icons/material-symbols/dark-mode-outline')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default'] MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default'] MaterialSymbolsKeyboardReturn: typeof import('~icons/material-symbols/keyboard-return')['default']
MaterialSymbolsKeyboardShift: typeof import('~icons/material-symbols/keyboard-shift')['default']
MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default'] MaterialSymbolsLightModeOutline: typeof import('~icons/material-symbols/light-mode-outline')['default']
MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default'] MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default'] MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
@ -117,6 +118,7 @@ declare module '@vue/runtime-core' {
MdiAlpha: typeof import('~icons/mdi/alpha')['default'] MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default'] MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default'] MdiApi: typeof import('~icons/mdi/api')['default']
MdiAppleKeyboardShift: typeof import('~icons/mdi/apple-keyboard-shift')['default']
MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default'] MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default']
MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default'] MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default'] MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']

94
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -10,6 +10,7 @@ import {
ref, ref,
useCopy, useCopy,
useDashboard, useDashboard,
useDebounceFn,
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
@ -39,6 +40,13 @@ const passwordProtected = ref(false)
const shared = ref<SharedView>({ id: '', meta: {}, password: undefined }) const shared = ref<SharedView>({ id: '', meta: {}, password: undefined })
const transitionDuration = computed({
get: () => shared.value.meta.transitionDuration || 250,
set: (duration) => {
shared.value.meta = { ...shared.value.meta, transitionDuration: duration }
},
})
const allowCSVDownload = computed({ const allowCSVDownload = computed({
get: () => !!shared.value.meta.allowCSVDownload, get: () => !!shared.value.meta.allowCSVDownload,
set: (allow) => { set: (allow) => {
@ -112,6 +120,8 @@ async function saveTheme() {
$e(`a:view:share:${viewTheme.value ? 'enable' : 'disable'}-theme`) $e(`a:view:share:${viewTheme.value ? 'enable' : 'disable'}-theme`)
} }
const saveTransitionDuration = useDebounceFn(updateSharedViewMeta, 1000, { maxWait: 2000 })
async function updateSharedViewMeta() { async function updateSharedViewMeta() {
try { try {
const meta = shared.value.meta && isString(shared.value.meta) ? JSON.parse(shared.value.meta) : shared.value.meta const meta = shared.value.meta && isString(shared.value.meta) ? JSON.parse(shared.value.meta) : shared.value.meta
@ -222,10 +232,28 @@ watch(passwordProtected, (value) => {
v-if="shared.type === ViewTypes.FORM" v-if="shared.type === ViewTypes.FORM"
v-model:checked="surveyMode" v-model:checked="surveyMode"
data-cy="nc-modal-share-view__survey-mode" data-cy="nc-modal-share-view__survey-mode"
class="!text-xs" class="!text-sm"
> >
Use Survey Mode Use Survey Mode
</a-checkbox> </a-checkbox>
<Transition name="layout" mode="out-in">
<div v-if="surveyMode" class="flex flex-col justify-center pl-6">
<a-form-item class="!my-1" :has-feedback="false" name="transitionDuration">
<template #label>
<div class="text-xs">Transition duration (in MS)</div>
</template>
<a-input
v-model:value="transitionDuration"
data-cy="nc-form-signin__email"
size="small"
class="!w-32"
type="number"
@change="saveTransitionDuration"
/>
</a-form-item>
</div>
</Transition>
</div> </div>
<div> <div>
@ -234,43 +262,53 @@ watch(passwordProtected, (value) => {
v-if="shared.type === ViewTypes.FORM" v-if="shared.type === ViewTypes.FORM"
v-model:checked="viewTheme" v-model:checked="viewTheme"
data-cy="nc-modal-share-view__with-theme" data-cy="nc-modal-share-view__with-theme"
class="!text-xs" class="!text-sm"
> >
Use Theme Use Theme
</a-checkbox> </a-checkbox>
<div v-if="viewTheme" class="flex pl-2"> <Transition name="layout" mode="out-in">
<LazyGeneralColorPicker <div v-if="viewTheme" class="flex pl-6">
data-cy="nc-modal-share-view__theme-picker" <LazyGeneralColorPicker
:model-value="shared.meta.theme?.primaryColor" data-cy="nc-modal-share-view__theme-picker"
:colors="projectThemeColors" class="!p-0"
:row-size="9" :model-value="shared.meta.theme?.primaryColor"
:advanced="false" :colors="projectThemeColors"
@input="onChangeTheme" :row-size="9"
/> :advanced="false"
</div> @input="onChangeTheme"
/>
</div>
</Transition>
</div> </div>
<div> <div>
<!-- Password Protection --> <!-- Password Protection -->
<a-checkbox v-model:checked="passwordProtected" data-cy="nc-modal-share-view__with-password" class="!text-xs"> <a-checkbox v-model:checked="passwordProtected" data-cy="nc-modal-share-view__with-password" class="!text-sm !my-1">
{{ $t('msg.info.beforeEnablePwd') }} {{ $t('msg.info.beforeEnablePwd') }}
</a-checkbox> </a-checkbox>
<div v-if="passwordProtected" class="ml-6 flex gap-2 mt-2 mb-4"> <Transition name="layout" mode="out-in">
<a-input <div v-if="passwordProtected" class="pl-6 flex gap-2 mt-2 mb-4">
v-model:value="shared.password" <a-input
data-cy="nc-modal-share-view__password" v-model:value="shared.password"
size="small" data-cy="nc-modal-share-view__password"
class="!text-xs max-w-[250px]" size="small"
type="password" class="!text-xs max-w-[250px]"
:placeholder="$t('placeholder.password.enter')" type="password"
/> :placeholder="$t('placeholder.password.enter')"
/>
<a-button data-cy="nc-modal-share-view__save-password" size="small" class="!text-xs" @click="saveShareLinkPassword">
{{ $t('placeholder.password.save') }} <a-button
</a-button> data-cy="nc-modal-share-view__save-password"
</div> size="small"
class="!text-xs"
@click="saveShareLinkPassword"
>
{{ $t('placeholder.password.save') }}
</a-button>
</div>
</Transition>
</div> </div>
<div> <div>
@ -279,7 +317,7 @@ watch(passwordProtected, (value) => {
v-if="shared && (shared.type === ViewTypes.GRID || shared.type === ViewTypes.KANBAN)" v-if="shared && (shared.type === ViewTypes.GRID || shared.type === ViewTypes.KANBAN)"
v-model:checked="allowCSVDownload" v-model:checked="allowCSVDownload"
data-cy="nc-modal-share-view__with-csv-download" data-cy="nc-modal-share-view__with-csv-download"
class="!text-xs" class="!text-sm"
> >
{{ $t('labels.downloadAllowed') }} {{ $t('labels.downloadAllowed') }}
</a-checkbox> </a-checkbox>

1
packages/nc-gui/lib/types.ts

@ -73,6 +73,7 @@ export interface TabItem {
export interface SharedViewMeta extends Record<string, any> { export interface SharedViewMeta extends Record<string, any> {
surveyMode?: boolean surveyMode?: boolean
transitionDuration?: number // in ms
withTheme?: boolean withTheme?: boolean
theme?: Partial<ThemeConfig> theme?: Partial<ThemeConfig>
allowCSVDownload?: boolean allowCSVDownload?: boolean

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

@ -20,18 +20,32 @@ enum TransitionDirection {
Right = 'right', Right = 'right',
} }
enum AnimationTarget {
ArrowLeft = 'arrow-left',
ArrowRight = 'arrow-right',
OkButton = 'ok-button',
SubmitButton = 'submit-button',
}
const { md } = useBreakpoints(breakpointsTailwind) const { md } = useBreakpoints(breakpointsTailwind)
const { v$, formState, formColumns, submitForm, submitted, secondsRemain, sharedFormView, onReset } = useSharedFormStoreOrThrow() const { v$, formState, formColumns, submitForm, submitted, secondsRemain, sharedFormView, sharedViewMeta, onReset } =
useSharedFormStoreOrThrow()
const isTransitioning = ref(false) const isTransitioning = ref(false)
const transitionName = ref<TransitionDirection>(TransitionDirection.Left) const transitionName = ref<TransitionDirection>(TransitionDirection.Left)
const animationTarget = ref<AnimationTarget>(AnimationTarget.ArrowRight)
const isAnimating = ref(false)
const el = ref<HTMLDivElement>() const el = ref<HTMLDivElement>()
provide(DropZoneRef, el) provide(DropZoneRef, el)
const transitionDuration = computed(() => sharedViewMeta.value.transitionDuration || 250)
const steps = computed(() => { const steps = computed(() => {
if (!formColumns.value) return [] if (!formColumns.value) return []
@ -72,17 +86,27 @@ function transition(direction: TransitionDirection) {
setTimeout(() => { setTimeout(() => {
transitionName.value = transitionName.value =
transitionName.value === TransitionDirection.Left ? TransitionDirection.Right : TransitionDirection.Left transitionName.value === TransitionDirection.Left ? TransitionDirection.Right : TransitionDirection.Left
}, 500) }, transitionDuration.value / 2)
setTimeout(() => { setTimeout(() => {
isTransitioning.value = false isTransitioning.value = false
setTimeout(focusInput, 100) setTimeout(focusInput, 100)
}, 1000) }, transitionDuration.value)
} }
async function goNext() { function animate(target: AnimationTarget) {
if (isLast.value) return animationTarget.value = target
isAnimating.value = true
setTimeout(() => {
isAnimating.value = false
}, transitionDuration.value / 2)
}
async function goNext(animationTarget?: AnimationTarget) {
if (isLast.value || submitted.value) return
if (!field.value || !field.value.title) return if (!field.value || !field.value.title) return
@ -93,13 +117,22 @@ async function goNext() {
if (!isValid) return if (!isValid) return
} }
transition(TransitionDirection.Left) animate(animationTarget || AnimationTarget.ArrowRight)
setTimeout(
() => {
transition(TransitionDirection.Left)
goToNext() goToNext()
},
animationTarget === AnimationTarget.OkButton ? 300 : 0,
)
} }
async function goPrevious() { async function goPrevious(animationTarget?: AnimationTarget) {
if (isFirst.value) return if (isFirst.value || submitted.value) return
animate(animationTarget || AnimationTarget.ArrowLeft)
transition(TransitionDirection.Right) transition(TransitionDirection.Right)
@ -128,8 +161,19 @@ function resetForm() {
onReset(resetForm) onReset(resetForm)
onKeyStroke(['ArrowLeft', 'ArrowDown'], goPrevious) onKeyStroke(['ArrowLeft', 'ArrowDown'], () => {
onKeyStroke(['ArrowRight', 'ArrowUp', 'Enter', 'Space'], goNext) goPrevious(AnimationTarget.ArrowLeft)
})
onKeyStroke(['ArrowRight', 'ArrowUp'], () => {
goNext(AnimationTarget.ArrowRight)
})
onKeyStroke(['Enter', 'Space'], () => {
if (isLast.value) {
submitForm()
} else {
goNext(AnimationTarget.OkButton)
}
})
onMounted(() => { onMounted(() => {
focusInput() focusInput()
@ -154,7 +198,7 @@ onMounted(() => {
<div ref="el" class="pt-8 md:p-0 w-full h-full flex flex-col"> <div ref="el" class="pt-8 md:p-0 w-full h-full flex flex-col">
<div <div
v-if="sharedFormView" v-if="sharedFormView"
style="height: max(40vh, 250px); min-height: 250px" style="height: max(40vh, 225px); min-height: 225px"
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">
@ -171,7 +215,7 @@ onMounted(() => {
</div> </div>
<div class="h-full w-full flex items-center px-4 md:px-0"> <div class="h-full w-full flex items-center px-4 md:px-0">
<Transition :name="`slide-${transitionName}`" :duration="1000" mode="out-in"> <Transition :name="`slide-${transitionName}`" :duration="transitionDuration" mode="out-in">
<div <div
ref="el" ref="el"
:key="field.title" :key="field.title"
@ -219,6 +263,11 @@ onMounted(() => {
<div class="block text-[14px]" data-cy="nc-survey-form__field-description"> <div class="block text-[14px]" data-cy="nc-survey-form__field-description">
{{ field.description }} {{ field.description }}
</div> </div>
<div v-if="field.uidt === UITypes.LongText" class="text-sm text-gray-500 flex flex-wrap items-center">
Shift <MdiAppleKeyboardShift class="mx-1 text-primary" /> + Enter
<MaterialSymbolsKeyboardReturn class="mx-1 text-primary" /> to make a line break
</div>
</div> </div>
</div> </div>
</div> </div>
@ -227,10 +276,14 @@ onMounted(() => {
<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 <button
:class="
animationTarget === AnimationTarget.SubmitButton && isAnimating
? 'transform translate-y-[1px] translate-x-[1px] ring ring-accent ring-opacity-100'
: ''
"
type="submit" type="submit"
class="uppercase scaling-btn prose-sm" class="uppercase scaling-btn prose-sm"
data-cy="nc-survey-form__btn-submit" data-cy="nc-survey-form__btn-submit" @click="submitForm"
@click="submitForm"
> >
{{ $t('general.submit') }} {{ $t('general.submit') }}
</button> </button>
@ -245,7 +298,12 @@ onMounted(() => {
<button <button
class="bg-opacity-100 scaling-btn flex items-center gap-1" class="bg-opacity-100 scaling-btn flex items-center gap-1"
data-cy="nc-survey-form__btn-next" data-cy="nc-survey-form__btn-next"
:class="v$.localState[field.title]?.$error ? 'after:!bg-gray-100 after:!ring-red-500' : ''" :class="[
v$.localState[field.title]?.$error ? '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" @click="goNext"
> >
<Transition name="fade"> <Transition name="fade">
@ -261,7 +319,7 @@ onMounted(() => {
<!-- todo: i18n --> <!-- todo: i18n -->
<div class="hidden md:flex text-sm text-gray-500 items-center gap-1"> <div class="hidden md:flex text-sm text-gray-500 items-center gap-1">
Press Enter <MaterialSymbolsKeyboardReturn class="mt-1" /> Press Enter <MaterialSymbolsKeyboardReturn class="text-primary" />
</div> </div>
</div> </div>
</div> </div>
@ -319,9 +377,13 @@ onMounted(() => {
> >
<a-tooltip :title="isFirst ? '' : 'Go to previous'" :mouse-enter-delay="0.25" :mouse-leave-delay="0"> <a-tooltip :title="isFirst ? '' : 'Go to previous'" :mouse-enter-delay="0.25" :mouse-leave-delay="0">
<button <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" class="p-0.5 flex items-center group color-transition"
data-cy="nc-survey-form__icon-prev" data-cy="nc-survey-form__icon-prev" @click="goPrevious"
@click="goPrevious"
> >
<MdiChevronLeft :class="isFirst ? 'text-gray-300' : 'group-hover:text-accent'" class="text-2xl md:text-md" /> <MdiChevronLeft :class="isFirst ? 'text-gray-300' : 'group-hover:text-accent'" class="text-2xl md:text-md" />
</button> </button>
@ -332,9 +394,17 @@ onMounted(() => {
:mouse-enter-delay="0.25" :mouse-enter-delay="0.25"
:mouse-leave-delay="0" :mouse-leave-delay="0"
> >
<button class="p-0.5 flex items-center group color-transition" data-cy="nc-survey-form__icon-next" @click="goNext"> <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"
data-cy="nc-survey-form__icon-next" @click="goNext"
>
<MdiChevronRight <MdiChevronRight
:class="isLast || v$.localState[field.title]?.$error ? 'text-gray-300' : 'group-hover:text-accent'" :class="[isLast || v$.localState[field.title]?.$error ? 'text-gray-300' : 'group-hover:text-accent']"
class="text-2xl md:text-md" class="text-2xl md:text-md"
/> />
</button> </button>

Loading…
Cancel
Save