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:
parent
24276f0b95
commit
f970557b76
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -1,32 +1,15 @@
|
|||
<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 v-if="show" class="fixed inset-0 transform" @click="close">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75" />
|
||||
</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"
|
||||
<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 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">
|
||||
|
@ -37,7 +20,7 @@
|
|||
</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 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'"
|
||||
>
|
||||
|
@ -45,7 +28,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 w-full">
|
||||
<h2 v-if="$slots.hasOwnProperty('title')"
|
||||
<h2 v-if="$scopedSlots.hasOwnProperty('title')"
|
||||
class="text-2xl font-semibold text-center text-gray-900"
|
||||
>
|
||||
<slot name="title" />
|
||||
|
@ -53,22 +36,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 w-full">
|
||||
<div class="w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.hasOwnProperty('footer')" class="px-6 py-4 bg-gray-100 text-right">
|
||||
<div v-if="$scopedSlots.hasOwnProperty('footer')" class="px-6 py-4 bg-gray-100 text-right">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</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]
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
show: {
|
||||
immediate: true,
|
||||
handler: (show) => {
|
||||
if (show) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
show (newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
if (newVal) {
|
||||
document.body.classList.add('overflow-hidden')
|
||||
} else {
|
||||
document.body.style.overflow = null
|
||||
document.body.classList.remove('overflow-hidden')
|
||||
this.motions.body.apply('initial')
|
||||
this.motions.backdrop.apply('initial')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
document.addEventListener('keydown', this.closeOnEscape)
|
||||
},
|
||||
|
||||
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>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -5,25 +5,23 @@
|
|||
:open="open"
|
||||
:close="close"
|
||||
/>
|
||||
<transition name="fade">
|
||||
<div v-if="isOpen" v-on-click-outside="close" :class="dropdownClass">
|
||||
|
||||
<collapsible v-model="isOpen" :class="dropdownClass">
|
||||
<div class="py-1 " role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
<slot />
|
||||
</div>
|
||||
</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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
<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 () {
|
||||
toggleDropdown () {
|
||||
if (this.disabled) {
|
||||
this.isOpen = false
|
||||
}
|
||||
this.isOpen = !this.isOpen
|
||||
if (!this.isOpen) {
|
||||
this.searchTerm = ''
|
||||
},
|
||||
openDropdown () {
|
||||
this.isOpen = this.disabled ? false : !this.isOpen
|
||||
}
|
||||
},
|
||||
select (value) {
|
||||
if (!this.multiple) {
|
||||
this.closeDropdown()
|
||||
// Close after select
|
||||
this.toggleDropdown()
|
||||
}
|
||||
|
||||
if (this.emitKey) {
|
||||
|
|
|
@ -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')
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -49,8 +49,8 @@
|
|||
</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>
|
||||
|
@ -67,9 +67,7 @@
|
|||
</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,
|
||||
|
|
|
@ -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 {
|
|
@ -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>
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
},
|
|
@ -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,
|
||||
|
|
|
@ -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)"
|
||||
>
|
||||
|
@ -40,19 +40,19 @@ export default {
|
|||
|
||||
computed: {
|
||||
changeTypeOptions () {
|
||||
var newTypes = []
|
||||
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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
@ -171,7 +171,7 @@ export default {
|
|||
}),
|
||||
|
||||
computed: {
|
||||
formEndpoint: () => '/api/open/forms/{id}',
|
||||
formEndpoint: () => '/api/open/forms/{id}'
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -203,7 +203,7 @@ export default {
|
|||
this.alertSuccess('Form was deleted.')
|
||||
this.loadingDelete = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue