9a3d4 form page (#20)

* Form page new ui

* UI polishing, code cleanign & bug fixes

* form page divide into separate component & user route-view for tabs

* new pages change meta info

* Display columns  modal

Co-authored-by: Julien Nahum <jhumanj@MacBook-Pro-de-Julien.local>
This commit is contained in:
Chirag 2022-11-09 15:53:17 +05:30 committed by GitHub
parent 0292dc0ec6
commit 0ef6e0cbdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 929 additions and 767 deletions

View File

@ -19,7 +19,7 @@
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"> class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Integrations Integrations
</router-link> </router-link>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"> <a href="#" class="hidden lg:inline text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Feature Requests Feature Requests
</a> </a>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" <a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"

View File

@ -0,0 +1,56 @@
<template>
<div class="flex flex-wrap sm:flex-nowrap my-4 w-full">
<div class="w-full sm:w-auto border border-gray-300 rounded-md p-2 flex-grow select-all bg-gray-100">
<p class="select-all text-gray-900">
{{ content }}
</p>
</div>
<div class="w-full sm:w-40 sm:ml-2 mt-2 sm:mt-0 shrink-0">
<v-button color="light-gray" class="w-full" @click="copyToClipboard(content)">
<slot name="icon">
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 20 20" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M4.16667 12.4998C3.3901 12.4998 3.00182 12.4998 2.69553 12.373C2.28715 12.2038 1.9627 11.8794 1.79354 11.471C1.66667 11.1647 1.66667 10.7764 1.66667 9.99984V4.33317C1.66667 3.39975 1.66667 2.93304 1.84833 2.57652C2.00812 2.26292 2.26308 2.00795 2.57669 1.84816C2.93321 1.6665 3.39992 1.6665 4.33334 1.6665H10C10.7766 1.6665 11.1649 1.6665 11.4711 1.79337C11.8795 1.96253 12.204 2.28698 12.3731 2.69536C12.5 3.00165 12.5 3.38993 12.5 4.1665M10.1667 18.3332H15.6667C16.6001 18.3332 17.0668 18.3332 17.4233 18.1515C17.7369 17.9917 17.9919 17.7368 18.1517 17.4232C18.3333 17.0666 18.3333 16.5999 18.3333 15.6665V10.1665C18.3333 9.23308 18.3333 8.76637 18.1517 8.40985C17.9919 8.09625 17.7369 7.84128 17.4233 7.68149C17.0668 7.49984 16.6001 7.49984 15.6667 7.49984H10.1667C9.23325 7.49984 8.76654 7.49984 8.41002 7.68149C8.09642 7.84128 7.84145 8.09625 7.68166 8.40985C7.50001 8.76637 7.50001 9.23308 7.50001 10.1665V15.6665C7.50001 16.5999 7.50001 17.0666 7.68166 17.4232C7.84145 17.7368 8.09642 17.9917 8.41002 18.1515C8.76654 18.3332 9.23325 18.3332 10.1667 18.3332Z"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</slot>
<slot></slot>
</v-button>
</div>
</div>
</template>
<script>
export default {
name: 'CopyContent',
props: {
content: {
type: String,
required: true
},
},
data() {
return {}
},
computed: {},
watch: {},
mounted() {
},
methods: {
copyToClipboard(str) {
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
}
}
</script>

View File

@ -1,70 +0,0 @@
<template>
<div
class="border border-nt-blue-light bg-blue-50 dark:bg-notion-dark-light rounded-md p-4 mb-5 w-full mx-auto mt-4 select-all"
>
<div class="flex items-center">
<p class="select-all text-nt-blue flex-grow">
{{ embedCode }}
</p>
<div class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer" @click="copyToClipboard">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-nt-blue" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
/>
</svg>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'EmbedFormCode',
props: {
form: {
type: Object,
required: true
}
},
data () {
return {}
},
computed: {
embedCode () {
return '<iframe style="border:none;width:100%;" height="' + this.formHeight + 'px" src="' + this.form.share_url + '"></iframe>'
},
formHeight () {
let height = 200
if (!this.form.hide_title) {
height += 60
}
height += this.form.properties.filter((property) => {
return !property.hidden
}).length * 70
return height
}
},
watch: {},
mounted () {
},
methods: {
copyToClipboard () {
const str = this.embedCode
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
}
}
</script>

View File

@ -1,39 +1,41 @@
<template> <template>
<div <div
class="my-4 w-full mx-auto"> class="my-4 w-full mx-auto">
<h3 class="font-semibold mb-4"> <h3 class="font-semibold mb-4 text-xl">
Form Submissions Form Submissions
<span v-if="form && !isLoading && tableData.length > 0" class="text-right text-xs uppercase mb-2"> - <a :href="exportUrl" target="_blank">Export as CSV</a></span> <span v-if="form && !isLoading && tableData.length > 0" class="text-right text-xs uppercase mb-2"> - <a
<span v-if="form && !isLoading && formInitDone" class="float-right text-xs uppercase mb-2"> <a href="javascript:void(0);" @click="showColumnsModal=true">Display columns</a></span> :href="exportUrl" target="_blank">Export as CSV</a></span>
<span v-if="form && !isLoading && formInitDone" class="float-right text-xs uppercase mb-2"> <a
href="javascript:void(0);" @click="showColumnsModal=true">Display columns</a></span>
</h3> </h3>
<!-- Table columns modal --> <!-- Table columns modal -->
<modal :show="showColumnsModal" @close="showColumnsModal=false"> <modal :show="showColumnsModal" @close="showColumnsModal=false">
<div class="-m-6"> <template #icon>
<div class="px-6 py-3"> <svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<h2 class="text-nt-blue text-3xl font-bold"> <path d="M16 5H8C4.13401 5 1 8.13401 1 12C1 15.866 4.13401 19 8 19H16C19.866 19 23 15.866 23 12C23 8.13401 19.866 5 16 5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
Display columns <path d="M8 15C9.65685 15 11 13.6569 11 12C11 10.3431 9.65685 9 8 9C6.34315 9 5 10.3431 5 12C5 13.6569 6.34315 15 8 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</h2> </svg>
</div> </template>
<div class="border-t py-4 px-6"> <template #title>
<template v-if="properties.length > 0"> Display columns
<h4 class="font-bold mb-2">Form Fields</h4> </template>
<div v-for="field in properties" :key="field.id" class="p-2 border">
{{ field.name }} <div class="px-4">
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right" /> <template v-if="properties.length > 0">
</div> <h4 class="font-bold mb-2">Form Fields</h4>
</template> <div v-for="field in properties" :key="field.id" class="p-2 border">
<template v-if="removed_properties.length > 0"> {{ field.name }}
<h4 class="font-bold mb-2 mt-4">Removed Fields</h4> <v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right"/>
<div v-for="field in removed_properties" :key="field.id" class="p-2 border"> </div>
{{ field.name }} </template>
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right" /> <template v-if="removed_properties.length > 0">
</div> <h4 class="font-bold mb-2 mt-4">Removed Fields</h4>
</template> <div v-for="field in removed_properties" :key="field.id" class="p-2 border">
</div> {{ field.name }}
<div class="flex justify-end mt-4 pb-5 px-6"> <v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right"/>
<v-button color="gray" shade="light" @click="showColumnsModal=false">Close</v-button> </div>
</div> </template>
</div> </div>
</modal> </modal>
@ -87,8 +89,8 @@ export default {
this.getSubmissionsData() this.getSubmissionsData()
}, },
watch: { watch: {
form () { form() {
if(!this.form){ if (this.form === null) {
return return
} }
this.initFormStructure() this.initFormStructure()
@ -117,28 +119,38 @@ export default {
return return
} }
// Add a "created at" column // check if form properties already has a created_at column
const columns = clonedeep(this.form.properties) let hasCreatedAt = false
columns.push({ this.form.properties.forEach((property) => {
"name": "Created at", if (property.id === 'created_at') {
"id": "created_at", hasCreatedAt = true
"type": "date", }
"width": 140,
}) })
this.$set(this.form, 'properties', columns)
if (!hasCreatedAt) {
// Add a "created at" column
const columns = clonedeep(this.form.properties)
columns.push({
"name": "Created at",
"id": "created_at",
"type": "date",
"width": 140,
})
this.$set(this.form, 'properties', columns)
}
this.formInitDone = true this.formInitDone = true
this.properties = clonedeep(this.form.properties) this.properties = clonedeep(this.form.properties)
this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : [] this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : []
// Get display columns from local storage // Get display columns from local storage
const tmpColumns = window.localStorage.getItem('display-columns-formid-'+this.form.id) const tmpColumns = window.localStorage.getItem('display-columns-formid-' + this.form.id)
if(tmpColumns !== null && tmpColumns){ if (tmpColumns !== null && tmpColumns) {
this.displayColumns = JSON.parse(tmpColumns) this.displayColumns = JSON.parse(tmpColumns)
this.onChangeDisplayColumns() this.onChangeDisplayColumns()
}else{ } else {
this.form.properties.forEach((field) => { this.form.properties.forEach((field) => {
this.displayColumns[field.id] = true this.displayColumns[field.id] = true
}) })
} }
}, },
@ -148,7 +160,7 @@ export default {
} }
this.isLoading = true this.isLoading = true
axios.get('/api/open/forms/' + this.form.id + '/submissions?page=' + this.currentPage).then((response) => { axios.get('/api/open/forms/' + this.form.id + '/submissions?page=' + this.currentPage).then((response) => {
const resData = response.data; const resData = response.data
this.tableData = this.tableData.concat(resData.data.map((record) => record.data)) this.tableData = this.tableData.concat(resData.data.map((record) => record.data))
@ -168,8 +180,8 @@ export default {
this.$refs.shadows.toggleShadow() this.$refs.shadows.toggleShadow()
this.$refs.shadows.calcDimensions() this.$refs.shadows.calcDimensions()
}, },
onChangeDisplayColumns(){ onChangeDisplayColumns() {
window.localStorage.setItem('display-columns-formid-'+this.form.id, JSON.stringify(this.displayColumns)) window.localStorage.setItem('display-columns-formid-' + this.form.id, JSON.stringify(this.displayColumns))
const final_properties = this.properties.concat(this.removed_properties).filter((field) => { const final_properties = this.properties.concat(this.removed_properties).filter((field) => {
return this.displayColumns[field.id] === true return this.displayColumns[field.id] === true
}) })

View File

@ -1,60 +0,0 @@
<template>
<div class="border border-nt-blue-light bg-blue-50 dark:bg-notion-dark-light rounded-md p-4 mb-5 w-full mx-auto mt-4 select-all">
<div class="flex items-center">
<p class="select-all text-nt-blue flex-grow truncate">
<a v-if="link" :href="form.share_url" target="_blank">
{{ form.share_url }}
</a>
<span v-else>
{{ form.share_url }}
</span>
</p>
<div class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer" @click="copyToClipboard(form.share_url)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-nt-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ShareFormUrl',
props: {
form: {
type: Object,
required: true
},
link: {
type: Boolean,
default: false
}
},
data () {
return {
}
},
computed: {
},
watch: {
},
mounted () {
},
methods: {
copyToClipboard (str) {
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
}
}
</script>

View File

@ -172,7 +172,9 @@ export default {
beforeDestroy() { beforeDestroy() {
const parent = document.getElementById('table-page') const parent = document.getElementById('table-page')
parent.removeEventListener('scroll', this.handleScroll) if (parent) {
parent.removeEventListener('scroll', this.handleScroll)
}
window.removeEventListener('resize', this.handleScroll) window.removeEventListener('resize', this.handleScroll)
}, },

View File

@ -1,29 +1,35 @@
<template> <template>
<modal :show="show" @close="$emit('close')"> <modal :show="show" @close="$emit('close')">
<form @submit.prevent="createTemplate" @keydown="templateForm.onKeydown($event)"> <template #icon>
<div class="-m-6"> <svg class="w-10 h-10 text-blue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<div class="p-6"> <path d="M17 27C16.0681 27 15.6022 27 15.2346 26.8478C14.7446 26.6448 14.3552 26.2554 14.1522 25.7654C14 25.3978 14 24.9319 14 24V17.2C14 16.0799 14 15.5198 14.218 15.092C14.4097 14.7157 14.7157 14.4097 15.092 14.218C15.5198 14 16.0799 14 17.2 14H24C24.9319 14 25.3978 14 25.7654 14.1522C26.2554 14.3552 26.6448 14.7446 26.8478 15.2346C27 15.6022 27 16.0681 27 17M24.2 34H30.8C31.9201 34 32.4802 34 32.908 33.782C33.2843 33.5903 33.5903 33.2843 33.782 32.908C34 32.4802 34 31.9201 34 30.8V24.2C34 23.0799 34 22.5198 33.782 22.092C33.5903 21.7157 33.2843 21.4097 32.908 21.218C32.4802 21 31.9201 21 30.8 21H24.2C23.0799 21 22.5198 21 22.092 21.218C21.7157 21.4097 21.4097 21.7157 21.218 22.092C21 22.5198 21 23.0799 21 24.2V30.8C21 31.9201 21 32.4802 21.218 32.908C21.4097 33.2843 21.7157 33.5903 22.092 33.782C22.5198 34 23.0799 34 24.2 34Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<h2 class="text-nt-blue text-3xl font-bold mb-6"> </svg>
Create template </template>
</h2> <template #title>
<p> Create template
New template will be create from your form <span class="font-semibold">{{form.title}}</span>. </template>
Template will be public for all to create form quickly. <div class="p-4">
</p> <p>
</div> New template will be create from your form <span class="font-semibold">{{form.title}}</span>.
<div class="border-t py-4 px-6"> Template will be public for all to create form quickly.
<text-input name="name" :form="templateForm" class="mt-4" label="Title" :required="true" /> </p>
<text-input name="slug" :form="templateForm" class="mt-4" label="Slug" :required="true" />
<rich-text-area-input name="description" :form="templateForm" class="mt-4" label="Description" :required="true" /> <form @submit.prevent="createTemplate" @keydown="templateForm.onKeydown($event)" class="mt-6">
<text-input name="image_url" :form="templateForm" class="mt-4" label="Image" :required="true" /> <div class="-m-6">
<questions-editor name="questions" :form="templateForm" class="mt-4" label="Frequently asked questions" /> <div class="border-t py-4 px-6">
</div> <text-input name="name" :form="templateForm" class="mt-4" label="Title" :required="true" />
<div class="flex justify-end mt-4 pb-5 px-6"> <text-input name="slug" :form="templateForm" class="mt-4" label="Slug" :required="true" />
<v-button class="mr-2" :loading="templateForm.busy">Create</v-button> <rich-text-area-input name="description" :form="templateForm" class="mt-4" label="Description" :required="true" />
<v-button color="gray" shade="light" @click.prevent="$emit('close')">Close</v-button> <text-input name="image_url" :form="templateForm" class="mt-4" label="Image" :required="true" />
</div> <questions-editor name="questions" :form="templateForm" class="mt-4" label="Frequently asked questions" />
</div> </div>
</form> <div class="flex justify-end mt-4 pb-5 px-6">
<v-button class="mr-2" :loading="templateForm.busy">Create</v-button>
<v-button color="white" @click.prevent="$emit('close')">Close</v-button>
</div>
</div>
</form>
</div>
</modal> </modal>
</template> </template>

View File

@ -1,78 +0,0 @@
<template>
<modal :show="show" @close="$emit('close')">
<div id="form-prefill-url-content" ref="content" class="px-4">
<h2 class="text-nt-blue text-3xl font-bold mb-4 flex items-center">
<span>Url Form Prefill</span>
<pro-tag class="ml-4 pb-3" />
</h2>
<p>
Create dynamic links when sharing your form (whether it's embedded or not), that allows you to prefill
your form fields. You can use this to personalize the form when sending it to multiple contacts for instance.
</p>
<h3 class="mt-6 border-t text-xl font-semibold mb-4 pt-6">
How does it work?
</h3>
<p>
Complete your form below and fill only the fields you want to prefill. You can even leave the required fields empty.
</p>
<div class="rounded-lg p-5 bg-gray-100 dark:bg-gray-900 mt-4">
<open-form v-if="form" :theme="theme" :loading="false" :show-hidden="true" :form="form" :fields="form.properties" @submit="generateUrl">
<template #submit-btn="{submitForm}">
<v-button class="mt-2 px-8 mx-1" @click.prevent="submitForm">
Generate Pre-filled URL
</v-button>
</template>
</open-form>
</div>
<template v-if="prefillFormData">
<h3 class="mt-6 text-xl font-semibold mb-4 pt-6">
Your Prefill url
</h3>
<form-url-prefill :form="form" :form-data="prefillFormData" />
</template>
<div class="flex justify-end mt-4">
<v-button color="gray" shade="light" @click="$emit('close')">Close</v-button>
</div>
</div>
</modal>
</template>
<script>
import FormUrlPrefill from '../../open/forms/components/FormUrlPrefill'
import ProTag from '../../common/ProTag'
import OpenForm from '../../open/forms/OpenForm'
import { themes } from '~/config/form-themes'
export default {
name: 'UrlFormPrefillModal',
components: { FormUrlPrefill, ProTag, OpenForm },
props: {
show: { type: Boolean, required: true },
form: { type: Object, required: true }
},
data: () => ({
prefillFormData: null,
theme: themes.default
}),
computed: {},
methods: {
generateUrl (formData, onFailure) {
this.prefillFormData = formData
this.$nextTick().then(() => {
this.$refs.content.parentElement.parentElement.parentElement.scrollTop = (this.$refs.content.offsetHeight - this.$refs.content.parentElement.parentElement.parentElement.offsetHeight + 50)
})
}
}
}
</script>

View File

@ -0,0 +1,53 @@
<template>
<div>
<h3 class="font-semibold text-xl">Embed</h3>
<p>Embed your form on your website by copying the HTML code below.</p>
<copy-content :content="embedCode" buttonText="Copy Code">
<template #icon>
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 18 18" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M11.0833 11.5L13.5833 9L11.0833 6.5M6.91667 6.5L4.41667 9L6.91667 11.5M5.5 16.5H12.5C13.9001 16.5 14.6002 16.5 15.135 16.2275C15.6054 15.9878 15.9878 15.6054 16.2275 15.135C16.5 14.6002 16.5 13.9001 16.5 12.5V5.5C16.5 4.09987 16.5 3.3998 16.2275 2.86502C15.9878 2.39462 15.6054 2.01217 15.135 1.77248C14.6002 1.5 13.9001 1.5 12.5 1.5H5.5C4.09987 1.5 3.3998 1.5 2.86502 1.77248C2.39462 2.01217 2.01217 2.39462 1.77248 2.86502C1.5 3.3998 1.5 4.09987 1.5 5.5V12.5C1.5 13.9001 1.5 14.6002 1.77248 15.135C2.01217 15.6054 2.39462 15.9878 2.86502 16.2275C3.3998 16.5 4.09987 16.5 5.5 16.5Z"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
Copy Code
</copy-content>
</div>
</template>
<script>
import CopyContent from '../../../open/forms/components/CopyContent'
export default {
name: 'EmbedCode',
components: { CopyContent },
props: {
form: { type: Object, required: true }
},
data: () => ({
}),
computed: {
embedCode() {
return '<iframe style="border:none;width:100%;" height="' + this.formHeight + 'px" src="' + this.form.share_url + '"></iframe>'
},
formHeight() {
let height = 200
if (!this.form.hide_title) {
height += 60
}
height += this.form.properties.filter((property) => {
return !property.hidden
}).length * 70
return height
}
},
methods: {}
}
</script>

View File

@ -0,0 +1,130 @@
<template>
<div>
<div v-if="loadingDuplicate || loadingDelete" class="pr-4 pt-2">
<loader class="h-6 w-6 mx-auto"/>
</div>
<dropdown v-else class="inline" dusk="nav-dropdown">
<template #trigger="{toggle}">
<v-button color="light-gray" class="mr-2" @click="toggle">
<svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.00016 2.83366C8.4604 2.83366 8.8335 2.46056 8.8335 2.00033C8.8335 1.54009 8.4604 1.16699 8.00016 1.16699C7.53993 1.16699 7.16683 1.54009 7.16683 2.00033C7.16683 2.46056 7.53993 2.83366 8.00016 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M13.8335 2.83366C14.2937 2.83366 14.6668 2.46056 14.6668 2.00033C14.6668 1.54009 14.2937 1.16699 13.8335 1.16699C13.3733 1.16699 13.0002 1.54009 13.0002 2.00033C13.0002 2.46056 13.3733 2.83366 13.8335 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M2.16683 2.83366C2.62707 2.83366 3.00016 2.46056 3.00016 2.00033C3.00016 1.54009 2.62707 1.16699 2.16683 1.16699C1.70659 1.16699 1.3335 1.54009 1.3335 2.00033C1.3335 2.46056 1.70659 2.83366 2.16683 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</v-button>
</template>
<router-link :to="{name:'forms.show_public', params: {slug: form.slug}}" target="_blank"
class="block sm:hidden 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"
v-track.view_form_click="{form_id:form.id, form_slug:form.slug}"
>
<svg class="w-4 h-4 mr-2" 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>
View form
</router-link>
<a href="#"
class="block 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"
v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
@click.prevent="duplicateForm"
>
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" 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>
Duplicate form
</a>
<a href="#"
class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center"
v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
@click.prevent="alertConfirm('Do you really want to delete this form?',deleteForm)"
>
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" 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>
Delete form
</a>
<a href="#" v-if="user.admin"
class="block 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="showCreateTemplateModal=true"
>
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"/>
</svg>
Create Template
</a>
</dropdown>
<create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/>
</div>
</template>
<script>
import axios from 'axios'
import {mapGetters, mapState} from 'vuex'
import Dropdown from '../../../common/Dropdown'
import CreateTemplateModal from '../CreateTemplateModal'
export default {
name: 'ExtraMenu',
components: { Dropdown, CreateTemplateModal },
props: {
form: { type: Object, required: true }
},
data: () => ({
loadingDuplicate: false,
loadingDelete: false,
showCreateTemplateModal: false
}),
computed: {
...mapGetters({
user: 'auth/user'
}),
formEndpoint: () => '/api/open/forms/{id}',
},
methods: {
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
})
},
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>

View File

@ -0,0 +1,108 @@
<template>
<div>
<v-button
class="w-full"
color="light-gray"
v-track.regenerate_form_link_click="{form_id:form.id, form_slug:form.slug}"
@click="showGenerateFormLinkModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 text-blue-600 inline" 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>
Regenerate form link
</v-button>
<!-- Regenerate form link modal -->
<modal :show="showGenerateFormLinkModal" @close="showGenerateFormLinkModal=false">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue-600" 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>
</template>
<template #title>
Generate new form link
</template>
<div class="p-4">
<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 class="border-t py-4 mt-4">
<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 border p-4 bg-gray-50 rounded-md mt-4">
https://opnform.com/forms/contact
</p>
<div class="text-center mt-4">
<v-button :loading="loadingNewLink" color="outline-blue" @click="regenerateLink('slug')">
Generate a Human Readable URL
</v-button>
</div>
</div>
<div class="border-t pt-4 mt-4">
<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 border bg-gray-50 rounded-md mt-4">
https://opnform.com/forms/b4417f9c-34ae-4421-8006-832ee47786e7
</p>
<div class="text-center mt-4">
<v-button :loading="loadingNewLink" color="outline-blue" @click="regenerateLink('uuid')">
Generate a Random ID URL
</v-button>
</div>
</div>
</div>
</modal>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'RegenerateFormLink',
components: {},
props: {
form: { type: Object, required: true }
},
data: () => ({
loadingNewLink: false,
showGenerateFormLinkModal: false,
}),
computed: {
formEndpoint: () => '/api/open/forms/{id}',
},
methods: {
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
})
},
}
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<div>
<h3 class="font-semibold text-xl">Share Link</h3>
<p>Your form is now published and ready to be shared with the world! Copy this link to share your form
on social media, messaging apps or via email.</p>
<copy-content :content="form.share_url">
<template #icon>
<svg class="h-4 w-4 -mt-1 text-blue-600 inline mr-1" viewBox="0 0 20 10" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M7.49984 9.16634H5.83317C3.53198 9.16634 1.6665 7.30086 1.6665 4.99967C1.6665 2.69849 3.53198 0.833008 5.83317 0.833008H7.49984M12.4998 9.16634H14.1665C16.4677 9.16634 18.3332 7.30086 18.3332 4.99967C18.3332 2.69849 16.4677 0.833008 14.1665 0.833008H12.4998M5.83317 4.99967L14.1665 4.99968"
stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
Copy Link
</copy-content>
</div>
</template>
<script>
import CopyContent from '../../../open/forms/components/CopyContent'
export default {
name: 'ShareLink',
components: { CopyContent },
props: {
form: { type: Object, required: true }
},
data: () => ({
}),
computed: {},
methods: {}
}
</script>

View File

@ -0,0 +1,98 @@
<template>
<div>
<v-button
class="w-full"
color="light-gray"
v-track.url_form_prefill_click="{form_id:form.id, form_slug:form.slug}"
@click="showUrlFormPrefillModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4 text-blue-600 inline" 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>
Url form pre-fill
<pro-tag class="ml-2"/>
</v-button>
<modal :show="showUrlFormPrefillModal" @close="showUrlFormPrefillModal=false">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue" 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>
</template>
<template #title>
<span>Url Form Prefill</span>
<pro-tag class="ml-4 pb-3" />
</template>
<div class="p-4">
<p>
Create dynamic links when sharing your form (whether it's embedded or not), that allows you to prefill
your form fields. You can use this to personalize the form when sending it to multiple contacts for instance.
</p>
<h3 class="mt-6 border-t text-xl font-semibold mb-4 pt-6">
How does it work?
</h3>
<p>
Complete your form below and fill only the fields you want to prefill. You can even leave the required fields empty.
</p>
<div class="rounded-lg p-5 bg-gray-100 dark:bg-gray-900 mt-4">
<open-form v-if="form" :theme="theme" :loading="false" :show-hidden="true" :form="form" :fields="form.properties" @submit="generateUrl">
<template #submit-btn="{submitForm}">
<v-button class="mt-2 px-8 mx-1" @click.prevent="submitForm">
Generate Pre-filled URL
</v-button>
</template>
</open-form>
</div>
<template v-if="prefillFormData">
<h3 class="mt-6 text-xl font-semibold mb-4 pt-6">
Your Prefill url
</h3>
<form-url-prefill :form="form" :form-data="prefillFormData" />
</template>
</div>
</modal>
</div>
</template>
<script>
import FormUrlPrefill from '../../../open/forms/components/FormUrlPrefill'
import ProTag from '../../../common/ProTag'
import OpenForm from '../../../open/forms/OpenForm'
import { themes } from '~/config/form-themes'
export default {
name: 'UrlFormPrefill',
components: { FormUrlPrefill, ProTag, OpenForm },
props: {
form: { type: Object, required: true }
},
data: () => ({
prefillFormData: null,
theme: themes.default,
showUrlFormPrefillModal: false,
}),
computed: {},
methods: {
generateUrl (formData, onFailure) {
this.prefillFormData = formData
this.$nextTick().then(() => {
this.$refs.content.parentElement.parentElement.parentElement.scrollTop = (this.$refs.content.offsetHeight - this.$refs.content.parentElement.parentElement.parentElement.offsetHeight + 50)
})
}
}
}
</script>

View File

@ -10,16 +10,17 @@
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" /> <loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
<div v-else class="my-3"> <div v-else class="my-3">
<div v-for="(questionForm, quesKey) in allQuestions" :key="quesKey" class="bg-gray-100 p-2 mb-4"> <div v-for="(questionForm, quesKey) in allQuestions" :key="quesKey" class="bg-gray-100 p-2 mb-4">
<v-button color="red" size="small" nativeType="button" class="text-right mb-2" @click.prevent="onRemove(quesKey)"> <v-button color="red" size="small" class="mb-2" @click.prevent="onRemove(quesKey)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<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" /> <path d="M3 6H5M5 6H21M5 6V20C5 20.5304 5.21071 21.0391 5.58579 21.4142C5.96086 21.7893 6.46957 22 7 22H17C17.5304 22 18.0391 21.7893 18.4142 21.4142C18.7893 21.0391 19 20.5304 19 20V6H5ZM8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M10 11V17M14 11V17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
Remove
</v-button> </v-button>
<text-input name="question" :form="questionForm" placeholder="Question title" /> <text-input name="question" :form="questionForm" placeholder="Question title" />
<rich-text-area-input name="answer" :form="questionForm" class="mt-4" placeholder="Question response" /> <rich-text-area-input name="answer" :form="questionForm" class="mt-4" placeholder="Question response" />
</div> </div>
<v-button v-if="addNew" color="green" size="small" nativeType="button" class="mt-2 flex" @click.prevent="onAdd"> <v-button v-if="addNew" color="green" size="small" nativeType="button" class="mt-2 flex" @click.prevent="onAdd">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-1" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
Add New Add New

View File

@ -1,481 +0,0 @@
<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 }}
<span v-if="form.visibility=='draft'" class="float-right text-white p-2 text-xs inline rounded-lg font-semibold mr-2 bg-gray-400 dark:bg-gray-700">Draft (not public)</span>
</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 class="w-full sm:w-1/2 px-2 flex mb-5" v-if="user.admin">
<div 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="showCreateTemplateModal=true"
>
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z" />
</svg>
<span class="font-semibold group relative-hover:text-blue-500">
Create template
</span>
</template>
</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>
<create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false" />
<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 CreateTemplateModal from '../../components/pages/forms/CreateTemplateModal'
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/loadIfEmpty', store.state['open/workspaces'].currentId)
})
}
export default {
name: 'EditForm',
components: { UrlFormPrefillModal, CreateTemplateModal, 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,
showCreateTemplateModal: 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>

View File

@ -0,0 +1,34 @@
<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'
export default {
components: {FormStats},
props: {
form: { type: Object, required: true }
},
metaInfo() {
return {title: (this.form) ? 'Form Analytics - '+this.form.title : 'Form Analytics'}
},
data: () => ({
}),
mounted() {},
computed: {},
methods: {
}
}
</script>

View File

@ -0,0 +1,225 @@
<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="#" @click.prevent="goBack" class="flex text-blue mb-2 font-semibold text-sm">
<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">
<h2 class="flex-grow text-gray-900 truncate">
{{ form.title }}
</h2>
<div class="flex">
<extra-menu :form="form" />
<v-button target="_blank" :to="{name:'forms.show_public', params: {slug: form.slug}}"
color="white" class="mr-2 text-blue-600 hidden sm:block"
v-track.view_form_click="{form_id:form.id, form_slug:form.slug}">
<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>
<ul class="flex text-gray-500">
<li class="pr-1">{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}</li>
<li class="list-disc ml-6 pr-1">{{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
</li>
<li class="list-disc ml-6 pr-1 text-blue-500" v-if="form.visibility=='draft'">Draft (not public)</li>
<li class="list-disc ml-6">Edited {{ form.last_edited_human }}</li>
</ul>
<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="mt-4 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li class="mr-6" v-for="(tab, i) in tabsList" :key="i+1">
<router-link :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 border-transparent 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">
<transition name="fade" mode="out-in">
<router-view :form="form" />
</transition>
</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 axios from 'axios'
import store from '~/store'
import Form from 'vform'
import {mapGetters, mapState} from 'vuex'
import ProTag from '../../../components/common/ProTag'
import VButton from "../../../components/common/Button";
import ExtraMenu from '../../../components/pages/forms/show/ExtraMenu'
const loadForms = function () {
store.commit('open/forms/startLoading')
store.dispatch('open/workspaces/loadIfEmpty').then(() => {
store.dispatch('open/forms/loadIfEmpty', store.state['open/workspaces'].currentId)
})
}
export default {
name: 'ShowForm',
components: {
VButton,
ProTag,
ExtraMenu
},
beforeRouteEnter(to, from, next) {
loadForms()
next()
},
beforeRouteLeave(to, from, next) {
this.workingForm = null
next()
},
middleware: 'auth',
data() {
return {
tabsList: [
{
name: 'Submissions',
route: 'forms.show'
},
{
name: 'Analytics',
route: 'forms.show.analytics'
},
{
name: 'Share',
route: 'forms.show.share'
}
]
}
},
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}',
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() {
if (this.form) {
this.workingForm = new Form(this.form)
}
},
metaInfo() {
return {title: this.$t('home')}
},
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>

View File

@ -0,0 +1,47 @@
<template>
<div>
<share-link class="mt-4" :form="form" />
<embed-code class="mt-6" :form="form" />
<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" :form="form" />
</div>
</div>
</template>
<script>
import ShareLink from '../../../components/pages/forms/show/ShareLink'
import EmbedCode from '../../../components/pages/forms/show/EmbedCode'
import UrlFormPrefill from '../../../components/pages/forms/show/UrlFormPrefill'
import RegenerateFormLink from '../../../components/pages/forms/show/RegenerateFormLink'
export default {
components: {
ShareLink,
EmbedCode,
UrlFormPrefill,
RegenerateFormLink
},
props: {
form: { type: Object, required: true }
},
metaInfo() {
return {title: (this.form) ? 'Form Share - '+this.form.title : 'Form Share'}
},
data: () => ({
}),
mounted() {},
computed: {},
methods: {
}
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<div id="table-page">
<form-submissions/>
</div>
</template>
<script>
import FormSubmissions from '../../../components/open/forms/components/FormSubmissions'
export default {
components: {FormSubmissions},
props: {
form: { type: Object, required: true }
},
metaInfo() {
return {title: (this.form) ? 'Form Submissions - '+this.form.title : 'Form Submissions'}
},
data: () => ({
}),
mounted() {},
computed: {},
methods: {
}
}
</script>

View File

@ -8,8 +8,17 @@ export default [
// Forms // Forms
{ path: '/forms/create', name: 'forms.create', component: page('forms/create.vue') }, { path: '/forms/create', name: 'forms.create', component: page('forms/create.vue') },
{ path: '/forms/:slug/show', name: 'forms.show', component: page('forms/show.vue') },
{ path: '/forms/:slug/edit', name: 'forms.edit', component: page('forms/edit.vue') }, { path: '/forms/:slug/edit', name: 'forms.edit', component: page('forms/edit.vue') },
{
path: '/forms/:slug/show',
component: page('forms/show/index.vue'),
children: [
{ path: '', redirect: { name: 'forms.show' } },
{ path: 'submissions', name: 'forms.show', component: page('forms/show/submissions.vue') },
{ path: 'analytics', name: 'forms.show.analytics', component: page('forms/show/analytics.vue') },
{ path: 'share', name: 'forms.show.share', component: page('forms/show/share.vue') }
]
},
// Subscription // Subscription
{ path: '/subscriptions/success', name: 'subscriptions.success', component: page('subscriptions/success.vue') }, { path: '/subscriptions/success', name: 'subscriptions.success', component: page('subscriptions/success.vue') },