Initial commit

This commit is contained in:
Julien Nahum
2022-09-20 21:59:52 +02:00
commit f8e6cd4dd6
479 changed files with 77078 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
<template>
<div class="flex flex-wrap flex-col">
<!-- Step 1: Select Database -->
<div ref="progress" class="w-full px-4 " :class="{
'md:mx-auto md:max-w-5xl':currentStep===0}"
>
<div class="flex items-center justify-between pb-2">
<v-button v-if="currentStep>0" color="gray" shade="light" class="hidden md:block mx-4 flex-shrink-0"
@click="goBack"
>
Previous
</v-button>
<v-button v-if="currentStep>0" :loading="loading || createFormLoading" color="nt-blue"
class="v-last-step hidden md:block mx-4 flex-shrink-0"
@click="nextStep"
>
{{ currentStep !== 1 ? 'Continue' : 'Create Form' }}
</v-button>
</div>
</div>
<transition v-if="stateReady" name="fade" mode="out-in">
<!-- Step1: Form Customization -->
<div v-if="currentStep===1" key="2">
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex border-t flex-grow"
:style="{
'max-height': editorMaxHeight + 'px'
}" :error="error"
:validation-error-response="validationErrorResponse"
@mounted="onResize"
/>
<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 v-if="currentStep===1" class="md:hidden pt-4 mb-16 px-6 border-t flex justify-between">
<v-button color="gray" shade="light" class="mt-2" @click="previousStep">
Previous
</v-button>
<v-button v-track.create_form_click :loading="createFormLoading" color="nt-blue" class="mt-2 px-5 v-last-step"
@click="nextStep"
>
Create Form
</v-button>
</div>
</div>
</template>
<script>
import Form from 'vform'
import {mapState, mapActions} from 'vuex'
import saveUpdateAlert from '../../mixins/forms/saveUpdateAlert'
import clonedeep from 'clone-deep'
const FormEditor = () => import('../../components/open/forms/components/FormEditor')
export default {
name: 'CreateForm',
components: {
FormEditor,
},
metaInfo() {
return {title: 'Create a new Form'}
},
mixins: [saveUpdateAlert],
middleware: 'auth',
data() {
return {
stateReady: false,
validationErrorResponse: null,
loading: false,
createFormLoading: false,
error: '',
createdFormId: null,
currentStep: 1,
editorMaxHeight: 500
}
},
computed: {
...mapState({
workspaces: state => state['open/workspaces'].content,
workspacesLoading: state => state['open/workspaces'].loading,
user: state => state.auth.user
}),
form: {
get() {
return this.$store.state['open/working_form'].content
},
/* We add a setter */
set(value) {
this.$store.commit('open/working_form/set', value)
}
},
workspace() {
return this.$store.getters['open/workspaces/getCurrent']()
},
createdForm() {
return this.$store.getters['open/forms/getById'](this.createdFormId)
},
fromOnboarding() {
return this.$route.params.from_onboarding
},
fbGroupLink() {
return window.config.links.facebook_group
}
},
watch: {
workspace() {
if (this.workspace) {
this.form.workspace_id = this.workspace.id
}
},
user() {
this.stateReady = true
}
},
mounted() {
this.initForm()
this.closeAlert()
this.loadWorkspaces()
this.stateReady = this.user !== null
},
created() {
window.addEventListener('resize', this.onResize)
},
destroyed() {
window.removeEventListener('resize', this.onResize)
},
methods: {
...mapActions({
loadWorkspaces: 'open/workspaces/loadIfEmpty'
}),
initForm() {
this.form = new Form({
title: 'My Form',
description: null,
workspace_id: this.workspace?.id,
properties: [],
notifies: false,
send_submission_confirmation: false,
webhook_url: null,
// Customization
theme: 'default',
width: 'centered',
dark_mode: 'auto',
color: '#3B82F6',
hide_title: false,
no_branding: false,
uppercase_labels: true,
transparent_background: false,
closes_at: null,
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
// Submission
submit_button_text: 'Submit',
re_fillable: false,
re_fill_button_text: 'Fill Again',
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
notification_sender: 'OpnForm',
notification_subject: 'We saved your answers',
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
notifications_include_submission: true,
use_captcha: false,
is_rating: false,
rating_max_value: 5,
max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
// Security & Privacy
can_be_indexed: true
})
},
nextStep() {
this.error = ''
if (this.currentStep === 0) {
this.form.workspace = clonedeep(this.workspace)
// Init editor max height
this.currentStep++
this.$nextTick(() => {
this.editorMaxHeight = window.innerHeight - (this.$refs.progress.offsetTop + this.$refs.progress.offsetHeight)
})
return
} else if (this.currentStep === 1) {
return this.submit()
}
this.currentStep++
},
submit() {
if (this.loading) return
this.form.workspace_id = this.workspace.id
this.validationErrorResponse = null
this.createFormLoading = true
this.form.post('/api/open/forms').then((response) => {
this.$store.commit('open/forms/addOrUpdate', response.data.form)
this.createdFormId = response.data.form.id
this.$logEvent('form_created', {form_id: response.data.form.id, form_slug: response.data.form.slug})
this.$getCrisp().push(['set', 'session:event', [[['form_created', {
form_id: response.data.form.id,
form_slug: response.data.form.slug
}, 'blue']]]])
this.displayFormModificationAlert(response.data)
this.$router.push({
name: 'forms.show',
params: {
slug: this.createdForm.slug,
new_form: response.data.users_first_form
}
})
}).catch((error) => {
if (error.response && error.response.status === 422) {
this.validationErrorResponse = error.response.data
this.$refs.editor.showValidationErrors()
}
}).finally(() => {
this.createFormLoading = false
})
},
previousStep() {
if (this.currentStep > 0) {
this.currentStep--
}
},
/**
* Compute max height of editor
*/
onResize() {
if (this.$refs.editor) {
this.editorMaxHeight = window.innerHeight - this.$refs.editor.$el.offsetTop
}
},
goBack() {
return this.$router.back();
}
}
}
</script>

