This commit is contained in:
Julien Nahum 2022-11-25 10:01:45 +01:00
commit acb28548b8
37 changed files with 2195 additions and 1483 deletions

View File

@ -17,7 +17,7 @@ class TemplateController extends Controller
public function create(CreateTemplateRequest $request) public function create(CreateTemplateRequest $request)
{ {
$this->middleware('admin'); $this->authorize('create', Template::class);
// Create template // Create template
$template = $request->getTemplate(); $template = $request->getTemplate();
@ -28,5 +28,4 @@ class TemplateController extends Controller
'template_id' => $template->id 'template_id' => $template->id
]); ]);
} }
} }

View File

@ -58,7 +58,8 @@ class FormResource extends JsonResource
'is_closed' => $this->is_closed, 'is_closed' => $this->is_closed,
'is_password_protected' => false, 'is_password_protected' => false,
'has_password' => $this->has_password, 'has_password' => $this->has_password,
'max_number_of_submissions_reached' => $this->max_number_of_submissions_reached 'max_number_of_submissions_reached' => $this->max_number_of_submissions_reached,
'form_pending_submission_key' => $this->form_pending_submission_key
]); ]);
} }

View File

@ -18,6 +18,7 @@ class UserResource extends JsonResource
'is_subscribed' => $this->is_subscribed, 'is_subscribed' => $this->is_subscribed,
'has_enterprise_subscription' => $this->has_enterprise_subscription, 'has_enterprise_subscription' => $this->has_enterprise_subscription,
'admin' => $this->admin, 'admin' => $this->admin,
'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id, 'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms, 'has_forms' => $this->has_forms,
] : []; ] : [];

View File

@ -166,6 +166,14 @@ class Form extends Model
return ($this->closes_at && now()->gt($this->closes_at)); return ($this->closes_at && now()->gt($this->closes_at));
} }
public function getFormPendingSubmissionKeyAttribute()
{
if ($this->updated_at?->timestamp) {
return "openform-" . $this->id . "-pending-submission-" . substr($this->updated_at?->timestamp, -6);
}
return null;
}
public function getMaxNumberOfSubmissionsReachedAttribute() public function getMaxNumberOfSubmissionsReachedAttribute()
{ {
return ($this->max_submissions_count && $this->max_submissions_count <= $this->submissions_count); return ($this->max_submissions_count && $this->max_submissions_count <= $this->submissions_count);

View File

@ -99,6 +99,11 @@ class User extends Authenticatable implements JWTSubject //, MustVerifyEmail
return in_array($this->email, config('services.admin_emails')); return in_array($this->email, config('services.admin_emails'));
} }
public function getTemplateEditorAttribute()
{
return $this->admin || in_array($this->email, config('services.template_editor_emails'));
}
/** /**
* ================================= * =================================
* Helper Related * Helper Related

View File

@ -0,0 +1,23 @@
<?php
namespace App\Policies;
use App\Models\Template;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class TemplatePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
return $user->template_editor;
}
}

View File

@ -4,10 +4,12 @@ namespace App\Providers;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Models\Integration\FormZapierWebhook; use App\Models\Integration\FormZapierWebhook;
use App\Models\Template;
use App\Models\Workspace; use App\Models\Workspace;
use App\Models\User; use App\Models\User;
use App\Policies\FormPolicy; use App\Policies\FormPolicy;
use App\Policies\Integration\FormZapierWebhookPolicy; use App\Policies\Integration\FormZapierWebhookPolicy;
use App\Policies\TemplatePolicy;
use App\Policies\WorkspacePolicy; use App\Policies\WorkspacePolicy;
use App\Policies\UserPolicy; use App\Policies\UserPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -22,7 +24,8 @@ class AuthServiceProvider extends ServiceProvider
protected $policies = [ protected $policies = [
Form::class => FormPolicy::class, Form::class => FormPolicy::class,
Workspace::class => WorkspacePolicy::class, Workspace::class => WorkspacePolicy::class,
FormZapierWebhook::class => FormZapierWebhookPolicy::class FormZapierWebhook::class => FormZapierWebhookPolicy::class,
Template::class => TemplatePolicy::class,
]; ];
/** /**

View File

@ -29,7 +29,8 @@ class StorageFileNameParser
public function getMovedFileName(): ?string public function getMovedFileName(): ?string
{ {
if ($this->fileName && $this->extension) { if ($this->fileName && $this->extension) {
return substr($this->fileName,0,50).'_'.$this->uuid.'.'.$this->extension; $fileName = substr($this->fileName, 0, 50).'_'.$this->uuid.'.'.$this->extension;
return mb_convert_encoding($fileName, 'UTF-8', 'UTF-8');
} }
return $this->uuid; return $this->uuid;
} }

View File

@ -52,5 +52,6 @@ return [
'amplitude_code' => env('AMPLITUDE_CODE'), 'amplitude_code' => env('AMPLITUDE_CODE'),
'crisp_website_id' => env('CRISP_WEBSITE_ID'), 'crisp_website_id' => env('CRISP_WEBSITE_ID'),
'admin_emails' => explode(",", env('ADMIN_EMAILS') ?? '') 'admin_emails' => explode(",", env('ADMIN_EMAILS') ?? ''),
'template_editor_emails' => explode(",", env('TEMPLATE_EDITOR_EMAILS') ?? '')
]; ];

2347
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,6 @@
"vue-prism-editor": "^1.2.2", "vue-prism-editor": "^1.2.2",
"vue-router": "^3.5.2", "vue-router": "^3.5.2",
"vue-tailwind": "^2.5.0", "vue-tailwind": "^2.5.0",
"vue-tour": "^2.0.0",
"vue2-editor": "^2.10.3", "vue2-editor": "^2.10.3",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.2", "vuex": "^3.6.2",

View File

@ -32,7 +32,7 @@
<server name="MAIL_FROM_NAME" value="NotionForms"/> <server name="MAIL_FROM_NAME" value="NotionForms"/>
<server name="QUEUE_CONNECTION" value="sync"/> <server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/> <server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/> <server name="TEMPLATE_EDITOR_EMAILS" value="admin@opnform.com"/>
<server name="JWT_SECRET" value="9K6whOetAFaokQgSIdbMQZuJuDV5uS2Y"/> <server name="JWT_SECRET" value="9K6whOetAFaokQgSIdbMQZuJuDV5uS2Y"/>
</php> </php>
</phpunit> </phpunit>

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

6
resources/js/app.js vendored
View File

@ -6,8 +6,6 @@ import App from '~/components/App'
import LoadScript from 'vue-plugin-load-script' import LoadScript from 'vue-plugin-load-script'
import Base from './base' import Base from './base'
import VueTour from 'vue-tour'
import '~/plugins' import '~/plugins'
import '~/components' import '~/components'
@ -16,10 +14,6 @@ Vue.config.productionTip = false
Vue.mixin(Base) Vue.mixin(Base)
Vue.use(LoadScript) Vue.use(LoadScript)
/* Vue Tour */
require('vue-tour/dist/vue-tour.css')
Vue.use(VueTour)
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
i18n, i18n,

