Work in progress
This commit is contained in:
153
client/pages/forms/create-guest.vue
Normal file
153
client/pages/forms/create-guest.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap flex-col">
|
||||
<transition v-if="stateReady" name="fade" mode="out-in">
|
||||
<div key="2">
|
||||
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
|
||||
@close="showInitialFormModal=false"
|
||||
/>
|
||||
<form-editor v-if="!workspacesLoading" ref="editor"
|
||||
class="w-full flex flex-grow"
|
||||
:error="error"
|
||||
:is-guest="isGuest"
|
||||
@openRegister="openRegister"
|
||||
/>
|
||||
<div v-else class="text-center mt-4 py-6">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<quick-register :show-register-modal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
|
||||
@afterLogin="afterLogin"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import Form from 'vform'
|
||||
import { useTemplatesStore } from '../../stores/templates'
|
||||
import { useWorkingFormStore } from '../../stores/working_form'
|
||||
import { useWorkspacesStore } from '../../stores/workspaces'
|
||||
import QuickRegister from '~/components/pages/auth/components/QuickRegister.vue'
|
||||
import initForm from '../../mixins/form_editor/initForm.js'
|
||||
import SeoMeta from '../../mixins/seo-meta.js'
|
||||
import CreateFormBaseModal from '../../components/pages/forms/create/CreateFormBaseModal.vue'
|
||||
|
||||
const loadTemplates = function () {
|
||||
const templatesStore = useTemplatesStore()
|
||||
templatesStore.startLoading()
|
||||
templatesStore.loadIfEmpty().then(() => {
|
||||
templatesStore.stopLoading()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'CreateFormGuest',
|
||||
components: {
|
||||
QuickRegister, CreateFormBaseModal
|
||||
},
|
||||
mixins: [initForm, SeoMeta],
|
||||
middleware: 'guest',
|
||||
|
||||
beforeRouteEnter (to, from, next) {
|
||||
loadTemplates()
|
||||
next()
|
||||
},
|
||||
|
||||
setup () {
|
||||
const templatesStore = useTemplatesStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
return {
|
||||
templatesStore,
|
||||
workingFormStore,
|
||||
workspacesStore,
|
||||
workspaces : computed(() => workspacesStore.content),
|
||||
workspacesLoading : computed(() => workspacesStore.loading)
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
metaTitle: 'Create a new Form as Guest',
|
||||
stateReady: false,
|
||||
loading: false,
|
||||
error: '',
|
||||
registerModal: false,
|
||||
isGuest: true,
|
||||
showInitialFormModal: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
form: {
|
||||
get () {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set (value) {
|
||||
this.workingFormStore.set(value)
|
||||
}
|
||||
},
|
||||
workspace () {
|
||||
return this.workspacesStore.getCurrent()
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
workspace () {
|
||||
if (this.workspace) {
|
||||
this.form.workspace_id = this.workspace.id
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
// Set as guest user
|
||||
const guestWorkspace = {
|
||||
id: null,
|
||||
name: 'Guest Workspace',
|
||||
is_enterprise: false,
|
||||
is_pro: false
|
||||
}
|
||||
this.workspacesStore.set([guestWorkspace])
|
||||
this.workspacesStore.setCurrentId(guestWorkspace.id)
|
||||
|
||||
this.initForm()
|
||||
if (this.$route.query.template !== undefined && this.$route.query.template) {
|
||||
const template = this.templatesStore.getBySlug(this.$route.query.template)
|
||||
if (template && template.structure) {
|
||||
this.form = new Form({ ...this.form.data(), ...template.structure })
|
||||
}
|
||||
} else {
|
||||
// No template loaded, ask how to start
|
||||
this.showInitialFormModal = true
|
||||
}
|
||||
this.closeAlert()
|
||||
this.stateReady = true
|
||||
},
|
||||
|
||||
created () {},
|
||||
unmounted () {},
|
||||
|
||||
methods: {
|
||||
openRegister () {
|
||||
this.registerModal = true
|
||||
},
|
||||
afterLogin () {
|
||||
this.registerModal = false
|
||||
this.isGuest = false
|
||||
this.workspacesStore.load()
|
||||
setTimeout(() => {
|
||||
if (this.$refs.editor) {
|
||||
this.$refs.editor.saveFormCreate()
|
||||
}
|
||||
}, 500)
|
||||
},
|
||||
formGenerated (form) {
|
||||
this.form = new Form({ ...this.form.data(), ...form })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
164
client/pages/forms/create.vue
Normal file
164
client/pages/forms/create.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap flex-col">
|
||||
<transition v-if="stateReady" name="fade" mode="out-in">
|
||||
<div key="2">
|
||||
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
|
||||
@close="showInitialFormModal=false"
|
||||
/>
|
||||
<form-editor v-if="!workspacesLoading" ref="editor"
|
||||
class="w-full flex flex-grow"
|
||||
:error="error"
|
||||
@on-save="formInitialHash=null"
|
||||
/>
|
||||
<div v-else class="text-center mt-4 py-6">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Form from 'vform'
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { useTemplatesStore } from '../../stores/templates'
|
||||
import { useWorkingFormStore } from '../../stores/working_form'
|
||||
import { useWorkspacesStore } from '../../stores/workspaces'
|
||||
import initForm from '../../mixins/form_editor/initForm.js'
|
||||
import SeoMeta from '../../mixins/seo-meta.js'
|
||||
import CreateFormBaseModal from '../../components/pages/forms/create/CreateFormBaseModal.vue'
|
||||
|
||||
const loadTemplates = function () {
|
||||
const templatesStore = useTemplatesStore()
|
||||
templatesStore.startLoading()
|
||||
templatesStore.loadIfEmpty().then(() => {
|
||||
templatesStore.stopLoading()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'CreateForm',
|
||||
components: { CreateFormBaseModal },
|
||||
|
||||
mixins: [initForm, SeoMeta],
|
||||
middleware: 'auth',
|
||||
|
||||
beforeRouteEnter (to, from, next) {
|
||||
loadTemplates()
|
||||
next()
|
||||
},
|
||||
|
||||
beforeRouteLeave (to, from, next) {
|
||||
if (this.isDirty()) {
|
||||
return this.alertConfirm('Changes you made may not be saved. Are you sure want to leave?', () => {
|
||||
window.onbeforeunload = null
|
||||
next()
|
||||
}, () => {})
|
||||
}
|
||||
next()
|
||||
},
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
const templatesStore = useTemplatesStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
return {
|
||||
templatesStore,
|
||||
workingFormStore,
|
||||
workspacesStore,
|
||||
user: computed(() => authStore.user),
|
||||
workspaces : computed(() => workspacesStore.content),
|
||||
workspacesLoading : computed(() => workspacesStore.loading)
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
metaTitle: 'Create a new Form',
|
||||
stateReady: false,
|
||||
loading: false,
|
||||
error: '',
|
||||
showInitialFormModal: false,
|
||||
formInitialHash: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
form: {
|
||||
get () {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set (value) {
|
||||
this.workingFormStore.set(value)
|
||||
}
|
||||
},
|
||||
workspace () {
|
||||
return this.workspacesStore.getCurrent()
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
workspace () {
|
||||
if (this.workspace) {
|
||||
this.form.workspace_id = this.workspace.id
|
||||
}
|
||||
},
|
||||
user () {
|
||||
this.stateReady = true
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
window.onbeforeunload = () => {
|
||||
if (this.isDirty()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
this.initForm()
|
||||
this.formInitialHash = this.hashString(JSON.stringify(this.form.data()))
|
||||
if (this.$route.query.template !== undefined && this.$route.query.template) {
|
||||
const template = this.templatesStore.getBySlug(this.$route.query.template)
|
||||
if (template && template.structure) {
|
||||
this.form = new Form({ ...this.form.data(), ...template.structure })
|
||||
}
|
||||
} else {
|
||||
// No template loaded, ask how to start
|
||||
this.showInitialFormModal = true
|
||||
}
|
||||
this.closeAlert()
|
||||
this.workspacesStore.loadIfEmpty()
|
||||
|
||||
this.stateReady = this.user !== null
|
||||
},
|
||||
|
||||
created () {},
|
||||
unmounted () {},
|
||||
|
||||
methods: {
|
||||
formGenerated (form) {
|
||||
this.form = new Form({ ...this.form.data(), ...form })
|
||||
},
|
||||
isDirty () {
|
||||
return !this.loading && this.formInitialHash && this.formInitialHash !== this.hashString(JSON.stringify(this.form.data()))
|
||||
},
|
||||
hashString (str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed
|
||||
let h2 = 0x41c6ce57 ^ seed
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i)
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761)
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677)
|
||||
}
|
||||
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
||||
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
148
client/pages/forms/edit.vue
Normal file
148
client/pages/forms/edit.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-col">
|
||||
<form-editor v-if="pageLoaded" ref="editor"
|
||||
:is-edit="true"
|
||||
@on-save="formInitialHash=null"
|
||||
/>
|
||||
<div v-else-if="!loading && error" class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else class="text-center mt-4 py-6">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import Form from 'vform'
|
||||
import { useFormsStore } from '../../stores/forms'
|
||||
import { useWorkingFormStore } from '../../stores/working_form'
|
||||
import { useWorkspacesStore } from '../../stores/workspaces'
|
||||
import Breadcrumb from '~/components/global/Breadcrumb.vue'
|
||||
import SeoMeta from '../../mixins/seo-meta.js'
|
||||
|
||||
const loadForms = function () {
|
||||
const formsStore = useFormsStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
formsStore.startLoading()
|
||||
workspacesStore.loadIfEmpty().then(() => {
|
||||
formsStore.load(workspacesStore.currentId)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'EditForm',
|
||||
components: { Breadcrumb },
|
||||
mixins: [SeoMeta],
|
||||
middleware: 'auth',
|
||||
|
||||
beforeRouteEnter (to, from, next) {
|
||||
const formsStore = useFormsStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
if (!formsStore.getBySlug(to.params.slug)) {
|
||||
loadForms()
|
||||
}
|
||||
workingFormStore.set(null) // Reset old working form
|
||||
next()
|
||||
},
|
||||
|
||||
beforeRouteLeave (to, from, next) {
|
||||
if (this.isDirty()) {
|
||||
return this.alertConfirm('Changes you made may not be saved. Are you sure want to leave?', () => {
|
||||
window.onbeforeunload = null
|
||||
next()
|
||||
}, () => {})
|
||||
}
|
||||
next()
|
||||
},
|
||||
|
||||
setup () {
|
||||
const formsStore = useFormsStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
return {
|
||||
formsStore,
|
||||
workingFormStore,
|
||||
workspacesStore,
|
||||
formsLoading : computed(() => formsStore.loading)
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
error: null,
|
||||
formInitialHash: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
updatedForm: {
|
||||
get () {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set (value) {
|
||||
this.workingFormStore.set(value)
|
||||
}
|
||||
},
|
||||
form () {
|
||||
return this.formsStore.getBySlug(this.$route.params.slug)
|
||||
},
|
||||
pageLoaded () {
|
||||
return !this.loading && this.updatedForm !== null
|
||||
},
|
||||
metaTitle () {
|
||||
return 'Edit ' + (this.form ? this.form.title : 'Your Form')
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
form () {
|
||||
this.updatedForm = new Form(this.form)
|
||||
}
|
||||
},
|
||||
|
||||
created () {},
|
||||
unmounted () {},
|
||||
|
||||
mounted () {
|
||||
window.onbeforeunload = () => {
|
||||
if (this.isDirty()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
this.closeAlert()
|
||||
if (!this.form) {
|
||||
loadForms()
|
||||
} else {
|
||||
this.updatedForm = new Form(this.form)
|
||||
this.formInitialHash = this.hashString(JSON.stringify(this.updatedForm.data()))
|
||||
}
|
||||
|
||||
if (this.updatedForm && (!this.updatedForm.notification_settings || Array.isArray(this.updatedForm.notification_settings))) {
|
||||
this.updatedForm.notification_settings = {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
isDirty () {
|
||||
return !this.loading && this.formInitialHash && this.formInitialHash !== this.hashString(JSON.stringify(this.updatedForm.data()))
|
||||
},
|
||||
hashString (str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed
|
||||
let h2 = 0x41c6ce57 ^ seed
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i)
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761)
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677)
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
221
client/pages/forms/show-public.vue
Normal file
221
client/pages/forms/show-public.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div v-if="form && !isIframe && (form.logo_picture || form.cover_picture)">
|
||||
<div v-if="form.cover_picture">
|
||||
<div id="cover-picture" class="max-h-56 w-full overflow-hidden flex items-center justify-center">
|
||||
<img alt="Form Cover Picture" :src="form.cover_picture" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="form.logo_picture" class="w-full p-5 relative mx-auto"
|
||||
:class="{'pt-20':!form.cover_picture, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form.width === 'centered', 'max-w-7xl': (form.width === 'full' && !isIframe) }"
|
||||
>
|
||||
<img alt="Logo Picture" :src="form.logo_picture"
|
||||
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
|
||||
class="w-20 h-20 object-contain absolute left-5 transition-all"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full mx-auto px-4"
|
||||
:class="{'mt-6':!isIframe, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form && (form.width === 'centered'), 'max-w-7xl': (form && form.width === 'full' && !isIframe)}"
|
||||
>
|
||||
<div v-if="!formLoading && !form">
|
||||
<h1 class="mt-6" v-text="'Whoops'" />
|
||||
<p class="mt-6">
|
||||
Unfortunately we could not find this form. It may have been deleted by it's author.
|
||||
</p>
|
||||
<p class="mb-10 mt-4">
|
||||
<router-link :to="{name:'index'}">
|
||||
Create your form for free with OpnForm
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="formLoading">
|
||||
<p class="text-center mt-6 p-4">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="recordLoading">
|
||||
<p class="text-center mt-6 p-4">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</p>
|
||||
</div>
|
||||
<open-complete-form v-show="!recordLoading" ref="open-complete-form" :form="form" class="mb-10"
|
||||
@password-entered="passwordEntered"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { computed } from 'vue'
|
||||
import { useFormsStore } from '../../stores/forms'
|
||||
import { useRecordsStore } from '../../stores/records'
|
||||
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
|
||||
import Cookies from 'js-cookie'
|
||||
import sha256 from 'js-sha256'
|
||||
import SeoMeta from '../../mixins/seo-meta.js'
|
||||
|
||||
const isFrame = window.location !== window.parent.location || window.frameElement
|
||||
|
||||
function handleDarkMode (form) {
|
||||
// Dark mode
|
||||
const body = document.body
|
||||
if (form.dark_mode === 'dark') {
|
||||
body.classList.add('dark')
|
||||
} else if (form.dark_mode === 'light') {
|
||||
body.classList.remove('dark')
|
||||
} else if (form.dark_mode === 'auto' && isFrame) {
|
||||
// Remove dark mode if embed in a notion basic site
|
||||
let parentUrl
|
||||
try {
|
||||
parentUrl = window.location.ancestorOrigins[0]
|
||||
} catch (e) {
|
||||
parentUrl = (window.location !== window.parent.location)
|
||||
? document.referrer
|
||||
: document.location.href
|
||||
}
|
||||
if (parentUrl.includes('.notion.site')) {
|
||||
body.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTransparentMode (form) {
|
||||
const isFrame = window.location !== window.parent.location || window.frameElement
|
||||
if (!isFrame || !form.transparent_background) return
|
||||
|
||||
const app = document.getElementById('app')
|
||||
app.classList.remove('bg-white')
|
||||
app.classList.remove('dark:bg-notion-dark')
|
||||
app.classList.add('bg-transparent')
|
||||
}
|
||||
|
||||
function loadForm (slug) {
|
||||
const formsStore = useFormsStore()
|
||||
if (formsStore.loading) return
|
||||
formsStore.startLoading()
|
||||
return axios.get('/api/forms/' + slug).then((response) => {
|
||||
const form = response.data
|
||||
formsStore.set([response.data])
|
||||
|
||||
// Custom code injection
|
||||
if (form.custom_code) {
|
||||
const scriptEl = document.createRange().createContextualFragment(form.custom_code)
|
||||
document.head.append(scriptEl)
|
||||
}
|
||||
|
||||
handleDarkMode(form)
|
||||
handleTransparentMode(form)
|
||||
|
||||
formsStore.stopLoading()
|
||||
}).catch(() => {
|
||||
formsStore.stopLoading()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { OpenCompleteForm },
|
||||
mixins: [SeoMeta],
|
||||
|
||||
beforeRouteEnter (to, from, next) {
|
||||
if (window.$crisp) {
|
||||
window.$crisp.push(['do', 'chat:hide'])
|
||||
}
|
||||
next()
|
||||
},
|
||||
|
||||
beforeRouteLeave (to, from, next) {
|
||||
if (window.$crisp) {
|
||||
window.$crisp.push(['do', 'chat:show'])
|
||||
}
|
||||
next()
|
||||
},
|
||||
|
||||
setup () {
|
||||
const formsStore = useFormsStore()
|
||||
const recordsStore = useRecordsStore()
|
||||
return {
|
||||
formsStore,
|
||||
forms : computed(() => formsStore.content),
|
||||
formLoading : computed(() => formsStore.loading),
|
||||
recordLoading : computed(() => recordsStore.loading)
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
submitted: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
loadForm(this.formSlug).then(() => {
|
||||
if (this.isIframe) return
|
||||
// Auto focus on first input
|
||||
const visibleElements = []
|
||||
document.querySelectorAll('input,button,textarea,[role="button"]').forEach(ele => {
|
||||
if (ele.offsetWidth !== 0 || ele.offsetHeight !== 0) {
|
||||
visibleElements.push(ele)
|
||||
}
|
||||
})
|
||||
if (visibleElements.length > 0) {
|
||||
visibleElements[0].focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
passwordEntered (password) {
|
||||
Cookies.set('password-' + this.form.slug, sha256(password), { expires: 7, sameSite: 'None', secure: true })
|
||||
loadForm(this.formSlug).then(() => {
|
||||
if (this.form.is_password_protected) {
|
||||
this.$refs['open-complete-form'].addPasswordError('Invalid password.')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
formSlug () {
|
||||
return this.$route.params.slug
|
||||
},
|
||||
form () {
|
||||
return this.formsStore.getBySlug(this.formSlug)
|
||||
},
|
||||
isIframe () {
|
||||
return window.location !== window.parent.location || window.frameElement
|
||||
},
|
||||
metaTitle () {
|
||||
if(this.form && this.form.is_pro && this.form.seo_meta.page_title) {
|
||||
return this.form.seo_meta.page_title
|
||||
}
|
||||
return this.form ? this.form.title : 'Create beautiful forms'
|
||||
},
|
||||
metaTemplate () {
|
||||
if (this.form && this.form.is_pro && this.form.seo_meta.page_title) {
|
||||
// Disable template if custom SEO title
|
||||
return '%s'
|
||||
}
|
||||
return null
|
||||
},
|
||||
metaDescription () {
|
||||
if (this.form && this.form.is_pro && this.form.seo_meta.page_description) {
|
||||
return this.form.seo_meta.page_description
|
||||
}
|
||||
return (this.form && this.form.description) ? this.form.description.substring(0, 160) : null
|
||||
},
|
||||
metaImage () {
|
||||
if (this.form && this.form.is_pro && this.form.seo_meta.page_thumbnail) {
|
||||
return this.form.seo_meta.page_thumbnail
|
||||
}
|
||||
return (this.form && this.form.cover_picture) ? this.form.cover_picture : null
|
||||
},
|
||||
metaTags () {
|
||||
return (this.form && this.form.can_be_indexed) ? [] : [{ name: 'robots', content: 'noindex' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
270
client/pages/forms/show/index.vue
Normal file
270
client/pages/forms/show/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="bg-white">
|
||||
<template v-if="form">
|
||||
<div class="flex bg-gray-50">
|
||||
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
|
||||
<div class="pt-4 pb-0">
|
||||
<a href="#" class="flex text-blue mb-2 font-semibold text-sm" @click.prevent="goBack">
|
||||
<svg class="w-3 h-3 text-blue mt-1 mr-1" viewBox="0 0 6 10" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Go back
|
||||
</a>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<h2 class="flex-grow text-gray-900 truncate">
|
||||
{{ form.title }}
|
||||
</h2>
|
||||
<div class="flex">
|
||||
<extra-menu :form="form" />
|
||||
|
||||
<v-button v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" target="_blank"
|
||||
:href="form.share_url" color="white"
|
||||
class="mr-2 text-blue-600 hidden sm:block"
|
||||
>
|
||||
<svg class="w-6 h-6 inline -mt-1" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</v-button>
|
||||
<v-button class="text-white" @click="openEdit">
|
||||
<svg class="inline mr-1 -mt-1" width="18" height="17" viewBox="0 0 18 17" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.99998 15.6662H16.5M1.5 15.6662H2.89545C3.3031 15.6662 3.50693 15.6662 3.69874 15.6202C3.8688 15.5793 4.03138 15.512 4.1805 15.4206C4.34869 15.3175 4.49282 15.1734 4.78107 14.8852L15.25 4.4162C15.9404 3.72585 15.9404 2.60656 15.25 1.9162C14.5597 1.22585 13.4404 1.22585 12.75 1.9162L2.28105 12.3852C1.9928 12.6734 1.84867 12.8175 1.7456 12.9857C1.65422 13.1348 1.58688 13.2974 1.54605 13.4675C1.5 13.6593 1.5 13.8631 1.5 14.2708V15.6662Z"
|
||||
stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Edit form
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-500 text-sm">
|
||||
<span class="pr-1">{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}</span>
|
||||
<span class="pr-1">- {{ form.submissions_count }}
|
||||
submission{{ form.submissions_count > 0 ? 's' : '' }}
|
||||
</span>
|
||||
<span>- Edited {{ form.last_edited_human }}</span>
|
||||
</p>
|
||||
<div v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)"
|
||||
class="mt-2 flex items-center flex-wrap gap-3"
|
||||
>
|
||||
<span v-if="form.visibility=='draft'"
|
||||
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
Draft - not publicly accessible
|
||||
</span>
|
||||
<span v-else-if="form.visibility=='closed'"
|
||||
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
Closed - won't accept new submissions
|
||||
</span>
|
||||
<span v-for="(tag,i) in form.tags" :key="tag"
|
||||
class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="form.closes_at" class="text-yellow-500">
|
||||
<span v-if="form.is_closed"> This form stopped accepting submissions on the {{
|
||||
displayClosesDate
|
||||
}} </span>
|
||||
<span v-else> This form will stop accepting submissions on the {{ displayClosesDate }} </span>
|
||||
</p>
|
||||
<p v-if="form.max_submissions_count > 0" class="text-yellow-500">
|
||||
<span v-if="form.max_number_of_submissions_reached"> The form is now closed because it reached its limit of {{
|
||||
form.max_submissions_count
|
||||
}} submissions. </span>
|
||||
<span v-else> This form will stop accepting submissions after {{ form.max_submissions_count }} submissions. </span>
|
||||
</p>
|
||||
|
||||
<form-cleanings class="mt-4" :form="form" />
|
||||
|
||||
<div class="border-b border-gray-200 dark:border-gray-700">
|
||||
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
|
||||
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
|
||||
<router-link :to="{ name: tab.route }"
|
||||
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
|
||||
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex bg-white">
|
||||
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
|
||||
<div class="py-4">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page" mode="out-in">
|
||||
<component :is="Component" :form="form" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="loading" class="text-center w-full p-5">
|
||||
<loader class="h-6 w-6 mx-auto" />
|
||||
</div>
|
||||
<div v-else class="text-center w-full p-5">
|
||||
Form not found.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import Form from 'vform'
|
||||
import { useAuthStore } from '../../../stores/auth'
|
||||
import { useFormsStore } from '../../../stores/forms'
|
||||
import { useWorkingFormStore } from '../../../stores/working_form'
|
||||
import { useWorkspacesStore } from '../../../stores/workspaces'
|
||||
import ProTag from '~/components/global/ProTag.vue'
|
||||
import VButton from '~/components/global/VButton.vue'
|
||||
import ExtraMenu from '../../../components/pages/forms/show/ExtraMenu.vue'
|
||||
import SeoMeta from '../../../mixins/seo-meta.js'
|
||||
import FormCleanings from '../../../components/pages/forms/show/FormCleanings.vue'
|
||||
|
||||
const loadForms = function () {
|
||||
const formsStore = useFormsStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
formsStore.startLoading()
|
||||
workspacesStore.loadIfEmpty().then(() => {
|
||||
formsStore.loadIfEmpty(workspacesStore.currentId)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ShowForm',
|
||||
components: {
|
||||
VButton,
|
||||
ProTag,
|
||||
ExtraMenu,
|
||||
FormCleanings
|
||||
},
|
||||
mixins: [SeoMeta],
|
||||
|
||||
beforeRouteEnter (to, from, next) {
|
||||
loadForms()
|
||||
next()
|
||||
},
|
||||
|
||||
beforeRouteLeave (to, from, next) {
|
||||
this.workingForm = null
|
||||
next()
|
||||
},
|
||||
middleware: 'auth',
|
||||
|
||||
setup () {
|
||||
const authStore = useAuthStore()
|
||||
const formsStore = useFormsStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
return {
|
||||
formsStore,
|
||||
workingFormStore,
|
||||
workspacesStore,
|
||||
user: computed(() => authStore.user),
|
||||
formsLoading: computed(() => formsStore.loading),
|
||||
workspacesLoading: computed(() => workspacesStore.loading)
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
metaTitle: 'Home',
|
||||
tabsList: [
|
||||
{
|
||||
name: 'Submissions',
|
||||
route: 'forms.show'
|
||||
},
|
||||
{
|
||||
name: 'Analytics',
|
||||
route: 'forms.show.analytics'
|
||||
},
|
||||
{
|
||||
name: 'Share',
|
||||
route: 'forms.show.share'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
workingForm: {
|
||||
get () {
|
||||
return this.workingFormStore.content
|
||||
},
|
||||
/* We add a setter */
|
||||
set (value) {
|
||||
this.workingFormStore.set(value)
|
||||
}
|
||||
},
|
||||
workspace () {
|
||||
if (!this.form) return null
|
||||
return this.workspacesStore.getById(this.form.workspace_id)
|
||||
},
|
||||
form () {
|
||||
return this.formsStore.getBySlug(this.$route.params.slug)
|
||||
},
|
||||
formEndpoint: () => '/api/open/forms/{id}',
|
||||
loading () {
|
||||
return this.formsLoading || this.workspacesLoading
|
||||
},
|
||||
displayClosesDate () {
|
||||
if (this.form.closes_at) {
|
||||
const dateObj = new Date(this.form.closes_at)
|
||||
return dateObj.getFullYear() + '-' +
|
||||
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(dateObj.getDate()).padStart(2, '0') + ' ' +
|
||||
String(dateObj.getHours()).padStart(2, '0') + ':' +
|
||||
String(dateObj.getMinutes()).padStart(2, '0')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
form () {
|
||||
this.workingForm = new Form(this.form)
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.form) {
|
||||
this.workingForm = new Form(this.form)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openCrisp () {
|
||||
window.$crisp.push(['do', 'chat:show'])
|
||||
window.$crisp.push(['do', 'chat:open'])
|
||||
},
|
||||
goBack () {
|
||||
this.$router.push({ name: 'home' })
|
||||
},
|
||||
openEdit () {
|
||||
this.$router.push({ name: 'forms.edit', params: { slug: this.form.slug } })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
70
client/pages/forms/show/share.vue
Normal file
70
client/pages/forms/show/share.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<share-link class="mt-4" :form="form" :extra-query-param="shareUrlForQueryParams" />
|
||||
|
||||
<embed-code class="mt-6" :form="form" :extra-query-param="shareUrlForQueryParams" />
|
||||
|
||||
<form-qr-code class="mt-6" :form="form" :extra-query-param="shareUrlForQueryParams" />
|
||||
|
||||
<advanced-form-url-settings :form="form" v-model="shareFormConfig" />
|
||||
|
||||
<div class="mt-6 pt-6 border-t w-full flex">
|
||||
<regenerate-form-link class="sm:w-1/2 mr-4" :form="form" />
|
||||
|
||||
<url-form-prefill class="sm:w-1/2 mr-4" :form="form" :extra-query-param="shareUrlForQueryParams" />
|
||||
|
||||
<embed-form-as-popup-modal class="sm:w-1/2" :form="form" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ShareLink from '../../../components/pages/forms/show/ShareLink.vue'
|
||||
import EmbedCode from '../../../components/pages/forms/show/EmbedCode.vue'
|
||||
import FormQrCode from '../../../components/pages/forms/show/FormQrCode.vue'
|
||||
import UrlFormPrefill from '../../../components/pages/forms/show/UrlFormPrefill.vue'
|
||||
import RegenerateFormLink from '../../../components/pages/forms/show/RegenerateFormLink.vue'
|
||||
import SeoMeta from '../../../mixins/seo-meta.js'
|
||||
import AdvancedFormUrlSettings from '../../../components/open/forms/components/AdvancedFormUrlSettings.vue'
|
||||
import EmbedFormAsPopupModal from '../../../components/pages/forms/show/EmbedFormAsPopupModal.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ShareLink,
|
||||
EmbedCode,
|
||||
FormQrCode,
|
||||
UrlFormPrefill,
|
||||
RegenerateFormLink,
|
||||
AdvancedFormUrlSettings,
|
||||
EmbedFormAsPopupModal
|
||||
},
|
||||
props: {
|
||||
form: { type: Object, required: true }
|
||||
},
|
||||
mixins: [SeoMeta],
|
||||
|
||||
data: () => ({
|
||||
shareFormConfig: {
|
||||
hide_title: false,
|
||||
auto_submit: false
|
||||
}
|
||||
}),
|
||||
|
||||
mounted() {},
|
||||
|
||||
computed: {
|
||||
metaTitle() {
|
||||
return (this.form) ? 'Form Share - '+this.form.title : 'Form Share'
|
||||
},
|
||||
shareUrlForQueryParams () {
|
||||
let queryStr = ''
|
||||
for (const [key, value] of Object.entries(this.shareFormConfig)) {
|
||||
if(value && value !== 'false' && value !== false){
|
||||
queryStr += '&' + encodeURIComponent(key) + "=" + encodeURIComponent(value)
|
||||
}
|
||||
}
|
||||
return queryStr.slice(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
30
client/pages/forms/show/stats.vue
Normal file
30
client/pages/forms/show/stats.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="font-semibold mt-4 text-xl">
|
||||
Form Analytics (last 30 days)
|
||||
</h3>
|
||||
<form-stats :form="form"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormStats from '../../../components/open/forms/components/FormStats.vue'
|
||||
import SeoMeta from '../../../mixins/seo-meta.js'
|
||||
|
||||
export default {
|
||||
name: 'Stats',
|
||||
components: {FormStats},
|
||||
props: {
|
||||
form: {type: Object, required: true},
|
||||
},
|
||||
mixins: [SeoMeta],
|
||||
|
||||
data: () => ({}),
|
||||
|
||||
computed: {
|
||||
metaTitle() {
|
||||
return (this.form ? ('Form Analytics - ' + this.form.title) : 'Form Analytics')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
client/pages/forms/show/submissions.vue
Normal file
31
client/pages/forms/show/submissions.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div id="table-page">
|
||||
<form-submissions/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSubmissions from '../../../components/open/forms/components/FormSubmissions.vue'
|
||||
import SeoMeta from '../../../mixins/seo-meta.js'
|
||||
|
||||
export default {
|
||||
components: {FormSubmissions},
|
||||
props: {
|
||||
form: {type: Object, required: true}
|
||||
},
|
||||
mixins: [SeoMeta],
|
||||
|
||||
data: () => ({}),
|
||||
|
||||
mounted() {
|
||||
},
|
||||
|
||||
computed: {
|
||||
metaTitle() {
|
||||
return (this.form) ? 'Form Submissions - ' + this.form.title : 'Form Submissions'
|
||||
},
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user