View File

@@ -0,0 +1,161 @@
<template>
<div class="w-full flex flex-col">
<div class="flex justify-center md:justify-between pb-2 px-2">
<div class="hidden md:block" />
<breadcrumb class="hidden md:flex sm:px-6 mx-auto max-w-lg" :path="breadcrumbs" />
<v-button v-if="!loading && updatedForm"
v-track.save_form_click
lass="hidden md:block"
:loading="updateFormLoading" @click="saveForm"
>
Save changes
</v-button>
</div>
<form-editor v-if="pageLoaded" ref="editor"
:style="{
'max-height': editorMaxHeight + 'px'
}"
:validation-error-response="validationErrorResponse"
@mounted="onResize"
/>
<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 axios from 'axios'
import store from '~/store'
import Breadcrumb from '../../components/common/Breadcrumb'
import Form from 'vform'
import saveUpdateAlert from '../../mixins/forms/saveUpdateAlert'
import { mapState } from 'vuex'
const FormEditor = () => import('../../components/open/forms/components/FormEditor')
const loadForms = function () {
store.commit('open/forms/startLoading')
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
store.dispatch('open/forms/load', store.state['open/workspaces'].currentId)
})
}
export default {
name: 'EditForm',
components: { Breadcrumb, FormEditor },
mixins: [saveUpdateAlert],
beforeRouteEnter (to, from, next) {
if (!store.getters['open/forms/getBySlug'](to.params.slug)) {
loadForms()
}
next()
},
middleware: 'auth',
data () {
return {
loading: false,
updateFormLoading: false,
error: null,
validationErrorResponse: null,
editorMaxHeight: 500
}
},
computed: {
...mapState({
formsLoading: state => state['open/forms'].loading
}),
updatedForm: {
get () {
return this.$store.state['open/working_form'].content
},
/* We add a setter */
set (value) {
this.$store.commit('open/working_form/set', value)
}
},
form () {
return this.$store.getters['open/forms/getBySlug'](this.$route.params.slug)
},
breadcrumbs () {
if (!this.form) {
return [{ route: { name: 'home' }, label: 'Your Forms' }]
}
return [
{ route: { name: 'home' }, label: 'Your Forms' },
{ label: this.form ? this.form.title : 'Your Form', route: { name: 'forms.show', params: { slug: this.form.slug } } },
{ label: 'Edit' }
]
},
formEndpoint: () => '/api/open/forms/{id}/',
pageLoaded () {
return !this.loading && this.updatedForm !== null
}
},
watch: {
form () {
this.updatedForm = new Form(this.form)
}
},
created () {
window.addEventListener('resize', this.onResize)
},
destroyed () {
window.removeEventListener('resize', this.onResize)
},
mounted () {
this.closeAlert()
if (!this.form) {
loadForms()
} else {
this.updatedForm = new Form(this.form)
}
},
metaInfo () {
return { title: 'Edit ' + (this.form ? this.form.title : 'Your Form') }
},
methods: {
saveForm () {
if (this.updateFormLoading) return
this.updateFormLoading = true
this.validationErrorResponse = null
this.updatedForm.put(this.formEndpoint.replace('{id}', this.form.id)).then((response) => {
const data = response.data
this.$store.commit('open/forms/addOrUpdate', data.form)
this.$router.push({ name: 'forms.show', params: { slug: this.form.slug } })
this.$logEvent('form_saved', { form_id: this.form.id, form_slug: this.form.slug })
this.displayFormModificationAlert(data)
}).catch((error) => {
if (error.response.status === 422) {
this.validationErrorResponse = error.response.data
this.$refs.editor.showValidationErrors()
}
}).finally(() => {
this.updateFormLoading = false
})
},
/**
* Compute max height of editor
*/
onResize () {
if (this.$refs.editor) {
this.editorMaxHeight = Math.max(500, window.innerHeight - this.$refs.editor.$el.offsetTop)
}
}
}
}
</script>

