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

View File

@ -58,7 +58,8 @@ class FormResource extends JsonResource
'is_closed' => $this->is_closed,
'is_password_protected' => false,
'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,
'has_enterprise_subscription' => $this->has_enterprise_subscription,
'admin' => $this->admin,
'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms,
] : [];

View File

@ -166,6 +166,14 @@ class Form extends Model
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()
{
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'));
}
public function getTemplateEditorAttribute()
{
return $this->admin || in_array($this->email, config('services.template_editor_emails'));
}
/**
* =================================
* 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\Integration\FormZapierWebhook;
use App\Models\Template;
use App\Models\Workspace;
use App\Models\User;
use App\Policies\FormPolicy;
use App\Policies\Integration\FormZapierWebhookPolicy;
use App\Policies\TemplatePolicy;
use App\Policies\WorkspacePolicy;
use App\Policies\UserPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -22,7 +24,8 @@ class AuthServiceProvider extends ServiceProvider
protected $policies = [
Form::class => FormPolicy::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
{
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;
}

View File

@ -52,5 +52,6 @@ return [
'amplitude_code' => env('AMPLITUDE_CODE'),
'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') ?? '')
];

2349
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-router": "^3.5.2",
"vue-tailwind": "^2.5.0",
"vue-tour": "^2.0.0",
"vue2-editor": "^2.10.3",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",

View File

@ -32,7 +32,7 @@
<server name="MAIL_FROM_NAME" value="NotionForms"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<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"/>
</php>
</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 Base from './base'
import VueTour from 'vue-tour'
import '~/plugins'
import '~/components'
@ -16,10 +14,6 @@ Vue.config.productionTip = false
Vue.mixin(Base)
Vue.use(LoadScript)
/* Vue Tour */
require('vue-tour/dist/vue-tour.css')
Vue.use(VueTour)
/* eslint-disable no-new */
new Vue({
i18n,

View File

@ -15,15 +15,17 @@
<workspace-dropdown class="ml-6"/>
</div>
<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">
Integrations
Templates
</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"
@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
</a>
@ -97,7 +99,7 @@
{{ $t('login') }}
</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
</v-button>
@ -178,6 +180,9 @@ export default {
}),
userOnboarded() {
return this.user && this.user.workspaces_count > 0
},
hasCrisp() {
return window.config.crisp_website_id
}
},

View File

@ -31,7 +31,7 @@ export default {
},
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 () {
return {

View File

@ -7,17 +7,17 @@
</div>
<modal :show="showPremiumModal" @close="showPremiumModal=false">
<h2 class="text-nt-blue">
OpenForm PRO
OpnForm PRO
</h2>
<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>.
<br><br>
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.
</h4>
<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>.
</h4>
<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"
>
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
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
to

View File

@ -91,7 +91,7 @@
>
<template #submit-btn="{submitForm}">
<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 }}
</open-form-button>
@ -191,6 +191,9 @@ export default {
workspace_id: this.form.workspace_id,
form_id: this.form.id
})
window.localStorage.removeItem(this.form.form_pending_submission_Key)
if (response.data.redirect && response.data.redirect_url) {
window.location.href = response.data.redirect_url
}

View File