View File

@ -15,15 +15,17 @@
<workspace-dropdown class="ml-6"/> <workspace-dropdown class="ml-6"/>
</div> </div>
<div class="hidden md:block ml-auto relative"> <div class="hidden md:block ml-auto relative">
<router-link :to="{name:'integrations'}" <router-link :to="{name:'templates'}"
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"> class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Integrations Templates
</router-link> </router-link>
<a href="#" class="hidden lg:inline text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Feature Requests
</a>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" <a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"
@click.prevent="$getCrisp().push(['do', 'helpdesk:search'])" @click.prevent="$getCrisp().push(['do', 'helpdesk:search'])" v-if="hasCrisp"
>
Help
</a>
<a href="https://help.opnform.com/en/" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"
target="_blank" v-else
> >
Help Help
</a> </a>
@ -97,7 +99,7 @@
{{ $t('login') }} {{ $t('login') }}
</router-link> </router-link>
<v-button size="small" :to="{ name: 'register' }" color="outline-blue" v-track.nav_create_form_click :arrow="true"> <v-button size="small" :to="{ name: 'forms.create.guest' }" color="outline-blue" v-track.nav_create_form_click :arrow="true">
Create a form Create a form
</v-button> </v-button>
@ -178,6 +180,9 @@ export default {
}), }),
userOnboarded() { userOnboarded() {
return this.user && this.user.workspaces_count > 0 return this.user && this.user.workspaces_count > 0
},
hasCrisp() {
return window.config.crisp_website_id
} }
}, },

View File