View File

@@ -0,0 +1,180 @@
<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 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:'welcome'}">
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>
<open-complete-form v-else ref="open-complete-form" :form="form" class="mb-10" @password-entered="passwordEntered" />
</div>
</div>
</template>
<script>
import axios from 'axios'
import store from '~/store'
import { mapState } from 'vuex'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm'
import Cookies from 'js-cookie'
import sha256 from 'js-sha256'
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) {
if (store.state['open/forms'].loading) return
store.commit('open/forms/startLoading')
return axios.get('/api/forms/' + slug).then((response) => {
const form = response.data
store.commit('open/forms/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)
store.commit('open/forms/stopLoading')
}).catch(() => {
store.commit('open/forms/stopLoading')
})
}
export default {
components: { OpenCompleteForm },
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()
},
data () {
return {
loading: false,
submitted: false
}
},
mounted () {
loadForm(this.formSlug).then(() => {
if (this.isIframe) return
// Auto focus on first input
const visibleElements = []
document.querySelectorAll('input,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 })
loadForm(this.formSlug).then(() => {
if (this.form.is_password_protected) {
this.$refs['open-complete-form'].addPasswordError('Invalid password.')
}
})
}
},
computed: {
...mapState({
forms: state => state['open/forms'].content,
formLoading: state => state['open/forms'].loading
}),
formSlug () {
return this.$route.params.slug
},
form () {
return this.$store.getters['open/forms/getBySlug'](this.formSlug)
},
isIframe () {
return window.location !== window.parent.location || window.frameElement
},
metaTitle () {
return this.form ? this.form.title : 'Create beautiful forms'
},
metaTags () {
return (this.form && this.form.can_be_indexed) ? [] : [{ name: 'robots', content: 'noindex' }]
}
}
}
</script>

View File

@@ -0,0 +1,462 @@
<template>
<div class="flex mt-6">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<breadcrumb class="sm:px-6" :path="breadcrumbs" />
<div v-if="form" class="sm:px-6">
<h2 class="text-nt-blue text-3xl font-bold z-10 mt-6 mb-3">
{{ form.title }}
</h2>
<p class="mb-3">
<span v-if="form.views_count">This form has been seen
<span class="font-semibold">{{ form.views_count }}</span> time{{ form.views_count > 0 ? 's' : '' }}
and it has received
<span class="font-semibold">{{ form.submissions_count }}</span> submission{{ form.submissions_count > 0 ? 's' : '' }}.</span>
</p>
<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>
<div class="flex justify-center">
<share-form-url :form="form" :link="true" />
</div>
<!-- Open Form -->
<div class="flex flex-wrap -mx-2">
<!-- Edit Form -->
<div class="w-full sm:w-1/2 px-2 flex">
<div v-track.edit_form_click="{form_id:form.id, form_slug:form.slug}"
class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 cursor-pointer hover:text-blue-500"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 "
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Edit form
</span>
<router-link :to="{name:'forms.edit',params:{slug:form.slug}}" class="absolute inset-0" />
</div>
</div>
<!-- Open Form -->
<div class="w-full sm:w-1/2 px-2 flex">
<div
v-track.open_form_click="{form_id:form.id, form_slug:form.slug}" class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700
hover:bg-blue-50 dark:hover:bg-blue-500 cursor-pointer hover:text-blue-500 dark:hover:text-white"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 "
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Open form
</span>
<a target="_blank" :href="form.share_url" class="absolute inset-0" />
</div>
</div>
<!-- Share/Embed form table -->
<div class="w-full sm:w-1/2 px-2 flex">
<div
v-track.share_embed_form_click="{form_id:form.id, form_slug:form.slug}"
class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 cursor-pointer hover:text-blue-500"
@click.prevent="showShareEmbedFormModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 "
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
/>
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Share/Embed form
</span>
</div>
</div>
<!-- Regenerate form link -->
<div class="w-full sm:w-1/2 px-2 flex">
<div v-track.regenerate_form_link_click="{form_id:form.id, form_slug:form.slug}"
class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 cursor-pointer hover:text-blue-500"
@click="showGenerateFormLinkModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 "
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Regenerate form link
</span>
</div>
</div>
<div class="w-full sm:w-1/2 px-2 flex">
<div v-track.url_form_prefill_click="{form_id:form.id, form_slug:form.slug}"
class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 cursor-pointer hover:text-blue-500"
@click="showUrlFormPrefillModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-7a2 2 0 012-2h2m3-4H9a2 2 0 00-2 2v7a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-1m-1 4l-3 3m0 0l-3-3m3 3V3"
/>
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Url form pre-fill <pro-tag class="ml-2" />
</span>
</div>
</div>
<div class="w-full sm:w-1/2 px-2 flex">
<div v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 cursor-pointer hover:text-blue-500"
@click="duplicateForm"
>
<template v-if="!loadingDuplicate">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 "
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
/>
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Duplicate form
</span>
</template>
<template v-else>
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</template>
</div>
</div>
<div class="w-full sm:w-1/2 px-2 flex mb-5">
<div v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
class="group relative transition-all mt-4 flex items-center p-3 px-6 w-full rounded-md bg-gray-50 dark:bg-gray-700 hover:bg-red-50 dark:hover:bg-red-900 cursor-pointer hover:text-red-500"
@click="alertConfirm('Do you really want to delete this form?',deleteForm)"
>
<template v-if="!loadingDelete">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span class="font-semibold group relative-hover:text-red-500">
Delete form
</span>
</template>
<loader v-else class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</div>
<!-- Form Submissions -->
<div class="pt-5 mt-5 border-t" id="table-page" v-if="form">
<form-submissions />
</div>
<!-- Form Analytics -->
<div class="pt-5 mt-5 border-t">
<h3 class="font-semibold">
Form Analytics (last 30 days)
</h3>
<form-stats :form="form" />
</div>
<!-- Share/Embed form modal -->
<modal :show="showShareEmbedFormModal" @close="showShareEmbedFormModal=false">
<div class="px-4">
<h2 class="text-nt-blue text-3xl font-bold mb-6">
Share/Embed your form
</h2>
<!-- Link -->
<h3 class="font-bold text-xl border-t pt-4">
Share
</h3>
<p>Share your form using the link below:</p>
<share-form-url :form="form" />
<!-- Embed -->
<h3 class="font-bold text-xl border-t pt-4">
Embed
</h3>
<p>
Embed your form on your website by copying the html code below.
</p>
<embed-form-code :form="form" />
<div class="flex justify-end mt-4">
<v-button color="gray" shade="light" @click="showShareEmbedFormModal=false">Close</v-button>
</div>
</div>
</modal>
<!-- Regenerate form link modal -->
<modal :show="showGenerateFormLinkModal" @close="showGenerateFormLinkModal=false">
<div class="-m-6">
<div class="p-6">
<h2 class="text-nt-blue text-3xl font-bold mb-6">
Generate new form link
</h2>
<p>
You can choose between two different URL formats for your form. <span class="font-semibold">Be careful, changing your form URL
is not a reversible operation</span>. Make sure to udpate your form URL everywhere where it's used.
</p>
</div>
<div class="border-t py-4 mt-4 px-6">
<h3 class="text-xl text-gray-700 font-semibold">
Human Readable URL
</h3>
<p>If your users are going to see this url, you might want to make nice and readable. Example:</p>
<p class="text-gray-600 p-4 bg-gray-100 rounded-md mt-4">
https://opnform.com/forms/contact
</p>
<div class="text-center mt-4">
<v-button :loading="loadingNewLink" @click="regenerateLink('slug')">
Generate a Human Readable URL
</v-button>
</div>
</div>
<div class="border-t pt-4 mt-4 px-6 pb-10">
<h3 class="text-xl text-gray-700 font-semibold">
Random ID URL
</h3>
<p>
If your user are not going to see your form url (if it's embedded), and if you prefer to have a random
non-guessable URL. Example:
</p>
<p class="text-gray-600 p-4 bg-gray-100 rounded-md mt-4">
https://opnform.com/forms/b4417f9c-34ae-4421-8006-832ee47786e7
</p>
<div class="text-center mt-4">
<v-button :loading="loadingNewLink" @click="regenerateLink('uuid')">
Generate a Random ID URL
</v-button>
</div>
</div>
<div class="flex justify-end mt-4 pb-5 px-6">
<v-button color="gray" shade="light" @click="showGenerateFormLinkModal=false">Close</v-button>
</div>
</div>
</modal>
<url-form-prefill-modal :form="form" :show="showUrlFormPrefillModal" @close="showUrlFormPrefillModal=false" />
</div>
<div v-else-if="loading" class="text-center w-full p-5">
<loader class="h-6 w-6 mx-auto" />
</div>
<div v-else>
Form not found.
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import store from '~/store'
import Form from 'vform'
import ShareFormUrl from '../../components/open/forms/components/ShareFormUrl'
import EmbedFormCode from '../../components/open/forms/components/EmbedFormCode'
import Breadcrumb from '../../components/common/Breadcrumb'
import { mapGetters, mapState } from 'vuex'
import ProTag from '../../components/common/ProTag'
import UrlFormPrefillModal from '../../components/pages/forms/UrlFormPrefillModal'
import FormStats from '../../components/open/forms/components/FormStats'
import FormSubmissions from '../../components/open/forms/components/FormSubmissions'
const loadForms = function () {
store.commit('open/forms/startLoading')
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
store.dispatch('open/forms/load', store.state['open/workspaces'].currentId)
})
}
export default {
name: 'EditForm',
components: { UrlFormPrefillModal, ProTag, Breadcrumb, ShareFormUrl, EmbedFormCode, FormStats, FormSubmissions },
beforeRouteEnter (to, from, next) {
loadForms()
next()
},
beforeRouteLeave (to, from, next) {
this.workingForm = null
next()
},
middleware: 'auth',
data () {
return {
loadingDuplicate: false,
loadingDelete: false,
loadingNewLink: false,
showNotionEmbedModal: false,
showShareEmbedFormModal: false,
showUrlFormPrefillModal: false,
showGenerateFormLinkModal: false
}
},
computed: {
...mapGetters({
user: 'auth/user'
}),
...mapState({
formsLoading: state => state['open/forms'].loading,
workspacesLoading: state => state['open/workspaces'].loading
}),
workingForm: {
get () {
return this.$store.state['open/working_form'].content
},
set (value) {
this.$store.commit('open/working_form/set', value)
}
},
workspace () {
if (!this.form) return null
return this.$store.getters['open/workspaces/getById'](this.form.workspace_id)
},
form () {
return this.$store.getters['open/forms/getBySlug'](this.$route.params.slug)
},
formEndpoint: () => '/api/open/forms/{id}',
breadcrumbs () {
if (!this.form) {
return [{ route: { name: 'home' }, label: 'Your Forms' }]
}
return [{ route: { name: 'home' }, label: 'Your Forms' }, { label: this.form.title }]
},
loading () {
return this.formsLoading || this.workspacesLoading
},
displayClosesDate(){
if(this.form.closes_at){
let 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 () {
this.updatedForm = new Form(this.form)
if (this.$route.params.hasOwnProperty('new_form') && this.$route.params.new_form) {
// if (!this.user.is_subscribed && !this.user.has_customer_id) {
// // Crisp offer
// this.$getCrisp().push(['set', 'session:event', [[['first_form_created', { form_id: this.form.id, form_slug: this.form.slug }, 'blue']]]])
//
// setTimeout(
// function () {
// window.$crisp.push(['do', 'chat:show'])
// window.$crisp.push(['do', 'chat:open'])
// window.$crisp.push([
// 'do',
// 'message:show',
// ['text',
// 'Hey there! I\m Julien the founder of NotionForms. Congrats on setting up your first OpnForm 🎉']
// ])
// setTimeout(
// function () {
// window.$crisp.push(['do', 'chat:show'])
// window.$crisp.push(['do', 'chat:open'])
// window.$crisp.push([
// 'do',
// 'message:show',
// ['text',
// 'A small gift to congratulate you? 🎁 I\'d be happy to offer you a 40% discount on your first month of a Pro subscription. Let me know if you\'re interested!']
// ])
// setTimeout(
// function () {
// window.$crisp.push(['do', 'chat:show'])
// window.$crisp.push(['do', 'chat:open'])
// window.$crisp.push([
// 'do',
// 'message:show',
// ['text',
// 'Just use the code "FIRSTFORM40" in the next 24 hours to get the discount! 🎉']
// ])
// }, 20000)
// }, 4000)
// }, 4000)
// }
}
},
metaInfo () {
return { title: this.$t('home') }
},
methods: {
openCrisp () {
window.$crisp.push(['do', 'chat:show'])
window.$crisp.push(['do', 'chat:open'])
},
duplicateForm () {
if (this.loadingDuplicate) return
this.loadingDuplicate = true
axios.post(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate').then((response) => {
this.$store.commit('open/forms/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
})
},
regenerateLink (option) {
if (this.loadingNewLink) return
this.loadingNewLink = true
axios.put(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option).then((response) => {
this.$store.commit('open/forms/addOrUpdate', response.data.form)
this.$router.push({ name: 'forms.show', params: { slug: response.data.form.slug } })
this.alertSuccess(response.data.message)
this.loadingNewLink = false
}).finally(() => {
this.showGenerateFormLinkModal = false
})
},
deleteForm () {
if (this.loadingDelete) return
this.loadingDelete = true
axios.delete(this.formEndpoint.replace('{id}', this.form.id)).then(() => {
this.$store.commit('open/forms/remove', this.form)
this.$router.push({ name: 'home' })
this.alertSuccess('Form was deleted.')
this.loadingDelete = false
})
}
}
}
</script>