Merge main and resolve conflicts

This commit is contained in:
JhumanJ 2022-11-16 11:19:30 +01:00
commit 415e6c6cc1
59 changed files with 1778 additions and 920 deletions

View File

@ -152,15 +152,25 @@ class AnswerFormRequest extends FormRequest
return ['email:filter'];
case 'date':
if (isset($property['date_range']) && $property['date_range']) {
$this->requestRules[$property['id'].'.*'] = ['date'];
return ['array'];
$this->requestRules[$property['id'].'.*'] = $this->getRulesForDate($property);
return ['array', 'min:2'];
}
return ['date'];
return $this->getRulesForDate($property);
default:
return [];
}
}
private function getRulesForDate($property)
{
if (isset($property['disable_past_dates']) && $property['disable_past_dates']) {
return ['date', 'after_or_equal:today'];
}else if (isset($property['disable_future_dates']) && $property['disable_future_dates']) {
return ['date', 'before_or_equal:today'];
}
return ['date'];
}
private function getSelectPropertyOptions($property): array
{
$type = $property['type'];

View File

@ -102,6 +102,8 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
'properties.*.use_am_pm' => 'boolean|nullable',
'properties.*.date_range' => 'boolean|nullable',
'properties.*.prefill_today' => 'boolean|nullable',
'properties.*.disable_past_dates' => 'boolean|nullable',
'properties.*.disable_future_dates' => 'boolean|nullable',
// Select / Multi Select field
'properties.*.allow_creation' => 'boolean|nullable',

View File

@ -42,6 +42,7 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
->markdown('mail.form.confirmation-submission-notification',[
'fields' => $formatter->getFieldsWithValue(),
'form' => $form,
'noBranding' => $form->no_branding
]);
}

View File

@ -490,7 +490,7 @@ class FormPropertyLogicRule implements Rule, DataAwareRule
if (is_array($conditions) && count($conditions) > 0) {
foreach($conditions as $val){
if (!in_array($val, static::ACTIONS_VALUES) ||
(in_array($this->field["type"], ['nf-text', 'nf-page-break', 'nf-divider', 'nf-image']) && !in_array($val, ['hide-block'])) ||
(in_array($this->field["type"], ['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image']) && !in_array($val, ['hide-block'])) ||
(isset($this->field["hidden"]) && $this->field["hidden"] && !in_array($val, ['show-block', 'require-answer'])) ||
(isset($this->field["required"]) && $this->field["required"] && !in_array($val, ['make-it-optional', 'hide-block']))
) {

View File

@ -1,6 +1,6 @@
<template>
<button v-if="!to" :type="nativeType" :disabled="loading" :class="btnClasses"
@click="$emit('click',$event)"
@click="onClick($event)"
>
<template v-if="!loading">
<span class="no-underline mx-auto">
@ -158,6 +158,12 @@ export default {
'p-x': 'px-4'
}
}
},
methods: {
onClick(event) {
this.$emit('click',event)
}
}
}
</script>

View File

@ -13,6 +13,7 @@
:date-format="useTime?'Z':'Y-m-d'"
:user-format="useTime ? amPm ? 'F j, Y - G:i K' : 'F j, Y - H:i' : 'F j, Y'"
:amPm="amPm"
:disabled-dates="disabledDates"
/>
<small v-if="help" :class="theme.default.help">
<slot name="help">{{ help }}</slot>
@ -32,7 +33,9 @@ export default {
props: {
withTime: { type: Boolean, default: false },
dateRange: { type: Boolean, default: false },
amPm: { type: Boolean, default: false }
amPm: { type: Boolean, default: false },
disablePastDates: { type: Boolean, default: false },
disableFutureDates: { type: Boolean, default: false }
},
data: () => ({
@ -74,6 +77,15 @@ export default {
const dateInput = this.$refs.datepicker.$el.getElementsByTagName('input')[0]
dateInput.style.setProperty('--tw-ring-color', this.color)
}
},
disabledDates (date) {
const today = new Date()
if(this.disablePastDates){
return new Date(date.getFullYear(), date.getMonth(), date.getDate()) < new Date(today.getFullYear(), today.getMonth(), today.getDate())
} else if(this.disableFutureDates){
return new Date(date.getFullYear(), date.getMonth(), date.getDate()) > new Date(today.getFullYear(), today.getMonth(), today.getDate())
}
return false;
}
}
}

View File