@ -31,7 +31,7 @@ export default {
}, },
props: { props: {
dropdownClass: { type: String, default: 'origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50' } dropdownClass: { type: String, default: 'origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-20' }
}, },
data () { data () {
return { return {

View File

@ -7,17 +7,17 @@
</div> </div>
<modal :show="showPremiumModal" @close="showPremiumModal=false"> <modal :show="showPremiumModal" @close="showPremiumModal=false">
<h2 class="text-nt-blue"> <h2 class="text-nt-blue">
OpenForm PRO OpnForm PRO
</h2> </h2>
<h4 v-if="user.is_subscribed && !user.has_enterprise_subscription" class="text-center mt-5"> <h4 v-if="user.is_subscribed && !user.has_enterprise_subscription" class="text-center mt-5">
We're happy to have you as a Pro customer. If you're having any issue with OpenForm, or if you have a We're happy to have you as a Pro customer. If you're having any issue with OpnForm, or if you have a
feature request, please <a href="mailto:contact@opnform.com">contact us</a>. feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
<br><br> <br><br>
If you need to collaborate, or to work with multiple workspaces, or just larger file uploads, you can If you need to collaborate, or to work with multiple workspaces, or just larger file uploads, you can
also upgrade our subscription to get an Enterprise subscription. also upgrade our subscription to get an Enterprise subscription.
</h4> </h4>
<h4 v-if="user.is_subscribed && user.has_enterprise_subscription" class="text-center mt-5"> <h4 v-if="user.is_subscribed && user.has_enterprise_subscription" class="text-center mt-5">
We're happy to have you as an Enterprise customer. If you're having any issue with OpenForm, or if you have a We're happy to have you as an Enterprise customer. If you're having any issue with OpnForm, or if you have a
feature request, please <a href="mailto:contact@opnform.com">contact us</a>. feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
</h4> </h4>
<p v-if="!user.is_subscribed" class="mt-4"> <p v-if="!user.is_subscribed" class="mt-4">
@ -25,7 +25,7 @@
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1" class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
> >
PRO PRO
</span> tag are available in the Pro plan of OpenForm. <b>You can play around and try all Pro features </span> tag are available in the Pro plan of OpnForm. <b>You can play around and try all Pro features
within within
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
to to

View File

@ -91,7 +91,7 @@
> >
<template #submit-btn="{submitForm}"> <template #submit-btn="{submitForm}">
<open-form-button :loading="loading" :theme="theme" :color="form.color" class="mt-2 px-8 mx-1" <open-form-button :loading="loading" :theme="theme" :color="form.color" class="mt-2 px-8 mx-1"
@click="submitForm" @click.prevent="submitForm"
> >
{{ form.submit_button_text }} {{ form.submit_button_text }}
</open-form-button> </open-form-button>
@ -191,6 +191,9 @@ export default {
workspace_id: this.form.workspace_id, workspace_id: this.form.workspace_id,
form_id: this.form.id form_id: this.form.id
}) })
window.localStorage.removeItem(this.form.form_pending_submission_Key)
if (response.data.redirect && response.data.redirect_url) { if (response.data.redirect && response.data.redirect_url) {
window.location.href = response.data.redirect_url window.location.href = response.data.redirect_url
} }

View File

@ -217,7 +217,15 @@ export default {
handler () { handler () {
this.formVersionId++ this.formVersionId++
} }
},
dataForm: {
deep: true,
handler () {
if(this.isPublicFormPage && this.form && this.dataFormValue){
window.localStorage.setItem(this.form.form_pending_submission_Key, JSON.stringify(this.dataFormValue))
} }
}
},
}, },
mounted () { mounted () {
@ -266,6 +274,14 @@ export default {
} }
}, },
initForm () { initForm () {
if (this.isPublicFormPage) {
const pendingData = window.localStorage.getItem(this.form.form_pending_submission_Key)
if(pendingData !== null && pendingData){
this.dataForm = new Form(JSON.parse(pendingData))
return
}
}
const formData = clonedeep(this.dataForm ? this.dataForm.data() : {}) const formData = clonedeep(this.dataForm ? this.dataForm.data() : {})
let urlPrefill = null let urlPrefill = null
if (this.isPublicFormPage && this.form.is_pro) { if (this.isPublicFormPage && this.form.is_pro) {

View File

@ -1,14 +1,13 @@
<template> <template>
<div v-if="form" id="form-editor" class="w-full flex flex-grow relative overflow-x-hidden"> <div v-if="form" id="form-editor" class="w-full flex flex-grow relative overflow-x-hidden">
<!-- Form fields selection --> <!-- Form fields selection -->
<v-tour name="tutorial" :steps="steps"/>
<div class="w-full md:w-1/2 lg:w-2/5 border-r relative overflow-y-scroll md:max-w-sm flex-shrink-0"> <div class="w-full md:w-1/2 lg:w-2/5 border-r relative overflow-y-scroll md:max-w-sm flex-shrink-0">
<div class="p-4 bg-blue-50 border-b text-nt-blue-dark md:hidden"> <div class="p-4 bg-blue-50 border-b text-nt-blue-dark md:hidden">
We suggest you create this form on a device with a larger screen such as computed. That will allow you We suggest you create this form on a device with a larger screen such as computed. That will allow you
to preview your form changes. to preview your form changes.
</div> </div>
<div class="p-4 pb-0"> <div class="p-4 pb-0">
<a href="#" @click.prevent="$router.back()" class="flex text-blue mb-2 font-semibold text-sm"> <a v-if="!isGuest" href="#" @click.prevent="$router.back()" 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"> <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" <path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round"/> stroke-linejoin="round"/>
@ -88,6 +87,11 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
isGuest: {
required: false,
type: Boolean,
default: false
},
}, },
data() { data() {
@ -123,7 +127,7 @@ export default {
{ {
target: '#v-step-0', target: '#v-step-0',
header: { header: {
title: 'Welcome to the OpenForm Editor!' title: 'Welcome to the OpnForm Editor!'
}, },
content: 'Discover <strong>your form Editor</strong>!' content: 'Discover <strong>your form Editor</strong>!'
}, },
@ -160,20 +164,16 @@ export default {
mounted() { mounted() {
this.$emit('mounted') this.$emit('mounted')
this.startTour()
}, },
methods: { methods: {
startTour() {
if (!this.user.has_forms) {
this.$tours.tutorial.start()
}
},
showValidationErrors() { showValidationErrors() {
this.showFormErrorModal = true this.showFormErrorModal = true
}, },
saveForm() { saveForm() {
if (this.isEdit) { if(this.isGuest) {
this.saveFormGuest()
} else if (this.isEdit) {
this.saveFormEdit() this.saveFormEdit()
} else { } else {
this.saveFormCreate() this.saveFormCreate()
@ -230,6 +230,9 @@ export default {
}).finally(() => { }).finally(() => {
this.updateFormLoading = false this.updateFormLoading = false
}) })
},
saveFormGuest() {
this.$emit('openRegister')
} }
} }
} }

View File

@ -266,7 +266,7 @@ export default {
]; ];
}, },
init() { init() {
if (this.$route.name === 'forms.create') { // Set Default fields if (this.$route.name === 'forms.create' || this.$route.name === 'forms.create.guest') { // Set Default fields
this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields() this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields()
} else { } else {
this.formFields = clonedeep(this.form.properties).map((field) => { this.formFields = clonedeep(this.form.properties).map((field) => {

View File

@ -5,7 +5,7 @@
</div> </div>
<dropdown v-else class="inline" dusk="nav-dropdown"> <dropdown v-else class="inline" dusk="nav-dropdown">
<template #trigger="{toggle}"> <template #trigger="{toggle}">
<v-button color="light-gray" class="mr-2" @click="toggle"> <v-button color="white" class="mr-2" @click="toggle">
<svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none" <svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path <path
@ -34,6 +34,26 @@
</svg> </svg>
View form View form
</router-link> </router-link>
<router-link v-if="isMainPage" :to="{name:'forms.edit', params: {slug: form.slug}}"
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.edit_form_click="{form_id:form.id, form_slug:form.slug}"
>
<svg class="w-4 h-4 mr-2" 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
</router-link>
<a href="#" v-if="isMainPage"
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="copyLink"
>
<svg class="w-4 h-4 mr-2" viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.00016 8.33317H4.66683C2.82588 8.33317 1.3335 6.84079 1.3335 4.99984C1.3335 3.15889 2.82588 1.6665 4.66683 1.6665H6.00016M10.0002 8.33317H11.3335C13.1744 8.33317 14.6668 6.84079 14.6668 4.99984C14.6668 3.15889 13.1744 1.6665 11.3335 1.6665H10.0002M4.66683 4.99984L11.3335 4.99984" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Copy link to share
</a>
<a href="#" <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" 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}" v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
@ -47,20 +67,7 @@
</svg> </svg>
Duplicate form Duplicate form
</a> </a>
<a href="#" <a href="#" v-if="user.template_editor"
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" 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" @click.prevent="showCreateTemplateModal=true"
> >
@ -71,8 +78,45 @@
</svg> </svg>
Create Template Create Template
</a> </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="showDeleteFormModal=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">
<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>
</dropdown> </dropdown>
<!-- Delete Form Modal -->
<modal :show="showDeleteFormModal" icon-color="red" @close="showDeleteFormModal=false" max-width="sm">
<template #icon>
<svg class="w-10 h-10" 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>
</template>
<template #title>
Delete form
</template>
<div class="p-3">
<p>
If you want to permanently delete this form and all of its data, you can do so below.
</p>
<div class="flex mt-4">
<v-button class="sm:w-1/2 mr-4" color="white" @click.prevent="showDeleteFormModal=false">Cancel</v-button>
<v-button class="sm:w-1/2" color="red" :loading="loadingDelete" @click.prevent="deleteForm">Yes, delete it</v-button>
</div>
</div>
</modal>
<create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/> <create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/>
</div> </div>
</template> </template>
@ -87,12 +131,14 @@ export default {
name: 'ExtraMenu', name: 'ExtraMenu',
components: { Dropdown, CreateTemplateModal }, components: { Dropdown, CreateTemplateModal },
props: { props: {
form: { type: Object, required: true } form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
}, },
data: () => ({ data: () => ({
loadingDuplicate: false, loadingDuplicate: false,
loadingDelete: false, loadingDelete: false,
showDeleteFormModal: false,
showCreateTemplateModal: false showCreateTemplateModal: false
}), }),
@ -104,6 +150,14 @@ export default {
}, },
methods: { methods: {
copyLink(){
const el = document.createElement('textarea')
el.value = this.form.share_url
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
},
duplicateForm() { duplicateForm() {
if (this.loadingDuplicate) return if (this.loadingDuplicate) return
this.loadingDuplicate = true this.loadingDuplicate = true
@ -127,4 +181,3 @@ export default {
} }
} }
</script> </script>

View File

@ -0,0 +1,50 @@
import Form from "vform";
export default {
methods: {
initForm() {
this.form = new Form({
title: 'My Form',
description: null,
visibility: 'public',
workspace_id: this.workspace?.id,
properties: [],
notifies: false,
slack_notifies: false,
send_submission_confirmation: false,
webhook_url: null,
// Customization
theme: 'default',
width: 'centered',
dark_mode: 'auto',
color: '#3B82F6',
hide_title: false,
no_branding: false,
uppercase_labels: true,
transparent_background: false,
closes_at: null,
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
// Submission
submit_button_text: 'Submit',
re_fillable: false,
re_fill_button_text: 'Fill Again',
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
notification_sender: 'OpnForm',
notification_subject: 'We saved your answers',
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
notifications_include_submission: true,
use_captcha: false,
is_rating: false,
rating_max_value: 5,
max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
// Security & Privacy
can_be_indexed: true
})
},
}
}

View File

@ -0,0 +1,105 @@
<template>
<div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<form @submit.prevent="login" @keydown="form.onKeydown($event)" class="mt-4">
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<!-- Password -->
<text-input native-type="password" placeholder="Your password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Remember Me -->
<div class="relative flex items-center my-5">
<v-checkbox v-model="remember" class="w-full md:w-1/2" name="remember" size="small">
{{ $t('remember_me') }}
</v-checkbox>
<div class="w-full md:w-1/2 text-right">
<a href="#" @click.prevent="showForgotModal=true" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700">
Forgot your password?
</a>
</div>
</div>
<!-- Submit Button -->
<v-button dusk="btn_login" :loading="form.busy">Log in to continue</v-button>
<p class="text-gray-500 mt-4">
Don't have an account?
<a href="#" v-if="isQuick" @click.prevent="$emit('openRegister')" class="font-semibold ml-1">Sign Up</a>
<router-link v-else :to="{name:'register'}" class="font-semibold ml-1">Sign Up</router-link>
</p>
</form>
</div>
</template>
<script>
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 {
name: 'LoginForm',
components: {
OpenFormFooter,
Testimonials,
ForgotPasswordModal
},
props: {
isQuick: {
type: Boolean,
required: false,
default: false
}
},
data: () => ({
form: new Form({
email: '',
password: ''
}),
remember: false,
showForgotModal: false
}),
methods: {
async login () {
// Submit the form.
const { data } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', {
token: data.token,
remember: this.remember
})
// Fetch the user.
await this.$store.dispatch('auth/fetchUser')
// Redirect home.
this.redirect()
},
redirect () {
if(this.isQuick){
this.$emit('afterQuickLogin')
return
}
const intendedUrl = Cookies.get('intended_url')
if (intendedUrl) {
Cookies.remove('intended_url')
this.$router.push({ path: intendedUrl })
} else {
this.$router.push({ name: 'home' })
}
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<div>
<!-- Login modal -->
<modal :show="showLoginModal" @close="showLoginModal=false" max-width="lg">
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
<template #title>
Login to OpnForm
</template>
<div class="px-4">
<login-form :isQuick="true" @openRegister="openRegister" @afterQuickLogin="afterQuickLogin" />
</div>
</modal>
<!-- Register modal -->
<modal :show="showRegisterModal" @close="$emit('close')" max-width="lg">
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
<template #title>
Create an account
</template>
<div class="px-4">
<register-form :isQuick="true" @openLogin="openLogin" @afterQuickLogin="afterQuickLogin" />
</div>
</modal>
</div>
</template>
<script>
import LoginForm from './LoginForm'
import RegisterForm from './RegisterForm'
export default {
name: 'QuickRegister',
components: {
LoginForm,
RegisterForm
},
props: {
showRegisterModal: {
type: Boolean,
required: true
}
},
data: () => ({
showLoginModal: false,
}),
mounted() {
},
methods: {
openLogin(){
this.showLoginModal = true
this.$emit('close')
},
openRegister(){
this.showLoginModal = false
this.$emit('reopen')
},
afterQuickLogin(){
this.showLoginModal = false
this.$emit('afterLogin')
}
}
}
</script>

View File

@ -0,0 +1,128 @@
<template>
<div>
<form @submit.prevent="register" @keydown="form.onKeydown($event)" class="mt-4">
<!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" placeholder="Your name" :required="true" />
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option"
label="How did you hear about us?" :required="true"
/>
<!-- Password -->
<text-input native-type="password" placeholder="Enter password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password"
name="password_confirmation" :label="$t('confirm_password')"
/>
<checkbox-input :form="form" name="agree_terms" :required="true">
<template #label>
I agree with the <router-link :to="{name:'terms-conditions'}" target="_blank">Terms and conditions</router-link> and <router-link :to="{name:'privacy-policy'}" target="_blank">Privacy policy</router-link> of the website and I accept them.
</template>
</checkbox-input>
<!-- Submit Button -->
<v-button :loading="form.busy">Create an account</v-button>
<p class="text-gray-500 mt-4">
Already have an account?
<a href="#" v-if="isQuick" @click.prevent="$emit('openLogin')" class="font-semibold ml-1">Log In</a>
<router-link v-else :to="{name:'login'}" class="font-semibold ml-1">Log In</router-link>
</p>
<!-- GitHub Register Button -->
<login-with-github />
</form>
</div>
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
import SelectInput from '../../../components/forms/SelectInput'
import { initCrisp } from '../../../middleware/check-auth'
export default {
name: 'RegisterForm',
components: {
SelectInput,
LoginWithGithub,
},
props: {
isQuick: {
type: Boolean,
required: false,
default: false
}
},
data: () => ({
form: new Form({
name: '',
email: '',
password: '',
password_confirmation: '',
agree_terms: false
}),
mustVerifyEmail: false
}),
computed: {
hearAboutUsOptions () {
const options = [
{ name: 'Facebook', value: 'facebook' },
{ name: 'Twitter', value: 'twitter' },
{ name: 'Reddit', value: 'reddit' },
{ name: 'Github', value: 'github' },
{ name: 'Search Engine (Google, DuckDuckGo...)', value: 'search_engine' },
{ name: 'Friend or Colleague', value: 'friend_colleague' },
{ name: 'Blog/Article', value: 'blog_article' }
].map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
options.push({ name: 'Other', value: 'other' })
return options
}
},
methods: {
async register () {
// Register the user.
const { data } = await this.form.post('/api/register')
// Must verify email fist.
if (data.status) {
this.mustVerifyEmail = true
} else {
// Log in the user.
const { data: { token } } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', { token })
// Update the user.
await this.$store.dispatch('auth/updateUser', { user: data })
// Track event
this.$logEvent('register', { source: this.form.hear_about_us })
initCrisp(data).then(() => {
this.$getCrisp().push(['set', 'session:event', [[['register', {}, 'blue']]]])
})
// Redirect
if(this.isQuick){
this.$emit('afterQuickLogin')
}else{
this.$router.push({ name: 'forms.create' })
}
}
}
}
}
</script>

View File

@ -1,7 +1,5 @@
<template> <template>
<div> <div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<div class="flex mt-6 mb-10"> <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: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"> <div class="w-full md:w-1/2 md:p-6">
@ -11,35 +9,7 @@
</h2> </h2>
<small>Welcome back! Please enter your details.</small> <small>Welcome back! Please enter your details.</small>
<form @submit.prevent="login" @keydown="form.onKeydown($event)" class="mt-4"> <login-form />
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<!-- Password -->
<text-input native-type="password" placeholder="Your password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Remember Me -->
<div class="relative flex items-center my-5">
<v-checkbox v-model="remember" class="w-full md:w-1/2" name="remember" size="small">
{{ $t('remember_me') }}
</v-checkbox>
<div class="w-full md:w-1/2 text-right">
<a href="#" @click.prevent="showForgotModal=true" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700">
Forgot your password?
</a>
</div>
</div>
<!-- Submit Button -->
<v-button dusk="btn_login" :loading="form.busy">Log in to continue</v-button>
<p class="text-gray-500 mt-4">
Don't have an account? <router-link :to="{name:'register'}" class="font-semibold ml-1">Sign Up</router-link>
</p>
</form>
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 "> <div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 ">
@ -86,17 +56,15 @@
</template> </template>
<script> <script>
import Form from 'vform'
import Cookies from 'js-cookie'
import OpenFormFooter from '../../components/pages/OpenFormFooter' import OpenFormFooter from '../../components/pages/OpenFormFooter'
import Testimonials from '../../components/pages/welcome/Testimonials' import Testimonials from '../../components/pages/welcome/Testimonials'
import ForgotPasswordModal from './ForgotPasswordModal' import LoginForm from './components/LoginForm'
export default { export default {
components: { components: {
OpenFormFooter, OpenFormFooter,
Testimonials, Testimonials,
ForgotPasswordModal LoginForm
}, },
middleware: 'guest', middleware: 'guest',
@ -106,42 +74,11 @@ export default {
}, },
data: () => ({ data: () => ({
form: new Form({
email: '',
password: ''
}),
remember: false,
showForgotModal: false
}), }),
methods: { methods: {
async login () {
// Submit the form.
const { data } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', {
token: data.token,
remember: this.remember
})
// Fetch the user.
await this.$store.dispatch('auth/fetchUser')
// Redirect home.
this.redirect()
},
redirect () {
const intendedUrl = Cookies.get('intended_url')
if (intendedUrl) {
Cookies.remove('intended_url')
this.$router.push({ path: intendedUrl })
} else {
this.$router.push({ name: 'home' })
}
}
} }
} }
</script> </script>

View File

@ -1,56 +1,20 @@
<template> <template>
<div> <div>
<div class="flex mt-6 mb-10"> <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:max-w-6xl mx-auto px-4 flex items-center md:flex-row-reverse flex-wrap">
<div class="w-full md:w-1/2 md:p-6"> <div class="w-full lg:w-1/2 md:p-6">
<div class="border rounded-md p-6 shadow-md sticky top-4"> <div class="border rounded-md p-6 shadow-md sticky top-4">
<h2 class="font-semibold text-2xl"> <h2 class="font-semibold text-2xl">
Create an account Create an account
</h2> </h2>
<small>Sign up in less than 2 minutes.</small> <small>Sign up in less than 2 minutes.</small>
<form @submit.prevent="register" @keydown="form.onKeydown($event)" class="mt-4"> <register-form />
<!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" placeholder="Your name" :required="true" />
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option"
label="How did you hear about us?" :required="true"
/>
<!-- Password -->
<text-input native-type="password" placeholder="Enter password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password"
name="password_confirmation" :label="$t('confirm_password')"
/>
<checkbox-input :form="form" name="agree_terms" :required="true">
<template #label>
I agree with the <router-link :to="{name:'terms-conditions'}" target="_blank">Terms and conditions</router-link> and <router-link :to="{name:'privacy-policy'}" target="_blank">Privacy policy</router-link> of the website and I accept them.
</template>
</checkbox-input>
<!-- Submit Button -->
<v-button :loading="form.busy">Create an account</v-button>
<p class="text-gray-500 mt-4">
Already have an account? <router-link :to="{name:'login'}" class="font-semibold ml-1">Log In</router-link>
</p>
<!-- GitHub Register Button -->
<login-with-github />
</form>
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 "> <div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0 ">
<h1 class="font-bold"> <h1 class="font-bold">
Create beautiful Notion forms and share them anywhere Create beautiful forms and share them anywhere
</h1> </h1>
<p class="text-gray-900 my-4 text-lg"> <p class="text-gray-900 my-4 text-lg">
It takes seconds, you don't need to know how to code and it's free. It takes seconds, you don't need to know how to code and it's free.
@ -81,9 +45,9 @@
Unlimited submissions Unlimited submissions
</p> </p>
</div> </div>
<div class="mt-3 p-6"> <!-- <div class="mt-3 p-6">-->
<testimonials /> <!-- <testimonials />-->
</div> <!-- </div>-->
</div> </div>
</div> </div>
</div> </div>
@ -92,19 +56,15 @@
</template> </template>
<script> <script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
import SelectInput from '../../components/forms/SelectInput'
import OpenFormFooter from '../../components/pages/OpenFormFooter' import OpenFormFooter from '../../components/pages/OpenFormFooter'
import { initCrisp } from '../../middleware/check-auth'
import Testimonials from '../../components/pages/welcome/Testimonials' import Testimonials from '../../components/pages/welcome/Testimonials'
import RegisterForm from './components/RegisterForm'
export default { export default {
components: { components: {
Testimonials, Testimonials,
SelectInput, OpenFormFooter,
LoginWithGithub, RegisterForm
OpenFormFooter
}, },
middleware: 'guest', middleware: 'guest',
@ -114,62 +74,13 @@ export default {
}, },
data: () => ({ data: () => ({
form: new Form({
name: '',
email: '',
password: '',
password_confirmation: '',
agree_terms: false
}),
mustVerifyEmail: false
}), }),
computed: { computed: {
hearAboutUsOptions () {
const options = [
{ name: 'Facebook', value: 'facebook' },
{ name: 'Twitter', value: 'twitter' },
{ name: 'Reddit', value: 'reddit' },
{ name: 'Github', value: 'github' },
{ name: 'Search Engine (Google, DuckDuckGo...)', value: 'search_engine' },
{ name: 'Friend or Colleague', value: 'friend_colleague' },
{ name: 'Blog/Article', value: 'blog_article' }
].map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
options.push({ name: 'Other', value: 'other' })
return options
}
}, },
methods: { methods: {
async register () {
// Register the user.
const { data } = await this.form.post('/api/register')
// Must verify email fist.
if (data.status) {
this.mustVerifyEmail = true
} else {
// Log in the user.
const { data: { token } } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', { token })
// Update the user.
await this.$store.dispatch('auth/updateUser', { user: data })
// Track event
this.$logEvent('register', { source: this.form.hear_about_us })
initCrisp(data).then(() => {
this.$getCrisp().push(['set', 'session:event', [[['register', {}, 'blue']]]])
})
// Redirect home.
this.$router.push({ name: 'forms.create' })
}
}
} }
} }
</script> </script>

View File

@ -0,0 +1,153 @@
<template>
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:style="{
'max-height': editorMaxHeight + 'px'
}" :error="error"
:isGuest="isGuest"
@mounted="onResize"
@openRegister="openRegister"
/>
<div v-else class="text-center mt-4 py-6">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
</div>
</transition>
<quick-register :showRegisterModal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
@afterLogin="afterLogin"/>
</div>
</template>
<script>
import store from '~/store'
import Form from 'vform'
import {mapState, mapActions} from 'vuex'
import QuickRegister from '../auth/components/QuickRegister'
import initForm from "../../mixins/form_editor/initForm"
const FormEditor = () => import('../../components/open/forms/components/FormEditor')
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
name: 'CreateFormGuest',
mixins: [initForm],
components: {
FormEditor,
QuickRegister
},
middleware: 'guest',
metaInfo() {
return {title: 'Create a new Form as Guest'}
},
beforeRouteEnter(to, from, next) {
loadTemplates()
next()
},
data() {
return {
stateReady: false,
loading: false,
error: '',
editorMaxHeight: 500,
registerModal: false,
isGuest: true
}
},
computed: {
...mapState({
workspaces: state => state['open/workspaces'].content,
workspacesLoading: state => state['open/workspaces'].loading,
}),
form: {
get() {
return this.$store.state['open/working_form'].content
},
/* We add a setter */
set(value) {
this.$store.commit('open/working_form/set', value)
}
},
workspace() {
return this.$store.getters['open/workspaces/getCurrent']()
},
},
watch: {
workspace() {
if (this.workspace) {
this.form.workspace_id = this.workspace.id
}
}
},
mounted() {
// Set as guest user
const guestWorkspace = {
id: null,
name: "Guest Workspace",
is_enterprise: false,
is_pro: false
}
this.$store.commit('open/workspaces/set', [guestWorkspace])
this.$store.commit('open/workspaces/setCurrentId', guestWorkspace.id)
this.initForm()
if (this.$route.query.template !== undefined && this.$route.query.template) {
const template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
if (template && template.structure) {
this.form = new Form({...this.form.data(), ...template.structure})
}
}
this.closeAlert()
this.stateReady = true
},
created() {
window.addEventListener('resize', this.onResize)
},
destroyed() {
window.removeEventListener('resize', this.onResize)
},
methods: {
...mapActions({
loadWorkspaces: 'open/workspaces/load'
}),
/**
* Compute max height of editor
*/
onResize() {
if (this.$refs.editor) {
this.editorMaxHeight = window.innerHeight - this.$refs.editor.$el.offsetTop
}
},
openRegister() {
this.registerModal = true
},
afterLogin() {
this.registerModal = false
this.isGuest = false
this.loadWorkspaces()
setTimeout(() => {
this.$refs.editor.saveFormCreate()
}, 500)
}
}
}
</script>

View File

@ -21,6 +21,7 @@
import store from '~/store' import store from '~/store'
import Form from 'vform' import Form from 'vform'
import {mapState, mapActions} from 'vuex' import {mapState, mapActions} from 'vuex'
import initForm from "../../mixins/form_editor/initForm";
const FormEditor = () => import('../../components/open/forms/components/FormEditor') const FormEditor = () => import('../../components/open/forms/components/FormEditor')
@ -33,6 +34,8 @@ const loadTemplates = function () {
export default { export default {
name: 'CreateForm', name: 'CreateForm',
mixins: [initForm],
components: { components: {
FormEditor, FormEditor,
}, },
@ -41,7 +44,7 @@ export default {
return {title: 'Create a new Form'} return {title: 'Create a new Form'}
}, },
beforeRouteEnter (to, from, next) { beforeRouteEnter(to, from, next) {
loadTemplates() loadTemplates()
next() next()
}, },
@ -75,12 +78,6 @@ export default {
workspace() { workspace() {
return this.$store.getters['open/workspaces/getCurrent']() return this.$store.getters['open/workspaces/getCurrent']()
}, },
fromOnboarding() {
return this.$route.params.from_onboarding
},
fbGroupLink() {
return window.config.links.facebook_group
}
}, },
watch: { watch: {
@ -95,15 +92,12 @@ export default {
}, },
mounted() { mounted() {
if(this.$route.query.template !== undefined && this.$route.query.template){
let template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
if(template && template.structure){
this.form = new Form(template.structure)
}else{
this.initForm() this.initForm()
if (this.$route.query.template !== undefined && this.$route.query.template) {
const template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
if (template && template.structure) {
this.form = new Form({...this.form.data(), ...template.structure})
} }
}else{
this.initForm()
} }
this.closeAlert() this.closeAlert()
this.loadWorkspaces() this.loadWorkspaces()
@ -122,50 +116,6 @@ export default {
...mapActions({ ...mapActions({
loadWorkspaces: 'open/workspaces/loadIfEmpty' loadWorkspaces: 'open/workspaces/loadIfEmpty'
}), }),
initForm() {
this.form = new Form({
title: 'My Form',
description: null,
visibility: 'public',
workspace_id: this.workspace?.id,
properties: [],
notifies: false,
slack_notifies: false,
send_submission_confirmation: false,
webhook_url: null,
// Customization
theme: 'default',
width: 'centered',
dark_mode: 'auto',
color: '#3B82F6',
hide_title: false,
no_branding: false,
uppercase_labels: true,
transparent_background: false,
closes_at: null,
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
// Submission
submit_button_text: 'Submit',
re_fillable: false,
re_fill_button_text: 'Fill Again',
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
notification_sender: 'OpnForm',
notification_subject: 'We saved your answers',
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
notifications_include_submission: true,
use_captcha: false,
is_rating: false,
rating_max_value: 5,
max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
// Security & Privacy
can_be_indexed: true
})
},
/** /**
* Compute max height of editor * Compute max height of editor
*/ */

View File

@ -1,21 +1,27 @@
<template> <template>
<div class="flex flex-col min-h-full mt-6"> <div class="bg-white">
<div class="w-full flex-grow md:w-3/5 lg:w-1/2 md:mx-auto md:max-w-2xl px-4"> <div class="flex bg-gray-50 pb-5">
<div> <div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
<div class="flex flex-wrap items-center mt-6 mb-4"> <div class="pt-4 pb-0">
<h2 class="text-nt-blue text-3xl font-bold flex-grow"> <div class="flex">
<h2 class="flex-grow text-gray-900">
Your Forms Your Forms
</h2> </h2>
<v-button v-track.create_form_click class="mt-4 sm:mt-0" :to="{name:'forms.create'}" @click="showCreateFormModal=true"> <v-button v-track.create_form_click :to="{name:'forms.create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Create a new form Create a new form
</v-button> </v-button>
</div> </div>
<small class="flex text-gray-500">Manage your forms and submissions.</small>
<p v-if="!formsLoading && enrichedForms.length === 0 && !isFilteringForms"> </div>
You don't have any form yet. </div>
</p> </div>
<div v-else-if="forms.length > 0" class="mb-10"> <div class="flex bg-white">
<text-input v-if="forms.length > 5" class="mb-6" :form="searchForm" name="search" label="Search a form" <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">
<text-input v-if="forms.length > 0" class="mb-6" :form="searchForm" name="search" label="Search a form"
placeholder="Name of form to search" placeholder="Name of form to search"
/> />
<div v-if="allTags.length > 0" class="mb-6"> <div v-if="allTags.length > 0" class="mb-6">
@ -27,18 +33,36 @@
{{ tag }} {{ tag }}
</div> </div>
</div> </div>
<div v-if="enrichedForms && enrichedForms.length" class="border border border-gray-300 dark:bg-notion-dark-light rounded-md w-full"> <div v-if="!formsLoading && enrichedForms.length === 0" class="flex flex-wrap justify-center max-w-4xl">
<div v-for="(form, index) in enrichedForms" :key="form.id" <img loading="lazy" class="w-56"
class="p-4 w-full mx-auto border-gray-300 hover:bg-blue-100 dark:hover:bg-blue-900 transition-colors cursor-pointer relative" :src="asset('img/pages/forms/search_notfound.png')" alt="search-not-found">
:class="{'border-t':index!==0, 'bg-gray-50 dark:bg-gray-400':form.visibility=='draft'}" <h3 class="w-full mt-4 text-center text-gray-900 font-semibold">No forms found</h3>
<div v-if="isFilteringForms && enrichedForms.length === 0 && searchForm.search" class="mt-2 w-full text-center">
Your search "{{searchForm.search}}" did not match any forms. Please try again.
</div>
<v-button v-if="forms.length === 0" class="mt-4" v-track.create_form_click :to="{name:'forms.create'}">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Create a new form
</v-button>
</div>
<div v-else-if="forms.length > 0" class="mb-10">
<div v-if="enrichedForms && enrichedForms.length">
<div v-for="(form) in enrichedForms" :key="form.id"
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center"
> >
<div class="items-center space-x-4 truncate"> <div class="flex-grow items-center truncate cursor-pointer" role="button" @click.prevent="viewForm(form)">
<p class="truncate float-left"> <span class="font-semibold text-gray-900 dark:text-white">{{ form.title }}</span>
{{ form.title }} <span v-if="form.submissions_count" class="text-gray-400 ml-1">- {{ <ul class="flex text-gray-500">
form.submissions_count <li class="pr-1">{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}</li>
}} Submission{{ form.submissions_count > 0 ? 's' : '' }}</span> <li class="list-disc ml-6 pr-1">{{ form.submissions_count }}
</p> submission{{ form.submissions_count > 0 ? 's' : '' }}
<div v-if="form.tags && form.tags.length > 0" class="float-right hidden sm:block"> </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>
<div v-if="form.tags && form.tags.length > 0" class="mt-1">
<template v-for="(tag,i) in form.tags"> <template v-for="(tag,i) in form.tags">
<div v-if="i<1" :key="tag" <div v-if="i<1" :key="tag"
class="bg-gray-300 dark:bg-gray-700 text-white px-2 py-1 mr-2 text-xs inline rounded-lg font-semibold" class="bg-gray-300 dark:bg-gray-700 text-white px-2 py-1 mr-2 text-xs inline rounded-lg font-semibold"
@ -53,22 +77,16 @@
</template> </template>
</div> </div>
</div> </div>
<router-link class="absolute inset-0" <extra-menu :form="form" :isMainPage="true" />
:to="{params:{slug:form.slug},name:'forms.show'}"
/>
</div> </div>
</div> </div>
<p class="text-gray-400 dark:text-gray-600 mt-2 px-4">
You have {{ forms.length }} forms<template v-if="isFilteringForms">
({{ enrichedForms.length }} matching search criteria)
</template>.
</p>
</div> </div>
<div v-if="formsLoading" class="text-center"> <div v-if="formsLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" /> <loader class="h-6 w-6 text-nt-blue mx-auto" />
</div> </div>
</div> </div>
</div> </div>
</div>
<open-form-footer class="mt-8 border-t" /> <open-form-footer class="mt-8 border-t" />
</div> </div>
</template> </template>
@ -80,6 +98,7 @@ import Fuse from 'fuse.js'
import Form from 'vform' import Form from 'vform'
import TextInput from '../components/forms/TextInput' import TextInput from '../components/forms/TextInput'
import OpenFormFooter from '../components/pages/OpenFormFooter' import OpenFormFooter from '../components/pages/OpenFormFooter'
import ExtraMenu from '../components/pages/forms/show/ExtraMenu'
const loadForms = function () { const loadForms = function () {
store.commit('open/forms/startLoading') store.commit('open/forms/startLoading')
@ -89,7 +108,7 @@ const loadForms = function () {
} }
export default { export default {
components: { OpenFormFooter, TextInput }, components: { OpenFormFooter, TextInput, ExtraMenu },
beforeRouteEnter (to, from, next) { beforeRouteEnter (to, from, next) {
loadForms() loadForms()
@ -127,6 +146,9 @@ export default {
} else { } else {
this.selectedTags.splice(idx, 1) this.selectedTags.splice(idx, 1)
} }
},
viewForm (form) {
this.$router.push({name: 'forms.show', params: {slug: form.slug}})
} }
}, },

View File

@ -2,9 +2,9 @@
<div class="flex flex-col min-h-full mt-6"> <div class="flex flex-col min-h-full mt-6">
<div class="w-full flex-grow md:w-4/5 lg:w-2/3 md:mx-auto md:max-w-4xl px-4"> <div class="w-full flex-grow md:w-4/5 lg:w-2/3 md:mx-auto md:max-w-4xl px-4">
<breadcrumb :path="breadcrumbs" /> <breadcrumb :path="breadcrumbs"/>
<div v-if="templatesLoading" class="text-center"> <div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" /> <loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div> </div>
<p v-else-if="template === null || !template"> <p v-else-if="template === null || !template">
Template does not exist. Template does not exist.
@ -16,7 +16,9 @@
</h2> </h2>
</div> </div>
<div class="mb-10"> <div class="mb-10">
<img :src="template.image_url" alt="" class="w-full shadow-xl rounded-lg my-5"/> <div class="w-full shadow-xl rounded-lg my-5 max-h-72 flex items-center justify-center overflow-hidden">
<img :src="template.image_url" alt="Template cover image" class="w-full object-cover"/>
</div>
<div v-html="template.description"></div> <div v-html="template.description"></div>
<div class="mt-5 text-center"> <div class="mt-5 text-center">
<v-button class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}"> <v-button class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}">
@ -24,11 +26,13 @@
</v-button> </v-button>
</div> </div>
<h3 class="text-center text-gray-500">Template Preview</h3> <h3 class="text-center text-gray-500 mt-6 mb-2">Template Preview</h3>
<open-complete-form ref="open-complete-form" :form="form" :creating="true" class="my-5 p-4 bg-gray-50 rounded-lg"/> <open-complete-form ref="open-complete-form" :form="form" :creating="true"
class="mb-4 p-4 bg-gray-50 rounded-lg overflow-hidden"/>
<div v-if="template.questions.length > 0" id="questions">
<h3 class="text-xl font-semibold mb-3">Frequently asked questions</h3> <h3 class="text-xl font-semibold mb-3">Frequently asked questions</h3>
<div v-if="template.questions.length > 0" class="mt-5 pt-2"> <div class="mt-5 pt-2">
<div v-for="(ques,ques_key) in template.questions" :key="ques_key" class="my-3 border rounded-lg"> <div v-for="(ques,ques_key) in template.questions" :key="ques_key" class="my-3 border rounded-lg">
<h5 class="border-b p-2">{{ ques.question }}</h5> <h5 class="border-b p-2">{{ ques.question }}</h5>
<div class="p-2" v-html="ques.answer"></div> <div class="p-2" v-html="ques.answer"></div>
@ -37,45 +41,46 @@
</div> </div>
</div> </div>
</div> </div>
<open-form-footer class="mt-8 border-t" />
</div> </div>
</template> <open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script> <script>
import store from '~/store' import store from '~/store'
import Form from 'vform' import Form from 'vform'
import { mapGetters, mapState } from 'vuex' import {mapGetters, mapState} from 'vuex'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter' import OpenFormFooter from '../../components/pages/OpenFormFooter'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm' import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm'
import Breadcrumb from "../../components/common/Breadcrumb"; import Breadcrumb from "../../components/common/Breadcrumb";
const loadTemplates = function () { const loadTemplates = function () {
store.commit('open/templates/startLoading') store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => { store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading') store.commit('open/templates/stopLoading')
}) })
} }
export default { export default {
components: {Breadcrumb, OpenFormFooter, OpenCompleteForm }, components: {Breadcrumb, OpenFormFooter, OpenCompleteForm},
beforeRouteEnter (to, from, next) { beforeRouteEnter(to, from, next) {
loadTemplates() loadTemplates()
next() next()
}, },
props: { props: {
metaTitle: { type: String, default: 'Templates' }, metaTitle: {type: String, default: 'Templates'},
metaDescription: { type: String, default: 'Public templates for create form quickly!' } metaDescription: {type: String, default: 'Public templates for create form quickly!'}
}, },
data () { data() {
return { return {}
}
}, },
mounted () {}, mounted() {
},
methods: {}, methods: {},
@ -83,18 +88,18 @@
...mapState({ ...mapState({
templatesLoading: state => state['open/templates'].loading templatesLoading: state => state['open/templates'].loading
}), }),
breadcrumbs () { breadcrumbs() {
if (!this.template) { if (!this.template) {
return [{ route: { name: 'templates' }, label: 'Templates' }] return [{route: {name: 'templates'}, label: 'Templates'}]
} }
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.template.name }] return [{route: {name: 'templates'}, label: 'Templates'}, {label: this.template.name}]
}, },
template () { template() {
return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug) return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug)
}, },
form (){ form() {
return new Form(this.template.structure) return new Form(this.template.structure)
} }
} }
} }
</script> </script>

