Vue 3 better animation (#257)

* vue-3-better-animation

* Working on migration to vueuse/motion

* Form sidebar animations

* Clean code

* Added animations for modal

* Finished implementing better animations

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>
This commit is contained in:
Julien Nahum 2023-12-08 19:21:04 +01:00 committed by GitHub
parent 24276f0b95
commit f970557b76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1756 additions and 870 deletions

1948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
"@vue/compat": "^3.3.9",
"@vueuse/components": "^10.5.0",
"@vueuse/core": "^10.5.0",
"@vueuse/motion": "^2.0.0",
"axios": "^0.21.1",
"chart.js": "^4.4.0",
"clone-deep": "^4.0.1",

View File

@ -1,74 +1,57 @@
<template>
<Teleport to="body">
<transition leave-active-class="duration-200" name="fade" appear>
<div v-if="show" class="fixed z-30 top-0 inset-x-0 px-4 pt-6 sm:px-0 sm:flex sm:items-top sm:justify-center">
<transition enter-active-class="transition-all delay-75 linear duration-300"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-all linear duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
appear @after-leave="leaveCallback"
<portal to="modals" :order="portalOrder">
<transition @leave="(el,done) => motions.backdrop.leave(done)">
<div v-if="show" v-motion="'backdrop'" :variants="motionFadeIn"
class="fixed z-30 top-0 inset-0 px-4 sm:px-0 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
:class="{'backdrop-blur-sm':backdropBlur}"
@click.self="close"
>
<div ref="content" v-motion="'body'" :variants="motionSlideBottom"
class="self-start bg-white dark:bg-notion-dark w-full relative p-4 md:p-6 my-6 rounded-xl shadow-xl"
:class="maxWidthClass"
>
<div v-if="show" class="fixed inset-0 transform" @click="close">
<div class="absolute inset-0 bg-gray-500 opacity-75" />
<div v-if="closeable" class="absolute top-4 right-4">
<button class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="close">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</transition>
<transition enter-active-class="delay-75 linear duration-300"
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
leave-active-class="linear duration-200" appear
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div v-if="show"
class="modal-content bg-white dark:bg-notion-dark rounded-lg overflow-y-auto shadow-xl transform transition-all sm:w-full"
:class="maxWidthClass"
>
<div class="bg-white relative dark:bg-notion-dark p-4 md:p-6">
<div v-if="closeable" class="absolute top-4 right-4">
<button class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="close">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div class="sm:flex sm:flex-col sm:items-start">
<div v-if="$slots.hasOwnProperty('icon')" class="flex w-full justify-center mb-4">
<div class="w-14 h-14 rounded-full flex justify-center items-center"
:class="'bg-'+iconColor+'-100 text-'+iconColor+'-600'"
>
<slot name="icon" />
</div>
</div>
<div class="mt-3 text-center sm:mt-0 w-full">
<h2 v-if="$slots.hasOwnProperty('title')"
class="text-2xl font-semibold text-center text-gray-900"
>
<slot name="title" />
</h2>
</div>
</div>
<div class="mt-2 w-full">
<slot />
<div class="sm:flex sm:flex-col sm:items-start">
<div v-if="$scopedSlots.hasOwnProperty('icon')" class="flex w-full justify-center mb-4">
<div class="w-14 h-14 rounded-full flex justify-center items-center"
:class="'bg-'+iconColor+'-100 text-'+iconColor+'-600'"
>
<slot name="icon" />
</div>
</div>
<div v-if="$slots.hasOwnProperty('footer')" class="px-6 py-4 bg-gray-100 text-right">
<slot name="footer" />
<div class="mt-3 text-center sm:mt-0 w-full">
<h2 v-if="$scopedSlots.hasOwnProperty('title')"
class="text-2xl font-semibold text-center text-gray-900"
>
<slot name="title" />
</h2>
</div>
</div>
</transition>
<div class="w-full">
<slot />
</div>
<div v-if="$scopedSlots.hasOwnProperty('footer')" class="px-6 py-4 bg-gray-100 text-right">
<slot name="footer" />
</div>
</div>
</div>
</transition>
</Teleport>
</portal>
</template>
<script>
import { useMotions } from '@vueuse/motion'
export default {
name: 'Modal',
@ -76,6 +59,10 @@ export default {
show: {
default: false
},
backdropBlur: {
type: Boolean,
default: false
},
iconColor: {
default: 'blue'
},
@ -87,10 +74,12 @@ export default {
},
portalOrder: {
default: 1
},
afterLeave: {
type: Function,
required: false
}
},
setup () {
return {
motions: useMotions()
}
},
@ -103,52 +92,87 @@ export default {
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl'
}[this.maxWidth]
}
},
watch: {
show: {
immediate: true,
handler: (show) => {
if (show) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = null
},
motionFadeIn () {
return {
initial: {
opacity: 0,
transition: {
delay: 100,
duration: 200,
ease: 'easeIn'
}
},
enter: {
opacity: 1,
transition: {
duration: 200
}
}
}
},
motionSlideBottom () {
return {
initial: {
y: 150,
opacity: 0,
transition: {
ease: 'easeIn',
duration: 200
}
},
enter: {
y: 0,
opacity: 1,
transition: {
duration: 250,
ease: 'easeOut',
delay: 100
}
}
}
}
},
created () {
document.addEventListener('keydown', this.closeOnEscape)
watch: {
show (newVal, oldVal) {
if (newVal !== oldVal) {
if (newVal) {
document.body.classList.add('overflow-hidden')
} else {
document.body.classList.remove('overflow-hidden')
this.motions.body.apply('initial')
this.motions.backdrop.apply('initial')
}
}
}
},
beforeUnmount () {
document.removeEventListener('keydown', this.closeOnEscape)
document.body.classList.remove('overflow-hidden')
},
created () {
const closeOnEscape = (e) => {
if (e.key === 'Escape' && this.show) {
this.close()
}
}
document.addEventListener('keydown', closeOnEscape)
this.$once('hook:destroyed', () => {
document.removeEventListener('keydown', closeOnEscape)
})
},
methods: {
close () {
if (this.closeable) {
document.body.classList.remove('overflow-hidden')
this.$emit('close')
}
},
leaveCallback () {
if (this.afterLeave) {
this.afterLeave()
}
},
closeOnEscape (e) {
if (e.key === 'Escape' && this.show) {
this.close()
}
}
}
}
</script>
<style lang="scss" scoped>
.modal-content {
max-height: calc(100vh - 40px);
}
</style>

View File

@ -49,7 +49,7 @@
<template #trigger="{toggle}">
<button id="dropdown-menu-button" type="button"
class="flex items-center justify-center w-full rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-50 hover:bg-gray-50 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-gray-500"
dusk="nav-dropdown-button" @click.prevent="toggle()"
dusk="nav-dropdown-button" @click.stop="toggle()"
>
<img :src="user.photo_url" class="rounded-full w-6 h-6">
<p class="ml-2 hidden sm:inline">
@ -133,9 +133,9 @@
<script>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth';
import { useFormsStore } from '../stores/forms';
import { useWorkspacesStore } from '../stores/workspaces';
import { useAuthStore } from '../stores/auth'
import { useFormsStore } from '../stores/forms'
import { useWorkspacesStore } from '../stores/workspaces'
import Dropdown from './common/Dropdown.vue'
import WorkspaceDropdown from './WorkspaceDropdown.vue'
@ -153,7 +153,7 @@ export default {
authStore,
formsStore,
workspacesStore,
user : computed(() => authStore.user)
user: computed(() => authStore.user)
}
},