@ -12,6 +12,9 @@
<div v-if="field.type === 'nf-text' && field.content" :id="field.id" :key="field.id" class="nf-text w-full px-2 mb-3"
v-html="field.content"
/>
<div v-if="field.type === 'nf-code' && field.content" :id="field.id" :key="field.id" class="nf-code w-full px-2 mb-3"
v-html="field.content"
/>
<div v-if="field.type === 'nf-divider'" :id="field.id" :key="field.id" class="border-b my-4 w-full mx-2" />
<div v-if="field.type === 'nf-image' && (field.image_block || !isPublicFormPage)" :id="field.id" :key="field.id" class="my-4 w-full px-2">
<div v-if="!field.image_block" class="p-4 border border-dashed">
@ -375,6 +378,11 @@ export default {
if (field.use_am_pm) {
inputProperties.amPm = true
}
if (field.disable_past_dates) {
inputProperties.disablePastDates = true
}else if (field.disable_future_dates) {
inputProperties.disableFutureDates = true
}
} else if (field.type === 'files' || (field.type === 'url' && field.file_upload)) {
inputProperties.multiple = (field.multiple !== undefined && field.multiple)
inputProperties.mbLimit = 5

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>
<div
class="my-4 w-full mx-auto">
<h3 class="font-semibold mb-4">
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 && formInitDone" class="float-right text-xs uppercase mb-2"> <a href="javascript:void(0);" @click="showColumnsModal=true">Display columns</a></span>
<h3 class="font-semibold mb-4 text-xl">
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 && formInitDone" class="float-right text-xs uppercase mb-2"> <a
href="javascript:void(0);" @click="showColumnsModal=true">Display columns</a></span>
</h3>
<!-- Table columns modal -->
<modal :show="showColumnsModal" @close="showColumnsModal=false">
<div class="-m-6">
<div class="px-6 py-3">
<h2 class="text-nt-blue text-3xl font-bold">
Display columns
</h2>
</div>
<div class="border-t py-4 px-6">
<template v-if="properties.length > 0">
<h4 class="font-bold mb-2">Form Fields</h4>
<div v-for="field in properties" :key="field.id" class="p-2 border">
{{ field.name }}
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right" />
</div>
</template>
<template v-if="removed_properties.length > 0">
<h4 class="font-bold mb-2 mt-4">Removed Fields</h4>
<div v-for="field in removed_properties" :key="field.id" class="p-2 border">
{{ field.name }}
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right" />
</div>
</template>
</div>
<div class="flex justify-end mt-4 pb-5 px-6">
<v-button color="gray" shade="light" @click="showColumnsModal=false">Close</v-button>
</div>
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
<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"/>
</svg>
</template>
<template #title>
Display columns
</template>
<div class="px-4">
<template v-if="properties.length > 0">
<h4 class="font-bold mb-2">Form Fields</h4>
<div v-for="field in properties" :key="field.id" class="p-2 border">
{{ field.name }}
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right"/>
</div>
</template>
<template v-if="removed_properties.length > 0">
<h4 class="font-bold mb-2 mt-4">Removed Fields</h4>
<div v-for="field in removed_properties" :key="field.id" class="p-2 border">
{{ field.name }}
<v-switch v-model="displayColumns[field.id]" @input="onChangeDisplayColumns" class="float-right"/>
</div>
</template>
</div>
</modal>
@ -87,8 +89,8 @@ export default {
this.getSubmissionsData()
},
watch: {
form () {
if(!this.form){
form() {
if (this.form === null) {
return
}
this.initFormStructure()
@ -117,28 +119,38 @@ export default {
return
}
// Add a "created at" column
const columns = clonedeep(this.form.properties)
columns.push({
"name": "Created at",
"id": "created_at",
"type": "date",
"width": 140,
// check if form properties already has a created_at column
let hasCreatedAt = false
this.form.properties.forEach((property) => {
if (property.id === 'created_at') {
hasCreatedAt = true
}
})
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.properties = clonedeep(this.form.properties)
this.removed_properties = (this.form.removed_properties) ? clonedeep(this.form.removed_properties) : []
// Get display columns from local storage
const tmpColumns = window.localStorage.getItem('display-columns-formid-'+this.form.id)
if(tmpColumns !== null && tmpColumns){
const tmpColumns = window.localStorage.getItem('display-columns-formid-' + this.form.id)
if (tmpColumns !== null && tmpColumns) {
this.displayColumns = JSON.parse(tmpColumns)
this.onChangeDisplayColumns()
}else{
this.form.properties.forEach((field) => {
this.displayColumns[field.id] = true
} else {
this.form.properties.forEach((field) => {
this.displayColumns[field.id] = true
})
}
},
@ -148,7 +160,7 @@ export default {
}
this.isLoading = true
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))
@ -168,8 +180,8 @@ export default {
this.$refs.shadows.toggleShadow()
this.$refs.shadows.calcDimensions()
},
onChangeDisplayColumns(){
window.localStorage.setItem('display-columns-formid-'+this.form.id, JSON.stringify(this.displayColumns))
onChangeDisplayColumns() {
window.localStorage.setItem('display-columns-formid-' + this.form.id, JSON.stringify(this.displayColumns))
const final_properties = this.properties.concat(this.removed_properties).filter((field) => {
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

@ -196,6 +196,18 @@
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Image Block</p>
</div>
<!-- Code Block -->
<div
class="bg-gray-50 border hover:bg-gray-100 dark:bg-gray-900 rounded-md dark:hover:bg-gray-800 p-2 flex flex-col"
role="button" @click.prevent="addBlock('nf-code')"
>
<div class="mx-auto py-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
</div>
<p class="w-full text-xs text-gray-500 uppercase text-center font-semibold mb-4">Code Block</p>
</div>
</div>
</modal>
@ -242,6 +254,7 @@ export default {
'nf-page-break': 'Page Break',
'nf-divider': 'Divider',
'nf-image': 'Image',
'nf-code': 'Code Block',
}
}
},
@ -292,6 +305,8 @@ export default {
} else if (data.type === 'nf-page-break') {
data.next_btn_text = 'Next'
data.previous_btn_text = 'Previous'
} else if (data.type === 'nf-code') {
data.content = '<div class="text-blue-500 italic">This is a code block.</div>'
}
return data
},

View File

@ -107,7 +107,7 @@ export default {
})
},
actionOptions() {
if (['nf-text', 'nf-page-break', 'nf-divider', 'nf-image'].includes(this.field.type)) {
if (['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image'].includes(this.field.type)) {
return [{name: 'Hide Block', value: 'hide-block'}]
}

View File

@ -115,6 +115,10 @@
:form="field" label="Upload Image" :required="false"
/>
</div>
<div v-else-if="field.type == 'nf-code'" class="-mx-4 sm:-mx-6 p-5 border-b border-t">
<code-input name="content" class="mt-4 h-36" :form="field" label="Content"
help="You can add any html code, including iframes" />
</div>
<div v-else class="-mx-4 sm:-mx-6 p-5 border-b border-t">
<p>No settings found.</p>
</div>
@ -137,10 +141,11 @@
<script>
import ProTag from '../../../common/ProTag'
import FormBlockLogicEditor from '../components/form-logic-components/FormBlockLogicEditor'
import CodeInput from '../../../forms/CodeInput'
export default {
name: 'FormBlockOptionsModal',
components: {ProTag, FormBlockLogicEditor},
components: {ProTag, FormBlockLogicEditor, CodeInput},
props: {
field: {
type: Object,

View File

@ -167,6 +167,20 @@
<p class="text-gray-400 mb-5">
if enabled we will pre-fill this field with the current date
</p>
<v-checkbox v-model="field.disable_past_dates"
name="disable_past_dates" class="mb-3"
@input="onFieldDisablePastDatesChange"
>
Disable past dates
</v-checkbox>
<v-checkbox v-model="field.disable_future_dates"
name="disable_future_dates" class="mb-3"
@input="onFieldDisableFutureDatesChange"
>
Disable future dates
</v-checkbox>
</div>
<!-- select/multiselect Options -->
@ -484,6 +498,8 @@ export default {
if (this.field.prefill_today) {
this.$set(this.field, 'prefill', 'Pre-filled with current date')
this.$set(this.field, 'date_range', false)
this.$set(this.field, 'disable_future_dates', false)
this.$set(this.field, 'disable_past_dates', false)
} else {
this.$set(this.field, 'prefill', null)
}
@ -500,6 +516,20 @@ export default {
this.$set(this.field, 'allow_creation', false)
}
},
onFieldDisablePastDatesChange (val) {
this.$set(this.field, 'disable_past_dates', val)
if (this.field.disable_past_dates) {
this.$set(this.field, 'disable_future_dates', false)
this.$set(this.field, 'prefill_today', false)
}
},
onFieldDisableFutureDatesChange (val) {
this.$set(this.field, 'disable_future_dates', val)
if (this.field.disable_future_dates) {
this.$set(this.field, 'disable_past_dates', false)
this.$set(this.field, 'prefill_today', false)
}
}
}
}
</script>

View File

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

View File

@ -7,7 +7,7 @@
</p>
</div>
<div class="flex justify-center mt-5 md:mt-0">
<router-link :to="{ name: user ? 'home' : 'welcome' }" class="flex-shrink-0 font-bold flex items-center">
<router-link :to="{ name: user ? 'home' : 'welcome' }" class="flex-shrink-0 font-semibold flex items-center">
<img :src="asset('img/logo.svg')" alt="notion tools logo" class="w-10 h-10">
<span class="ml-2 text-xl text-black dark:text-white">
{{ appName }}

View File

@ -1,29 +1,35 @@
<template>
<modal :show="show" @close="$emit('close')">
<form @submit.prevent="createTemplate" @keydown="templateForm.onKeydown($event)">
<div class="-m-6">
<div class="p-6">
<h2 class="text-nt-blue text-3xl font-bold mb-6">
Create template
</h2>
<p>
New template will be create from your form <span class="font-semibold">{{form.title}}</span>.
Template will be public for all to create form quickly.
</p>
</div>
<div class="border-t py-4 px-6">
<text-input name="name" :form="templateForm" class="mt-4" label="Title" :required="true" />
<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" />
<text-input name="image_url" :form="templateForm" class="mt-4" label="Image" :required="true" />
<questions-editor name="questions" :form="templateForm" class="mt-4" label="Frequently asked questions" />
</div>
<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="gray" shade="light" @click.prevent="$emit('close')">Close</v-button>
</div>
</div>
</form>
<template #icon>
<svg class="w-10 h-10 text-blue" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
</template>
<template #title>
Create template
</template>
<div class="p-4">
<p>
New template will be create from your form <span class="font-semibold">{{form.title}}</span>.
Template will be public for all to create form quickly.
</p>
<form @submit.prevent="createTemplate" @keydown="templateForm.onKeydown($event)" class="mt-6">
<div class="-m-6">
<div class="border-t py-4 px-6">
<text-input name="name" :form="templateForm" class="mt-4" label="Title" :required="true" />
<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" />
<text-input name="image_url" :form="templateForm" class="mt-4" label="Image" :required="true" />
<questions-editor name="questions" :form="templateForm" class="mt-4" label="Frequently asked questions" />
</div>
<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>
</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" />
<div v-else class="my-3">
<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)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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" />
<v-button color="red" size="small" class="mb-2" @click.prevent="onRemove(quesKey)">
<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 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>
Remove
</v-button>
<text-input name="question" :form="questionForm" placeholder="Question title" />
<rich-text-area-input name="answer" :form="questionForm" class="mt-4" placeholder="Question response" />
</div>
<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" />
</svg>
Add New

View File

@ -0,0 +1,78 @@
<template>
<!-- Forgot password modal -->
<modal :show="show" @close="close" max-width="lg">
<template #icon>
<template v-if="isMailSent">
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect class="text-blue-50" width="56" height="56" rx="28" fill="currentColor"/>
<path d="M16.3333 22.1666L25.859 28.8346C26.6304 29.3746 27.016 29.6446 27.4356 29.7492C27.8061 29.8415 28.1937 29.8415 28.5643 29.7492C28.9838 29.6446 29.3695 29.3746 30.1408 28.8346L39.6666 22.1666M21.9333 37.3333H34.0666C36.0268 37.3333 37.0069 37.3333 37.7556 36.9518C38.4141 36.6163 38.9496 36.0808 39.2851 35.4223C39.6666 34.6736 39.6666 33.6935 39.6666 31.7333V24.2666C39.6666 22.3064 39.6666 21.3264 39.2851 20.5777C38.9496 19.9191 38.4141 19.3837 37.7556 19.0481C37.0069 18.6666 36.0268 18.6666 34.0666 18.6666H21.9333C19.9731 18.6666 18.993 18.6666 18.2443 19.0481C17.5857 19.3837 17.0503 19.9191 16.7147 20.5777C16.3333 21.3264 16.3333 22.3064 16.3333 24.2666V31.7333C16.3333 33.6935 16.3333 34.6736 16.7147 35.4223C17.0503 36.0808 17.5857 36.6163 18.2443 36.9518C18.993 37.3333 19.9731 37.3333 21.9333 37.3333Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
<template v-else>
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect class="text-blue-50" width="56" height="56" rx="28" fill="currentColor"/>
<path d="M33.8333 24.4999C33.8333 23.9028 33.6055 23.3057 33.1499 22.8501C32.6943 22.3945 32.0972 22.1667 31.5 22.1667M31.5 31.5C35.366 31.5 38.5 28.366 38.5 24.5C38.5 20.634 35.366 17.5 31.5 17.5C27.634 17.5 24.5 20.634 24.5 24.5C24.5 24.8193 24.5214 25.1336 24.5628 25.4415C24.6309 25.948 24.6649 26.2013 24.642 26.3615C24.6181 26.5284 24.5877 26.6184 24.5055 26.7655C24.4265 26.9068 24.2873 27.046 24.009 27.3243L18.0467 33.2866C17.845 33.4884 17.7441 33.5893 17.6719 33.707C17.608 33.8114 17.5608 33.9252 17.5322 34.0442C17.5 34.1785 17.5 34.3212 17.5 34.6065V36.6333C17.5 37.2867 17.5 37.6134 17.6272 37.863C17.739 38.0825 17.9175 38.261 18.137 38.3728C18.3866 38.5 18.7133 38.5 19.3667 38.5H22.1667V36.1667H24.5V33.8333H26.8333L28.6757 31.991C28.954 31.7127 29.0932 31.5735 29.2345 31.4945C29.3816 31.4123 29.4716 31.3819 29.6385 31.358C29.7987 31.3351 30.052 31.3691 30.5585 31.4372C30.8664 31.4786 31.1807 31.5 31.5 31.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
</template>
<template #title>
<template v-if="isMailSent">Check your email</template>
<template v-else>Forgot password?</template>
</template>
<template v-if="isMailSent">
<div class="text-center">We sent a password reset link to <br/><span>{{form.email}}</span></div>
<div class="w-full p-4 text-center">
<span class="mt-4">Didn't receive the email? <a href="#" class="ml-1" @click.prevent="send">Click to resend</a></span>
</div>
</template>
<template v-else>
<div class="text-center">No worries, we'll send you reset instructions.</div>
<form @submit.prevent="send" @keydown="form.onKeydown($event)" class="p-4">
<text-input name="email" :form="form" label="Email" placeholder="Your email address" :required="true" />
<div class="w-full mt-6">
<v-button :loading="form.busy" class="w-full my-3">Reset password</v-button>
</div>
</form>
</template>
<div class="w-full text-center">
<a href="#" @click.prevent="close" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700">
<svg class="inline mr-1" width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3334 6.99996H1.66669M1.66669 6.99996L7.50002 12.8333M1.66669 6.99996L7.50002 1.16663" stroke="#475467" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to log in
</a>
</div>
</modal>
</template>
<script>
import Form from 'vform'
export default {
name: 'ForgotPasswordModal',
components: { },
props: {
show: {
type: Boolean,
required: true
}
},
data: () => ({
isMailSent: false,
form: new Form({
email: ''
})
}),
methods: {
async send () {
const { data } = await this.form.post('/api/password/email')
this.isMailSent = true
},
close () {
this.$emit('close')
this.isMailSent = false
}
}
}
</script>

View File

@ -1,5 +1,7 @@
<template>
<div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<div class="flex mt-6 mb-10">
<div class="w-full md:max-w-6xl mx-auto px-4 flex md:flex-row-reverse flex-wrap">
<div class="w-full md:w-1/2 md:p-6">
@ -25,9 +27,9 @@
</v-checkbox>
<div class="w-full md:w-1/2 text-right">
<router-link :to="{ name: 'password.request' }" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700">
<a href="#" @click.prevent="showForgotModal=true" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700">
Forgot your password?
</router-link>
</a>
</div>
</div>
@ -88,11 +90,13 @@ import Form from 'vform'
import Cookies from 'js-cookie'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import Testimonials from '../../components/pages/welcome/Testimonials'
import ForgotPasswordModal from './ForgotPasswordModal'
export default {
components: {
OpenFormFooter,
Testimonials
Testimonials,
ForgotPasswordModal
},
middleware: 'guest',
@ -106,7 +110,8 @@ export default {
email: '',
password: ''
}),
remember: false
remember: false,
showForgotModal: false
}),
methods: {

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

@ -1,18 +1,18 @@
<template>
<card title="Account" class="bg-gray-50 dark:bg-notion-dark-light">
<h3 class="text-lg font-semibold mb-4">
Your Account
</h3>
<p class="text-gray-800 dark:text-gray-200">
You can delete your account. All your data will be removed. <span class="font-semibold">This cannot be undone.</span>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Danger zone</h3>
<p class="text-gray-600 text-sm mt-2">
This will permanently delete your entire account. All your forms, submissions and workspaces will be deleted.
<span class="text-red-500">
This cannot be undone.
</span>
</p>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4" color="red" @click="alertConfirm('Do you really want to delete your account?',deleteAccount)">
Delete my account
Delete account
</v-button>
</card>
</div>
</template>
<script>

View File

@ -1,9 +1,13 @@
<template>
<card title="Admin" class="bg-gray-50 dark:bg-notion-dark-light">
<h3 class="text-lg font-semibold mb-4">
<div>
<h3 class="font-semibold text-2xl text-gray-900">Admin settings</h3>
<small class="text-gray-600">Manage settings.</small>
<h3 class="mt-3 text-lg font-semibold mb-4">
Tools
</h3>
<div class="flex flex-wrap mb-10">
<div class="flex flex-wrap mb-5">
<a href="/stats">
<v-button class="mx-1" color="gray" shade="lighter">
Stats
@ -25,11 +29,9 @@
/>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4 w-full">
Impersonate User
</v-button>
<v-button :loading="loading" class="mt-4">Impersonate User</v-button>
</form>
</card>
</div>
</template>
<script>

View File

@ -1,12 +1,17 @@
<template>
<card title="Billing" class="bg-gray-50 dark:bg-notion-dark-light">
<v-button color="gray" shade="light" :loading="billingLoading" @click.prevent="openBillingDashboard">
Manage Subscription
</v-button>
<v-button color="red" class="mt-3" @click.prevent="cancelSubscription">
Cancel Subscription
</v-button>
</card>
<div>
<h3 class="font-semibold text-2xl text-gray-900">Billing details</h3>
<small class="text-gray-600">Manage your billing.</small>
<div class="mt-4">
<v-button color="gray" shade="light" :loading="billingLoading" @click.prevent="openBillingDashboard">
Manage Subscription
</v-button>
<v-button color="red" class="mt-3" @click.prevent="cancelSubscription">
Cancel Subscription
</v-button>
</div>
</div>
</template>
<script>

View File

@ -1,80 +1,38 @@
<template>
<div class="flex flex-wrap mt-6 md:max-w-3xl w-full md:mx-auto">
<div class="w-full md:w-1/3 md:pr-4">
<card :padding="false" class="bg-gray-50 dark:bg-notion-dark-light">
<ul>
<li v-for="tab in tabs" :key="tab.route">
<router-link :to="{ name: tab.route }"
class="px-6 py-4 flex items-center text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 hover:text-gray-900 hover:bg-gray-50 dark:hover:bg-gray-900 rounded"
active-class="text-nt-blue bg-indigo-50 dark:bg-gray-800 hover:bg-blue-50"
>
<template v-if="tab.route == 'settings.profile'">
<svg class="w-6 h-6 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="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</template>
<template v-else-if="tab.route == 'settings.account'">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7a4 4 0 11-8 0 4 4 0 018 0zM9 14a6 6 0 00-6 6v1h12v-1a6 6 0 00-6-6zM21 12h-6" />
</svg>
</template>
<template v-else-if="tab.route == 'settings.workspaces'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</template>
<template v-else-if="tab.route == 'settings.billing'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
/>
</svg>
</template>
<template v-else-if="tab.route == 'settings.password'">
<svg class="w-6 h-6 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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</template>
<span class="ml-2">
{{ tab.name }}
</span>
</router-link>
</li>
<li v-if="user.admin">
<router-link :to="{ name: 'settings.admin' }"
class="px-6 py-4 flex items-center text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 hover:text-gray-900 hover:bg-gray-50 dark:hover:bg-gray-900 rounded"
active-class="text-nt-blue bg-indigo-50 dark:bg-gray-800 hover:bg-blue-50"
>
<svg class="w-6 h-6 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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<span class="ml-2">
Admin
</span>
</router-link>
</li>
</ul>
</card>
</div>
<div class="bg-white">
<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">
<div class="flex">
<h2 class="flex-grow text-gray-900">
My Account
</h2>
</div>
<ul class="flex text-gray-500">
<li>{{ user.email }}</li>
</ul>
<div class="w-full md:w-2/3">
<transition name="fade" mode="out-in">
<router-view />
</transition>
<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="mt-8 pb-0">
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</div>
</div>
</div>
</template>
@ -85,19 +43,27 @@ import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
data () {
return {
}
},
computed: {
tabs () {
...mapGetters({
user: 'auth/user'
}),
tabsList () {
const tabs = [
{
name: 'Workspaces',
route: 'settings.workspaces'
},
{
name: this.$t('profile'),
name: 'Profile',
route: 'settings.profile'
},
{
name: this.$t('password'),
name: 'Workspace Settings',
route: 'settings.workspaces'
},
{
name: 'Password',
route: 'settings.password'
},
{
@ -113,11 +79,18 @@ export default {
})
}
if(this.user.admin){
tabs.push({
name: 'Admin',
route: 'settings.admin'
})
}
return tabs
},
...mapGetters({
user: 'auth/user'
})
}
},
methods: {
}
}
</script>

View File

@ -1,24 +1,25 @@
<template>
<card :title="$t('your_password')" class="bg-gray-50 dark:bg-notion-dark-light">
<form @submit.prevent="update" @keydown="form.onKeydown($event)">
<div>
<h3 class="font-semibold text-2xl text-gray-900">Password</h3>
<small class="text-gray-600">Manage your password.</small>
<form @submit.prevent="update" @keydown="form.onKeydown($event)" class="mt-3">
<alert-success class="mb-5" :form="form" :message="$t('password_updated')" />
<!-- Password -->
<text-input class="mt-8" native-type="password"
<text-input native-type="password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Password Confirmation-->
<text-input class="mt-8" native-type="password"
<text-input native-type="password"
name="password_confirmation" :form="form" :label="$t('confirm_password')" :required="true"
/>
<!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4 w-full">
{{ $t('update') }}
</v-button>
<v-button :loading="form.busy" class="mt-4">Update password</v-button>
</form>
</card>
</div>
</template>
<script>

View File

@ -1,6 +1,9 @@
<template>
<card title="Profile" class="bg-gray-50 dark:bg-notion-dark-light">
<form @submit.prevent="update" @keydown="form.onKeydown($event)">
<div>
<h3 class="font-semibold text-2xl text-gray-900">Profile details</h3>
<small class="text-gray-600">Update your username and manage your account details.</small>
<form @submit.prevent="update" @keydown="form.onKeydown($event)" class="mt-3">
<alert-success class="mb-5" :form="form" :message="$t('info_updated')" />
<!-- Name -->
@ -10,11 +13,9 @@
<text-input name="email" :form="form" :label="$t('email')" :required="true" />
<!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4 w-full">
{{ $t('update') }}
</v-button>
<v-button :loading="form.busy" class="mt-4">Save changes</v-button>
</form>
</card>
</div>
</template>
<script>

View File

@ -1,37 +1,41 @@
<template>
<card title="Workspaces" class="bg-gray-50 dark:bg-notion-dark-light">
<div>
<h3 class="font-semibold text-2xl text-gray-900">Workspace settings</h3>
<small class="text-gray-600">Manage your workspaces.</small>
<div v-if="loading" class="w-full text-blue-500 text-center">
<loader class="h-10 w-10 p-5"/>
</div>
<div v-else>
<div v-for="workspace in workspaces" :key="workspace.id"
class="border border-nt-blue-light shadow rounded-md p-4 mb-5 max-w-sm w-full flex group mx-auto bg-white dark:bg-notion-dark items-center"
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center"
>
<div class="flex space-x-4 flex-grow cursor-pointer" role="button" @click.prevent="switchWorkspace(workspace)">
<div class="flex space-x-4 flex-grow items-center cursor-pointer" role="button" @click.prevent="switchWorkspace(workspace)">
<img v-if="isUrl(workspace.icon)" :src="workspace.icon" :alt="workspace.name + ' icon'"
class="rounded-full h-12 w-12"
class="rounded-full h-12 w-12"
>
<div v-else class="rounded-full bg-nt-blue-lighter h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
v-text="workspace.icon"
<div v-else class="rounded-2xl bg-gray-100 h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
v-text="workspace.icon"
/>
<div class="flex-1 flex items-center space-y-4 py-1">
<p class="font-bold truncate" v-text="workspace.name"/>
<div class="space-y-4 py-1">
<div class="font-bold truncate">{{workspace.name}}</div>
</div>
</div>
<div v-if="workspaces.length > 1"
class="block md:hidden group-hover:block text-red-500 p-2 rounded hover:bg-red-50" role="button"
@click="deleteWorkspace(workspace)">
class="block md:hidden group-hover:block text-red-500 p-2 rounded hover:bg-red-50" role="button"
@click="deleteWorkspace(workspace)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
</div>
</div>
<div class="max-w-sm w-full mx-auto mt-4">
<v-button :loading="loading" class="w-full" @click="workspaceModal=true">
Create a new workspace
</v-button>
</div>
<v-button :loading="loading" class="mt-4" @click="workspaceModal=true">
<svg class="inline text-white mr-1 h-4 w-4" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Create new workspace
</v-button>
</div>
<!-- Workspace modal -->
@ -65,7 +69,7 @@
</div>
</modal>
</card>
</div>
</template>
<script>

View File

@ -8,8 +8,17 @@ export default [
// Forms
{ 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/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
{ path: '/subscriptions/success', name: 'subscriptions.success', component: page('subscriptions/success.vue') },

View File

@ -1,4 +1,4 @@
@component('mail::message')
@component('mail::message', ['noBranding' => $noBranding])
{!! $form->notification_body !!}

View File

@ -0,0 +1,19 @@
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color ?? 'primary' }}" target="_blank" rel="noopener">{{ $slot }}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -0,0 +1,11 @@
<tr>
<td>
<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>

View File

@ -0,0 +1,11 @@
<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
@if (trim($slot) === 'Laravel')
<img src="https://laravel.com/img/notification-logo.png" class="logo" alt="Laravel Logo">
@else
{{ $slot }}
@endif
</a>
</td>
</tr>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<style>
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
</head>
<body>
<table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
{{ $header ?? '' }}
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
{{ Illuminate\Mail\Markdown::parse($slot) }}
{{ $subcopy ?? '' }}
</td>
</tr>
</table>
</td>
</tr>
{{ $footer ?? '' }}
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,33 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@if (!(isset($noBranding) && $noBranding))
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@else
<div style="margin-top:25px;" />
@endif
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
@if (!(isset($noBranding) && $noBranding))
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endif
@endcomponent
@endslot
@endcomponent

View File

@ -0,0 +1,14 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-item">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -0,0 +1,7 @@
<table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

View File

@ -0,0 +1,3 @@
<div class="table">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</div>

View File

@ -0,0 +1,290 @@
/* Base */
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
body {
-webkit-text-size-adjust: none;
background-color: #ffffff;
color: #718096;
height: 100%;
line-height: 1.4;
margin: 0;
padding: 0;
width: 100% !important;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: #3869d4;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #3d4852;
font-size: 18px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: #edf2f7;
margin: 0;
padding: 0;
width: 100%;
}
.content {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 0;
padding: 0;
width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #3d4852;
font-size: 19px;
font-weight: bold;
text-decoration: none;
}
/* Logo */
.logo {
height: 75px;
max-height: 75px;
width: 75px;
}
/* Body */
.body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: #edf2f7;
border-bottom: 1px solid #edf2f7;
border-top: 1px solid #edf2f7;
margin: 0;
padding: 0;
width: 100%;
}
.inner-body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
background-color: #ffffff;
border-color: #e8e5ef;
border-radius: 2px;
border-width: 1px;
box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015);
margin: 0 auto;
padding: 0;
width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid #e8e5ef;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 14px;
}
/* Footer */
.footer {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
}
.footer p {
color: #b0adc5;
font-size: 12px;
text-align: center;
}
.footer a {
color: #b0adc5;
text-decoration: underline;
}
/* Tables */
.table table {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 30px auto;
width: 100%;
}
.table th {
border-bottom: 1px solid #edeff2;
margin: 0;
padding-bottom: 8px;
}
.table td {
color: #74787e;
font-size: 15px;
line-height: 18px;
margin: 0;
padding: 10px 0;
}
.content-cell {
max-width: 100vw;
padding: 32px;
}
/* Buttons */
.action {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
}
.button {
-webkit-text-size-adjust: none;
border-radius: 4px;
color: #fff;
display: inline-block;
overflow: hidden;
text-decoration: none;
}
.button-blue,
.button-primary {
background-color: #2d3748;
border-bottom: 8px solid #2d3748;
border-left: 18px solid #2d3748;
border-right: 18px solid #2d3748;
border-top: 8px solid #2d3748;
}
.button-green,
.button-success {
background-color: #48bb78;
border-bottom: 8px solid #48bb78;
border-left: 18px solid #48bb78;
border-right: 18px solid #48bb78;
border-top: 8px solid #48bb78;
}
.button-red,
.button-error {
background-color: #e53e3e;
border-bottom: 8px solid #e53e3e;
border-left: 18px solid #e53e3e;
border-right: 18px solid #e53e3e;
border-top: 8px solid #e53e3e;
}
/* Panels */
.panel {
border-left: #2d3748 solid 4px;
margin: 21px 0;
}
.panel-content {
background-color: #edf2f7;
color: #718096;
padding: 16px;
}
.panel-content p {
color: #718096;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Utilities */
.break-all {
word-break: break-all;
}

View File

@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
[{{ $slot }}]({{ $url }})

View File

@ -0,0 +1,9 @@
{!! strip_tags($header) !!}
{!! strip_tags($slot) !!}
@isset($subcopy)
{!! strip_tags($subcopy) !!}
@endisset
{!! strip_tags($footer) !!}

View File

@ -0,0 +1,31 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@if (!(isset($noBranding) && $noBranding))
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@endif
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
@if (!(isset($noBranding) && $noBranding))
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endif
@endcomponent
@endslot
@endcomponent

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -83,4 +83,52 @@ it('can not submit draft form', function () {
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertStatus(403);
});
it('can not submit form with past dates', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$submissionData = [];
$form->properties = collect($form->properties)->map(function ($property) use (&$submissionData) {
if(in_array($property['type'], ['date'])){
$property["disable_past_dates"] = true;
$submissionData[$property['id']] = now()->subDays(4)->format('Y-m-d');
}
return $property;
})->toArray();
$form->update();
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertStatus(422)
->assertJson([
'message' => 'The Date must be a date after or equal to today.'
]);
});
it('can not submit form with future dates', function () {
$user = $this->actingAsUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace);
$submissionData = [];
$form->properties = collect($form->properties)->map(function ($property) use (&$submissionData) {
if(in_array($property['type'], ['date'])){
$property["disable_future_dates"] = true;
$submissionData[$property['id']] = now()->addDays(4)->format('Y-m-d');
}
return $property;
})->toArray();
$form->update();
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
$this->postJson(route('forms.answer', $form->slug), $formData)
->assertStatus(422)
->assertJson([
'message' => 'The Date must be a date before or equal to today.'
]);
});