View File

@ -16,7 +16,7 @@
>it's free</span>. >it's free</span>.
</h3> </h3>
<div class="mt-6 flex justify-center"> <div class="mt-6 flex justify-center">
<v-button class="mr-1" :to="{ name: 'register' }" :arrow="true"> <v-button class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">
Create a form for FREE Create a form for FREE
</v-button> </v-button>
</div> </div>
@ -61,19 +61,19 @@
<more-features /> <more-features />
<div class="pt-20 pb-5 text-center bg-white dark:bg-notion-dark-light"> <!-- <div class="pt-20 pb-5 text-center bg-white dark:bg-notion-dark-light">-->
<h3 class="font-semibold text-3xl">See what people are saying</h3> <!-- <h3 class="font-semibold text-3xl">See what people are saying</h3>-->
<p class="w-full mt-2 mb-8"> <!-- <p class="w-full mt-2 mb-8">-->
These are the stories of our customers who have joined us with great pleasure when using this crazy feature. <!-- These are the stories of our customers who have joined us with great pleasure when using this crazy feature.-->
</p> <!-- </p>-->
<testimonials/> <!-- <testimonials/>-->
</div> <!-- </div>-->
<div class="w-full bg-blue-900 p-12 md:p-24 text-center"> <div class="w-full bg-blue-900 p-12 md:p-24 text-center">
<h4 class="font-semibold text-3xl text-white">Take your forms to the next level</h4> <h4 class="font-semibold text-3xl text-white">Take your forms to the next level</h4>
<p class="text-gray-300 my-8">No trial. Generous, unlimited free plan.</p> <p class="text-gray-300 my-8">No trial. Generous, unlimited free plan.</p>
<div class="mt-6 flex justify-center"> <div class="mt-6 flex justify-center">
<v-button :to="{ name: 'register' }" v-track.welcome_create_form_click :arrow="true" color="blue"> <v-button :to="{ name: 'forms.create.guest' }" v-track.welcome_create_form_click :arrow="true" color="blue">
Create a form for FREE Create a form for FREE
</v-button> </v-button>
</div> </div>