@ -217,7 +217,15 @@ export default {
handler () {
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 () {
@ -266,6 +274,14 @@ export default {
}
},
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() : {})
let urlPrefill = null
if (this.isPublicFormPage && this.form.is_pro) {

View File

@ -1,14 +1,13 @@
<template>
<div v-if="form" id="form-editor" class="w-full flex flex-grow relative overflow-x-hidden">
<!-- 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="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
to preview your form changes.
</div>
<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">
<path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round"/>
@ -88,6 +87,11 @@ export default {
type: Boolean,
default: false
},
isGuest: {
required: false,
type: Boolean,
default: false
},
},
data() {
@ -123,7 +127,7 @@ export default {
{
target: '#v-step-0',
header: {
title: 'Welcome to the OpenForm Editor!'
title: 'Welcome to the OpnForm Editor!'
},
content: 'Discover <strong>your form Editor</strong>!'
},
@ -160,20 +164,16 @@ export default {
mounted() {
this.$emit('mounted')
this.startTour()
},
methods: {
startTour() {
if (!this.user.has_forms) {
this.$tours.tutorial.start()
}
},
showValidationErrors() {
this.showFormErrorModal = true
},
saveForm() {
if (this.isEdit) {
if(this.isGuest) {
this.saveFormGuest()
} else if (this.isEdit) {
this.saveFormEdit()
} else {
this.saveFormCreate()
@ -230,6 +230,9 @@ export default {
}).finally(() => {
this.updateFormLoading = false
})
},
saveFormGuest() {
this.$emit('openRegister')
}
}
}

View File

@ -266,7 +266,7 @@ export default {
];
},
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()
} else {
this.formFields = clonedeep(this.form.properties).map((field) => {

View File

@ -5,7 +5,7 @@
</div>
<dropdown v-else class="inline" dusk="nav-dropdown">
<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"
xmlns="http://www.w3.org/2000/svg">
<path
@ -34,6 +34,26 @@
</svg>
View form
</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="#"
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}"
@ -47,10 +67,21 @@
</svg>
Duplicate form
</a>
<a href="#" v-if="user.template_editor"
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>
<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)"
@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">
@ -60,23 +91,36 @@
</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>
<!-- 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"/>
</div>
</template>
<script>
import axios from 'axios'
import {mapGetters, mapState} from 'vuex'
@ -87,12 +131,14 @@ export default {
name: 'ExtraMenu',
components: { Dropdown, CreateTemplateModal },
props: {
form: { type: Object, required: true }
form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
},
data: () => ({
loadingDuplicate: false,
loadingDelete: false,
showDeleteFormModal: false,
showCreateTemplateModal: false
}),
@ -104,6 +150,14 @@ export default {
},
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() {
if (this.loadingDuplicate) return
this.loadingDuplicate = true
@ -127,4 +181,3 @@ export default {
}
}
</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>
<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">
@ -11,35 +9,7 @@
</h2>
<small>Welcome back! Please enter your details.</small>
<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? <router-link :to="{name:'register'}" class="font-semibold ml-1">Sign Up</router-link>
</p>
</form>
<login-form />
</div>
</div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 ">
@ -86,17 +56,15 @@
</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'
import LoginForm from './components/LoginForm'
export default {
components: {
OpenFormFooter,
Testimonials,
ForgotPasswordModal
LoginForm
},
middleware: 'guest',
@ -106,42 +74,11 @@ export default {
},
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 () {
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

@ -1,56 +1,20 @@
<template>
<div>
<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">
<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 lg:w-1/2 md:p-6">
<div class="border rounded-md p-6 shadow-md sticky top-4">
<h2 class="font-semibold text-2xl">
Create an account
</h2>
<small>Sign up in less than 2 minutes.</small>
<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? <router-link :to="{name:'login'}" class="font-semibold ml-1">Log In</router-link>
</p>
<!-- GitHub Register Button -->
<login-with-github />
</form>
<register-form />
</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">
Create beautiful Notion forms and share them anywhere
Create beautiful forms and share them anywhere
</h1>
<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.
@ -81,9 +45,9 @@
Unlimited submissions
</p>
</div>
<div class="mt-3 p-6">
<testimonials />
</div>
<!-- <div class="mt-3 p-6">-->
<!-- <testimonials />-->
<!-- </div>-->
</div>
</div>
</div>
@ -92,19 +56,15 @@
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
import SelectInput from '../../components/forms/SelectInput'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import { initCrisp } from '../../middleware/check-auth'
import Testimonials from '../../components/pages/welcome/Testimonials'
import RegisterForm from './components/RegisterForm'
export default {
components: {
Testimonials,
SelectInput,
LoginWithGithub,
OpenFormFooter
OpenFormFooter,
RegisterForm
},
middleware: 'guest',
@ -114,62 +74,13 @@ export default {
},
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 home.
this.$router.push({ name: 'forms.create' })
}
}
}
}
</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,18 +21,21 @@
import store from '~/store'
import Form from 'vform'
import {mapState, mapActions} from 'vuex'
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')
store.commit('open/templates/stopLoading')
})
}
export default {
name: 'CreateForm',
mixins: [initForm],
components: {
FormEditor,
},
@ -41,7 +44,7 @@ export default {
return {title: 'Create a new Form'}
},
beforeRouteEnter (to, from, next) {
beforeRouteEnter(to, from, next) {
loadTemplates()
next()
},
@ -75,12 +78,6 @@ export default {
workspace() {
return this.$store.getters['open/workspaces/getCurrent']()
},
fromOnboarding() {
return this.$route.params.from_onboarding
},
fbGroupLink() {
return window.config.links.facebook_group
}
},
watch: {
@ -95,15 +92,12 @@ export default {
},
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.loadWorkspaces()
@ -122,50 +116,6 @@ export default {
...mapActions({
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
*/

View File

@ -1,71 +1,89 @@
<template>
<div class="flex flex-col min-h-full mt-6">
<div class="w-full flex-grow md:w-3/5 lg:w-1/2 md:mx-auto md:max-w-2xl px-4">
<div>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
Your Forms
</h2>
<v-button v-track.create_form_click class="mt-4 sm:mt-0" :to="{name:'forms.create'}" @click="showCreateFormModal=true">
Create a new form
</v-button>
<div class="bg-white">
<div class="flex bg-gray-50 pb-5">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
Your Forms
</h2>
<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
</v-button>
</div>
<small class="flex text-gray-500">Manage your forms and submissions.</small>
</div>
<p v-if="!formsLoading && enrichedForms.length === 0 && !isFilteringForms">
You don't have any form yet.
</p>
<div v-else-if="forms.length > 0" class="mb-10">
<text-input v-if="forms.length > 5" class="mb-6" :form="searchForm" name="search" label="Search a form"
placeholder="Name of form to search"
</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">
<text-input v-if="forms.length > 0" class="mb-6" :form="searchForm" name="search" label="Search a form"
placeholder="Name of form to search"
/>
<div v-if="allTags.length > 0" class="mb-6">
<div v-for="tag in allTags" :key="tag"
:class="['text-white p-2 text-xs inline rounded-lg font-semibold cursor-pointer mr-2',{'bg-gray-500 dark:bg-gray-400':selectedTags.includes(tag), 'bg-gray-300 dark:bg-gray-700':!selectedTags.includes(tag)}]"
title="Click for filter by tag(s)"
@click="onTagClick(tag)"
:class="['text-white p-2 text-xs inline rounded-lg font-semibold cursor-pointer mr-2',{'bg-gray-500 dark:bg-gray-400':selectedTags.includes(tag), 'bg-gray-300 dark:bg-gray-700':!selectedTags.includes(tag)}]"
title="Click for filter by tag(s)"
@click="onTagClick(tag)"
>
{{ tag }}
</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-for="(form, index) in enrichedForms" :key="form.id"
class="p-4 w-full mx-auto border-gray-300 hover:bg-blue-100 dark:hover:bg-blue-900 transition-colors cursor-pointer relative"
:class="{'border-t':index!==0, 'bg-gray-50 dark:bg-gray-400':form.visibility=='draft'}"
>
<div class="items-center space-x-4 truncate">
<p class="truncate float-left">
{{ form.title }} <span v-if="form.submissions_count" class="text-gray-400 ml-1">- {{
form.submissions_count
}} Submission{{ form.submissions_count > 0 ? 's' : '' }}</span>
</p>
<div v-if="form.tags && form.tags.length > 0" class="float-right hidden sm:block">
<template v-for="(tag,i) in form.tags">
<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"
>
{{ tag }}
</div>
<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"
>
{{ form.tags.length-1 }} more
</div>
</template>
<div v-if="!formsLoading && enrichedForms.length === 0" class="flex flex-wrap justify-center max-w-4xl">
<img loading="lazy" class="w-56"
:src="asset('img/pages/forms/search_notfound.png')" alt="search-not-found">
<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="flex-grow items-center truncate cursor-pointer" role="button" @click.prevent="viewForm(form)">
<span class="font-semibold text-gray-900 dark:text-white">{{ form.title }}</span>
<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>
<div v-if="form.tags && form.tags.length > 0" class="mt-1">
<template v-for="(tag,i) in form.tags">
<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"
>
{{ tag }}
</div>
<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"
>
{{ form.tags.length-1 }} more
</div>
</template>
</div>
</div>
<extra-menu :form="form" :isMainPage="true" />
</div>
<router-link class="absolute inset-0"
:to="{params:{slug:form.slug},name:'forms.show'}"
/>
</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 v-if="formsLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
<div v-if="formsLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</div>
</div>
@ -80,6 +98,7 @@ import Fuse from 'fuse.js'
import Form from 'vform'
import TextInput from '../components/forms/TextInput'
import OpenFormFooter from '../components/pages/OpenFormFooter'
import ExtraMenu from '../components/pages/forms/show/ExtraMenu'
const loadForms = function () {
store.commit('open/forms/startLoading')
@ -89,7 +108,7 @@ const loadForms = function () {
}
export default {
components: { OpenFormFooter, TextInput },
components: { OpenFormFooter, TextInput, ExtraMenu },
beforeRouteEnter (to, from, next) {
loadForms()
@ -127,6 +146,9 @@ export default {
} else {
this.selectedTags.splice(idx, 1)
}
},
viewForm (form) {
this.$router.push({name: 'forms.show', params: {slug: form.slug}})
}
},

View File

@ -1,34 +1,38 @@
<template>
<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="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">
<breadcrumb :path="breadcrumbs" />
<div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
<breadcrumb :path="breadcrumbs"/>
<div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
<p v-else-if="template === null || !template">
Template does not exist.
</p>
<div v-else>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
{{ template.name }}
</h2>
</div>
<p v-else-if="template === null || !template">
Template does not exist.
</p>
<div v-else>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
{{ template.name }}
</h2>
<div class="mb-10">
<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 class="mt-5 text-center">
<v-button class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}">
Use this template
</v-button>
</div>
<div class="mb-10">
<img :src="template.image_url" alt="" class="w-full shadow-xl rounded-lg my-5"/>
<div v-html="template.description"></div>
<div class="mt-5 text-center">
<v-button class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}">
Use this template
</v-button>
</div>
<h3 class="text-center text-gray-500">Template Preview</h3>
<open-complete-form ref="open-complete-form" :form="form" :creating="true" class="my-5 p-4 bg-gray-50 rounded-lg"/>
<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="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>
<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">
<h5 class="border-b p-2">{{ ques.question }}</h5>
<div class="p-2" v-html="ques.answer"></div>
@ -37,64 +41,65 @@
</div>
</div>
</div>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import store from '~/store'
import Form from 'vform'
import { mapGetters, mapState } from 'vuex'
import Fuse from 'fuse.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm'
import Breadcrumb from "../../components/common/Breadcrumb";
<script>
import store from '~/store'
import Form from 'vform'
import {mapGetters, mapState} from 'vuex'
import Fuse from 'fuse.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm'
import Breadcrumb from "../../components/common/Breadcrumb";
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
components: {Breadcrumb, OpenFormFooter, OpenCompleteForm },
export default {
components: {Breadcrumb, OpenFormFooter, OpenCompleteForm},
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
beforeRouteEnter(to, from, next) {
loadTemplates()
next()
},
props: {
metaTitle: { type: String, default: 'Templates' },
metaDescription: { type: String, default: 'Public templates for create form quickly!' }
},
props: {
metaTitle: {type: String, default: 'Templates'},
metaDescription: {type: String, default: 'Public templates for create form quickly!'}
},
data () {
return {
data() {
return {}
},
mounted() {
},
methods: {},
computed: {
...mapState({
templatesLoading: state => state['open/templates'].loading
}),
breadcrumbs() {
if (!this.template) {
return [{route: {name: 'templates'}, label: 'Templates'}]
}
return [{route: {name: 'templates'}, label: 'Templates'}, {label: this.template.name}]
},
mounted () {},
methods: {},
computed: {
...mapState({
templatesLoading: state => state['open/templates'].loading
}),
breadcrumbs () {
if (!this.template) {
return [{ route: { name: 'templates' }, label: 'Templates' }]
}
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.template.name }]
},
template () {
return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug)
},
form (){
return new Form(this.template.structure)
}
template() {
return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug)
},
form() {
return new Form(this.template.structure)
}
}
</script>
}
</script>

View File

@ -16,7 +16,7 @@
>it's free</span>.
</h3>
<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
</v-button>
</div>
@ -61,19 +61,19 @@
<more-features />
<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>
<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.
</p>
<testimonials/>
</div>
<!-- <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>-->
<!-- <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.-->
<!-- </p>-->
<!-- <testimonials/>-->
<!-- </div>-->
<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>
<p class="text-gray-300 my-8">No trial. Generous, unlimited free plan.</p>
<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
</v-button>
</div>

View File

@ -8,6 +8,7 @@ export default [
// Forms
{ 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/show',

View File

@ -1,12 +1,15 @@
<?php
it('can create template', function () {
$user = $this->actingAsUser();
$user = $this->createUser([
'email' => 'admin@opnform.com'
]);
$this->actingAsUser($user);
// Create Form
$workspace = $this->createUserWorkspace($user);
$form = $this->makeForm($user, $workspace);
// Create Template
$templateData = [
'name' => 'Demo Template',
@ -22,4 +25,4 @@ it('can create template', function () {
'type' => 'success',
'message' => 'Template created.'
]);
});
});

View File

@ -125,7 +125,7 @@ trait TestHelpers
->withProperties(FormFactory::formatProperties($dbProperties))
->forWorkspace($workspace)
->createdBy($user)
->make($data);
->make($data);
}
public function createForm(User $user, Workspace $workspace, array $data = [])
@ -135,9 +135,9 @@ trait TestHelpers
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()
@ -191,5 +191,5 @@ trait TestHelpers
}
$this->assertGuest();
}
}

View File

@ -25,3 +25,12 @@ it('can parse filenames', function () {
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);
});