View File

@ -4,7 +4,7 @@
dusk="workspace-dropdown"
>
<template v-if="workspace" #trigger="{toggle}">
<div class="flex items-center cursor group" role="button" @click.prevent="toggle()">
<div class="flex items-center cursor group" role="button" @click.stop="toggle()">
<div class="rounded-full h-8 8">
<img v-if="isUrl(workspace.icon)"
:src="workspace.icon"
@ -61,9 +61,9 @@ export default {
return {
formsStore,
workspacesStore,
user : computed(() => authStore.user),
workspaces : computed(() => workspacesStore.content),
loading : computed(() => workspacesStore.loading)
user: computed(() => authStore.user),
workspaces: computed(() => workspacesStore.content),
loading: computed(() => workspacesStore.loading)
}
},

View File

@ -5,25 +5,23 @@
:open="open"
:close="close"
/>
<transition name="fade">
<div v-if="isOpen" v-on-click-outside="close" :class="dropdownClass">
<div class="py-1 " role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
<slot />
</div>
<collapsible v-model="isOpen" :class="dropdownClass">
<div class="py-1 " role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
<slot />
</div>
</transition>
</collapsible>
</div>
</template>
<script>
import { ref } from 'vue'
import { vOnClickOutside } from '@vueuse/components'
import Collapsible from './transitions/Collapsible.vue'
export default {
name: 'Dropdown',
directives: {
onClickOutside: vOnClickOutside
},
components: { Collapsible },
directives: {},
props: {
dropdownClass: {
type: String,
@ -45,14 +43,11 @@ export default {
isOpen.value = !isOpen.value
}
const dropdownRef = ref(null)
return {
isOpen,
open,
close,
toggle,
dropdownRef
toggle
}
}
}

View File

@ -0,0 +1,59 @@
<template>
<transition @leave="(el,done) => motions.collapsible.leave(done)">
<div
v-if="modelValue"
key="dropdown"
v-motion="'collapsible'"
v-on-click-outside.bubble="close"
:variants="motionCollapse"
>
<slot />
</div>
</transition>
</template>
<script>
import { vOnClickOutside } from '@vueuse/components'
import { useMotions } from '@vueuse/motion'
export default {
name: 'Collapsible',
directives: {
onClickOutside: vOnClickOutside
},
props: {
modelValue: { type: Boolean },
closeOnClickAway: { type: Boolean, default: true }
},
setup () {
return {
motions: useMotions()
}
},
computed: {
motionCollapse () {
return {
enter: {
opacity: 1,
y: 0,
height: 'auto',
transition: { duration: 150, ease: 'easeOut' }
},
initial: {
opacity: 0,
y: -10,
height: 0,
transition: { duration: 75, ease: 'easeIn' }
}
}
}
},
methods: {
close () {
if (this.closeOnClickAway) {
this.$emit('update:modelValue', false)
}
}
}
}
</script>

View File

@ -1,11 +1,11 @@
<template>
<div v-on-click-outside="closeDropdown" class="v-select relative">
<div class="v-select relative">
<span class="inline-block w-full rounded-md">
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
class="cursor-pointer"
:style="inputStyle"
:class="[theme.SelectInput.input,{'py-2': !multiple || loading,'py-1': multiple, '!ring-red-500 !ring-2': hasError, '!cursor-not-allowed !bg-gray-200': disabled}, inputClass]"
@click="openDropdown"
@click.stop="toggleDropdown"
>
<div :class="{'h-6': !multiple, 'min-h-8': multiple && !loading}">
<transition name="fade" mode="out-in">
@ -31,11 +31,11 @@
</span>
</button>
</span>
<div v-show="isOpen"
class="absolute mt-1 rounded-md bg-white dark:bg-notion-dark-light shadow-lg z-10"
:class="dropdownClass"
<collapsible v-model="isOpen"
class="absolute mt-1 rounded-md bg-white dark:bg-notion-dark-light shadow-xl z-10"
:class="dropdownClass"
>
<ul tabindex="-1" role="listbox" aria-labelled by="listbox-label" aria-activedescendant="listbox-item-3"
<ul tabindex="-1" role="listbox"
class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
:class="{'max-h-42 py-1': !isSearchable,'max-h-48 pb-1': isSearchable}"
>
@ -50,7 +50,7 @@
<template v-if="filteredOptions.length > 0">
<li v-for="item in filteredOptions" :key="item[optionKey]" role="option" :style="optionStyle"
:class="{'px-3 pr-9': multiple, 'px-3': !multiple}"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover-bg-form-color focus:outline-none focus-text-white focus-nt-blue"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
@click="select(item)"
>
<slot name="option" :option="item" :selected="isSelected(item)" />
@ -61,29 +61,27 @@
</p>
<li v-if="allowCreation && searchTerm" role="option" :style="optionStyle"
:class="{'px-3 pr-9': multiple, 'px-3': !multiple}"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover-bg-form-color focus:outline-none focus-text-white focus-nt-blue"
class="text-gray-900 cursor-default select-none relative py-2 cursor-pointer group hover:text-white hover:bg-form-color focus:outline-none focus-text-white focus-nt-blue"
@click="createOption(searchTerm)"
>
Create <b class="px-1 bg-gray-300 rounded group-hover-text-black">{{ searchTerm }}</b>
</li>
</ul>
</div>
</collapsible>
</div>
</template>
<script>
import { vOnClickOutside } from '@vueuse/components'
import TextInput from '../TextInput.vue'
import Fuse from 'fuse.js'
import Collapsible from '../../common/transitions/Collapsible.vue'
import { themes } from '../../../config/form-themes'
import TextInput from '../TextInput.vue'
import debounce from 'debounce'
import Fuse from 'fuse.js'
export default {
name: 'VSelect',
components: { TextInput },
directives: {
onClickOutside: vOnClickOutside
},
components: { Collapsible, TextInput },
directives: {},
props: {
data: Array,
modelValue: { default: null },
@ -169,16 +167,19 @@ export default {
}
return this.modelValue === value
},
closeDropdown () {
this.isOpen = false
this.searchTerm = ''
},
openDropdown () {
this.isOpen = this.disabled ? false : !this.isOpen
toggleDropdown () {
if (this.disabled) {
this.isOpen = false
}
this.isOpen = !this.isOpen
if (!this.isOpen) {
this.searchTerm = ''
}
},
select (value) {
if (!this.multiple) {
this.closeDropdown()
// Close after select
this.toggleDropdown()
}
if (this.emitKey) {

View File

@ -21,4 +21,7 @@ export function registerComponents (app) {
app.component('NotionPage', defineAsyncComponent(() =>
import('./open/NotionPage.vue')
))
app.component('FormBlockLogicEditor', defineAsyncComponent(() =>
import('./open/forms/components/form-logic-components/FormBlockLogicEditor.vue')
))
}

View File

@ -0,0 +1,28 @@
<template>
<transition @leave="(el,done) => motions.slide.leave(done)">
<div v-if="show" v-motion-slide-right="'slide'"
class="absolute shadow-lg shadow-gray-800/30 top-0 h-[calc(100vh-53px)] right-0 lg:shadow-none lg:relative bg-white w-full md:w-1/2 lg:w-2/5 border-l overflow-y-scroll md:max-w-[20rem] flex-shrink-0 z-50"
>
<slot />
</div>
</transition>
</template>
<script>
import { useMotions } from '@vueuse/motion'
export default {
name: 'EditorRightSidebar',
props: {
show: {
type: Boolean,
default: false
}
},
setup (props) {
return {
motions: useMotions()
}
}
}
</script>

View File

@ -49,27 +49,25 @@
</div>
</div>
<div class="w-full flex grow overflow-y-scroll relative">
<div class="relative w-full shrink-0 overflow-y-scroll border-r md:w-1/2 md:max-w-sm lg:w-2/5">
<div class="w-full flex grow overflow-y-scroll relative bg-gray-50">
<div class="relative w-full bg-white shrink-0 overflow-y-scroll border-r md:w-1/2 md:max-w-sm lg:w-2/5">
<div class="border-b bg-blue-50 p-5 text-nt-blue-dark md:hidden">
Please create this form on a device with a larger screen. That will allow you to preview your form changes.
</div>
<form-information/>
<form-structure/>
<form-customization/>
<form-notifications/>
<form-about-submission/>
<form-information />
<form-structure />
<form-customization />
<form-notifications />
<form-about-submission />
<form-access />
<form-security-privacy/>
<form-security-privacy />
<form-custom-seo />
<form-custom-code/>
<form-custom-code />
</div>
<form-editor-preview />
<form-field-edit-sidebar />
<add-form-block-sidebar />
<form-editor-sidebar />
<!-- Form Error Modal -->
<form-error-modal
@ -86,12 +84,11 @@
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../../../stores/auth';
import { useFormsStore } from '../../../../stores/forms';
import { useWorkingFormStore } from '../../../../stores/working_form';
import { useWorkspacesStore } from '../../../../stores/workspaces';
import AddFormBlockSidebar from './form-components/AddFormBlockSidebar.vue'
import FormFieldEditSidebar from '../fields/FormFieldEditSidebar.vue'
import { useAuthStore } from '../../../../stores/auth'
import { useFormsStore } from '../../../../stores/forms'
import { useWorkingFormStore } from '../../../../stores/working_form'
import { useWorkspacesStore } from '../../../../stores/workspaces'
import FormEditorSidebar from './form-components/FormEditorSidebar.vue'
import FormErrorModal from './form-components/FormErrorModal.vue'
import FormInformation from './form-components/FormInformation.vue'
import FormStructure from './form-components/FormStructure.vue'
@ -109,8 +106,7 @@ import fieldsLogic from '../../../../mixins/forms/fieldsLogic.js'
export default {
name: 'FormEditor',
components: {
AddFormBlockSidebar,
FormFieldEditSidebar,
FormEditorSidebar,
FormEditorPreview,
FormNotifications,
FormAboutSubmission,
@ -156,7 +152,7 @@ export default {
formsStore,
workingFormStore,
workspacesStore,
user : computed(() => authStore.user)
user: computed(() => authStore.user)
}
},

View File

@ -1,7 +1,5 @@
<template>
<div v-if="showSidebar"
class="absolute shadow-lg shadow-blue-800/30 top-0 h-[calc(100vh-45px)] right-0 lg:shadow-none lg:relative bg-white w-full md:w-1/2 lg:w-2/5 border-l overflow-y-scroll md:max-w-[20rem] flex-shrink-0"
>
<div>
<div class="p-4 border-b sticky top-0 z-10 bg-white">
<div class="flex">
<button class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="closeSidebar">
@ -69,7 +67,7 @@ import { computed } from 'vue'
import { useWorkingFormStore } from '../../../../../stores/working_form'
export default {
name: 'AddFormBlockSidebar',
name: 'AddFormBlock',
components: {},
props: {},
@ -77,8 +75,7 @@ export default {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
selectedFieldIndex : computed(() => workingFormStore.selectedFieldIndex),
showAddFieldSidebar : computed(() => workingFormStore.showAddFieldSidebar)
selectedFieldIndex : computed(() => workingFormStore.selectedFieldIndex)
}
},
@ -183,9 +180,6 @@ export default {
this.workingFormStore.set(value)
}
},
showSidebar () {
return (this.form && this.showAddFieldSidebar) ?? false
},
defaultBlockNames () {
return {

View File

@ -0,0 +1,49 @@
<template>
<editor-right-sidebar :show="form && (showEditFieldSidebar || showAddFieldSidebar)">
<transition mode="out-in">
<form-field-edit v-if="showEditFieldSidebar" :key="editFieldIndex" v-motion-fade="'fade'" />
<add-form-block v-else-if="showAddFieldSidebar" v-motion-fade="'fade'" />
</transition>
</editor-right-sidebar>
</template>
<script>
import { computed } from 'vue'
import { useWorkingFormStore } from '../../../../../stores/working_form'
import EditorRightSidebar from '../../../editors/EditorRightSidebar.vue'
import FormFieldEdit from '../../fields/FormFieldEdit.vue'
import AddFormBlock from './AddFormBlock.vue'
export default {
name: 'FormEditorSidebar',
components: { EditorRightSidebar, AddFormBlock, FormFieldEdit },
props: {},
setup () {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
editFieldIndex: computed(() => workingFormStore.selectedFieldIndex),
showEditFieldSidebar: computed(() => workingFormStore.showEditFieldSidebar),
showAddFieldSidebar: computed(() => workingFormStore.showAddFieldSidebar)
}
},
data () {
return {}
},
computed: {
form: {
get () {
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
}
},
watch: {},
mounted () {
},
methods: {}
}
</script>

View File

@ -6,13 +6,13 @@
</div>
<SelectInput v-model="content.operator" class="w-full" :options="operators"
:name="'operator_'+property.id" placeholder="Comparison operator"
@input="operatorChanged()"
@update:modelValue="operatorChanged()"
/>
<template v-if="hasInput">
<component v-bind="inputComponentData" :is="inputComponentData.component" v-model="content.value" class="w-full"
:name="'value_'+property.id" placeholder="Filter Value"
@input="$emit('input',castContent(content))"
@update:modelValue="emitInput()"
/>
</template>
</div>
@ -131,7 +131,7 @@ export default {
} else if (typeof this.content.value === 'boolean' || typeof this.content.value === 'object') {
this.content.value = null
}
this.$emit('input', this.castContent(this.content))
this.emitInput()
},
needsInput () {
const operator = this.selectedOperator()
@ -165,6 +165,9 @@ export default {
return key.split('_').map(function (item) {
return item.charAt(0).toUpperCase() + item.substring(1)
}).join(' ')
},
emitInput () {
this.$emit('update:modelValue', this.castContent(this.content))
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<query-builder v-model="query" :rules="rules" :config="config" @input="onChange">
<query-builder v-model="query" :rules="rules" :config="config" @update:modelValue="onChange">
<template #groupOperator="props">
<div class="query-builder-group-slot__group-selection flex items-center px-5 border-b py-1 mb-1 flex">
<p class="mr-2 font-semibold">
@ -7,13 +7,13 @@
</p>
<select-input
wrapper-class="relative"
:value="props.currentOperator"
:model-value="props.currentOperator"
:options="props.operators"
emit-key="identifier"
option-key="identifier"
name="operator-input"
margin-bottom=""
@input="props.updateCurrentOperator($event)"
@update:modelValue="props.updateCurrentOperator($event)"
/>
</div>
</template>
@ -23,17 +23,17 @@
<template #rule="ruleCtrl">
<component
:is="ruleCtrl.ruleComponent"
:value="ruleCtrl.ruleData"
@input="ruleCtrl.updateRuleData"
:model-value="ruleCtrl.ruleData"
@update:modelValue="ruleCtrl.updateRuleData"
/>
</template>
</query-builder>
</template>
<script>
import { defineComponent } from 'vue'
import QueryBuilder from 'query-builder-vue-3'
import ColumnCondition from './ColumnCondition.vue'
import Vue from 'vue'
import GroupControlSlot from './GroupControlSlot.vue'
export default {
@ -66,7 +66,8 @@ export default {
identifier: property.id,
name: property.name,
component: (function () {
return Vue.extend(ColumnCondition).extend({
return defineComponent({
extends: ColumnCondition,
computed: {
property () {
return property
@ -111,7 +112,7 @@ export default {
methods: {
onChange () {
this.$emit('input', this.query)
this.$emit('update:modelValue', this.query)
}
}
}

View File

@ -67,7 +67,6 @@
</template>
<script>
import ProTag from '../../../../common/ProTag.vue'
import ConditionEditor from './ConditionEditor.vue'
import Modal from '../../../../Modal.vue'
import SelectInput from '../../../../forms/SelectInput.vue'
@ -75,7 +74,7 @@ import clonedeep from 'clone-deep'
export default {
name: 'FormBlockLogicEditor',
components: { SelectInput, Modal, ProTag, ConditionEditor },
components: { SelectInput, Modal, ConditionEditor },
props: {
field: {
type: Object,

View File

@ -1,7 +1,5 @@
<template>
<div v-if="showSidebar"
class="absolute shadow-lg shadow-blue-800/30 top-0 h-[calc(100vh-45px)] right-0 lg:shadow-none lg:relative bg-white w-full md:w-1/2 lg:w-2/5 border-l overflow-y-scroll md:max-w-[20rem] flex-shrink-0 overflow-x-hidden"
>
<div>
<div class="p-4 border-b sticky top-0 z-10 bg-white">
<button v-if="!field" class="text-gray-500 hover:text-gray-900 cursor-pointer" @click.prevent="closeSidebar">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -78,15 +76,14 @@ import FieldOptions from './components/FieldOptions.vue'
import BlockOptions from './components/BlockOptions.vue'
export default {
name: 'FormFieldEditSidebar',
name: 'FormFieldEdit',
components: { ChangeFieldType, FieldOptions, BlockOptions },
props: {},
setup () {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore,
selectedFieldIndex : computed(() => workingFormStore.selectedFieldIndex),
showEditFieldSidebar : computed(() => workingFormStore.showEditFieldSidebar)
selectedFieldIndex : computed(() => workingFormStore.selectedFieldIndex)
}
},
data () {
@ -108,9 +105,6 @@ export default {
field () {
return (this.form && this.selectedFieldIndex !== null) ? this.form.properties[this.selectedFieldIndex] : null
},
showSidebar () {
return (this.form && this.selectedFieldIndex !== null) ? (this.form.properties[this.selectedFieldIndex] && this.showEditFieldSidebar) : false
},
isBlockField () {
return this.field && this.field.type.startsWith('nf')
},

View File

@ -86,17 +86,16 @@
</div>
<!-- Logic Block -->
<form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />
<!-- <form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />-->
</div>
</template>
<script>
import CodeInput from '../../../../forms/CodeInput.vue'
const FormBlockLogicEditor = () => import('../../components/form-logic-components/FormBlockLogicEditor.vue')
export default {
name: 'BlockOptions',
components: { FormBlockLogicEditor, CodeInput },
components: { CodeInput },
props: {
field: {
type: Object,

View File

@ -1,7 +1,7 @@
<template>
<dropdown dusk="nav-dropdown" v-if="changeTypeOptions.length > 0">
<dropdown v-if="changeTypeOptions.length > 0" dusk="nav-dropdown">
<template #trigger="{toggle}">
<v-button class="relative" :class="btnClasses" size="small" color="light-gray" @click="toggle">
<v-button class="relative" :class="btnClasses" size="small" color="light-gray" @click.stop="toggle">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="h-4 w-4 text-blue-600 inline mr-1 -mt-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</svg>
@ -9,7 +9,7 @@
</v-button>
</template>
<a href="#" v-for="(op, index) in changeTypeOptions" :key="index"
<a v-for="(op, index) in changeTypeOptions" :key="index" href="#"
class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="changeType(op.value)"
>
@ -23,7 +23,7 @@ import Dropdown from '../../../../common/Dropdown.vue'
export default {
name: 'ChangeFieldType',
components: {Dropdown},
components: { Dropdown },
props: {
field: {
type: Object,
@ -34,25 +34,25 @@ export default {
required: true
}
},
data() {
data () {
return {}
},
computed: {
changeTypeOptions() {
var newTypes = []
changeTypeOptions () {
let newTypes = []
if (['text', 'email', 'phone', 'number'].includes(this.field.type)) {
newTypes = [
{'name': 'Text Input', 'value': 'text'},
{'name': 'Email Input', 'value': 'email'},
{'name': 'Phone Input', 'value': 'phone'},
{'name': 'Number Input', 'value': 'number'}
{ name: 'Text Input', value: 'text' },
{ name: 'Email Input', value: 'email' },
{ name: 'Phone Input', value: 'phone' },
{ name: 'Number Input', value: 'number' }
]
}
if (['select', 'multi_select'].includes(this.field.type)) {
newTypes = [
{'name': 'Select Input', 'value': 'select'},
{'name': 'Multi-Select Input', 'value': 'multi_select'}
{ name: 'Select Input', value: 'select' },
{ name: 'Multi-Select Input', value: 'multi_select' }
]
}
return newTypes.filter((item) => {
@ -68,11 +68,11 @@ export default {
watch: {},
mounted() {
mounted () {
},
methods: {
changeType(newType) {
changeType (newType) {
if (newType) {
this.$emit('changeType', newType)
}

View File

@ -293,7 +293,7 @@
<file-input v-else-if="field.type==='files'" name="prefill" class="mt-4"
:form="field"
label="Pre-filled file"
:multiple="field.multiple===true" :moveToFormAssets="true"
:multiple="field.multiple===true" :move-to-form-assets="true"
/>
<text-input v-else-if="!['files', 'signature'].includes(field.type)" name="prefill" class="mt-3"
:form="field"
@ -382,7 +382,7 @@
</div>
<!-- Logic Block -->
<form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />
<!-- <form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />-->
</div>
</template>
@ -391,11 +391,9 @@ import timezones from '../../../../../../data/timezones.json'
import countryCodes from '../../../../../../data/country_codes.json'
import CountryFlag from 'vue-country-flag-next'
const FormBlockLogicEditor = () => import('../../components/form-logic-components/FormBlockLogicEditor.vue')
export default {
name: 'FieldOptions',
components: { FormBlockLogicEditor, CountryFlag },
components: { CountryFlag },
props: {
field: {
type: Object,
@ -533,7 +531,7 @@ export default {
this.field.hidden = true
}
},
initRating() {
initRating () {
if (this.field.is_rating) {
this.$set(this.field, 'is_scale', false)
if (!this.field.rating_max_value) {

View File

@ -5,7 +5,7 @@
</div>
<dropdown v-else class="inline" dusk="nav-dropdown">
<template #trigger="{toggle}">
<v-button color="white" class="mr-2" @click="toggle">
<v-button color="white" class="mr-2" @click.stop="toggle">
<svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
@ -147,21 +147,21 @@ import Dropdown from '../../../common/Dropdown.vue'
import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue'
export default {
name: 'ExtraMenu',
components: { Dropdown, FormTemplateModal },
props: {
form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
},
name: 'ExtraMenu',
components: { Dropdown, FormTemplateModal },
props: {
form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
},
setup () {
const authStore = useAuthStore()
const formsStore = useFormsStore()
return {
formsStore,
user : computed(() => authStore.user)
}
},
setup () {
const authStore = useAuthStore()
const formsStore = useFormsStore()
return {
formsStore,
user: computed(() => authStore.user)
}
},
data: () => ({
loadingDuplicate: false,
@ -170,40 +170,40 @@ export default {
showFormTemplateModal: false
}),
computed: {
formEndpoint: () => '/api/open/forms/{id}',
},
computed: {
formEndpoint: () => '/api/open/forms/{id}'
},
methods: {
copyLink(){
const el = document.createElement('textarea')
el.value = this.form.share_url
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.alertSuccess('Copied!')
},
duplicateForm() {
if (this.loadingDuplicate) return
this.loadingDuplicate = true
axios.post(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate').then((response) => {
this.formsStore.addOrUpdate(response.data.new_form)
this.$router.push({name: 'forms.show', params: {slug: response.data.new_form.slug}})
this.alertSuccess('Form was successfully duplicated.')
this.loadingDuplicate = false
})
},
deleteForm() {
if (this.loadingDelete) return
this.loadingDelete = true
axios.delete(this.formEndpoint.replace('{id}', this.form.id)).then(() => {
this.formsStore.remove(this.form)
this.$router.push({name: 'home'})
this.alertSuccess('Form was deleted.')
this.loadingDelete = false
})
},
methods: {
copyLink () {
const el = document.createElement('textarea')
el.value = this.form.share_url
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.alertSuccess('Copied!')
},
duplicateForm () {
if (this.loadingDuplicate) return
this.loadingDuplicate = true
axios.post(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate').then((response) => {
this.formsStore.addOrUpdate(response.data.new_form)
this.$router.push({ name: 'forms.show', params: { slug: response.data.new_form.slug } })
this.alertSuccess('Form was successfully duplicated.')
this.loadingDuplicate = false
})
},
deleteForm () {
if (this.loadingDelete) return
this.loadingDelete = true
axios.delete(this.formEndpoint.replace('{id}', this.form.id)).then(() => {
this.formsStore.remove(this.form)
this.$router.push({ name: 'home' })
this.alertSuccess('Form was deleted.')
this.loadingDelete = false
})
}
}
}
</script>

View File

@ -1,5 +1,6 @@
import './axios'
import { registerLogEventOnApp } from './amplitude'
import { MotionPlugin } from '@vueuse/motion'
import './vapor'
import './sentry'
@ -11,6 +12,7 @@ function registerPlugin (app) {
app.use(Notifications)
app.use(metaManager)
app.use(MotionPlugin)
registerLogEventOnApp(app)
return app
}

View File

@ -31,13 +31,13 @@ export const useAppStore = defineStore('app', {
this.loaderHide()
},
loaderSetTimer (timerVal) {
this._timer = timerVal
this.loader._timer = timerVal
},
loaderPause () {
clearInterval(this.loader._timer)
},
loaderHide () {
clearInterval(this.loader._timer)
this.loaderPause()
this.loader._timer = null
setTimeout(() => {
this.loader.show = false
@ -68,4 +68,4 @@ export const useAppStore = defineStore('app', {
}, 100))
}
}
})
})