View File

@ -8,6 +8,7 @@ export default [
// Forms // Forms
{ path: '/forms/create', name: 'forms.create', component: page('forms/create.vue') }, { path: '/forms/create', name: 'forms.create', component: page('forms/create.vue') },
{ path: '/forms/create/guest', name: 'forms.create.guest', component: page('forms/create-guest.vue') },
{ path: '/forms/:slug/edit', name: 'forms.edit', component: page('forms/edit.vue') }, { path: '/forms/:slug/edit', name: 'forms.edit', component: page('forms/edit.vue') },
{ {
path: '/forms/:slug/show', path: '/forms/:slug/show',

View File

@ -1,7 +1,10 @@
<?php <?php
it('can create template', function () { it('can create template', function () {
$user = $this->actingAsUser(); $user = $this->createUser([
'email' => 'admin@opnform.com'
]);
$this->actingAsUser($user);
// Create Form // Create Form
$workspace = $this->createUserWorkspace($user); $workspace = $this->createUserWorkspace($user);

View File

@ -135,9 +135,9 @@ trait TestHelpers
return $form; return $form;
} }
public function createUser() public function createUser(array $data = [])
{ {
return \App\Models\User::factory()->create(); return \App\Models\User::factory()->create($data);
} }
public function createProUser() public function createProUser()

View File

@ -25,3 +25,12 @@ it('can parse filenames', function () {
expect($parsedFilename->getMovedFileName())->toBeNull(); expect($parsedFilename->getMovedFileName())->toBeNull();
}); });
it('can handles non-utf characters', function () {
$fileName = 'Образец_для_заполнения_85e16d7b-58ed-43bc-8dce-7d3ff7d69f41.png';
$parsedFilename = \App\Service\Storage\StorageFileNameParser::parse($fileName);
expect($parsedFilename->fileName)->toBe('Образец_для_заполнения');
expect($parsedFilename->uuid)->toBe('85e16d7b-58ed-43bc-8dce-7d3ff7d69f41');
expect($parsedFilename->extension)->toBe('png');
expect($parsedFilename->getMovedFileName())->toBe($fileName);
});