Migrate front-end to Nuxt app (#284)

* wip

* Managed to load a page

* Stuck at changing routes

* Fixed the router, and editable div

* WIP

* Fix app loader

* WIP

* Fix check-auth middleware

* Started to refactor input components

* WIP

* Added select input, v-click-outside for vselect

* update vselect & phone input

* Fixed the mixin

* input component updates

* Fix signature input import

* input component updates in vue3

* image input in vue3

* small fixes

* fix useFormInput watcher

* scale input in vue3

* Vue3: migrating from vuex to Pinia (#249)

* Vue3: migrating from vuex to Pinia

* toggle input fixes

* update configureCompat

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>

* support vue3 query builder

* Refactor inpus

* fix: Vue3 Query Builder - Logic Editor (#251)

* support vue3 query builder

* upgrade

* remove local from middleware

* Submission table pagination & migrate chart to vue3 (#254)

* Submission table Pagination in background

* migrate chart to vue3

* Form submissions pagination

* Form submissions

* Fix form starts

* Fix openSelect key issue

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>
Co-authored-by: Julien Nahum <julien@nahum.net>

* Vue 3 better animation (#257)

* vue-3-better-animation

* Working on migration to vueuse/motion

* Form sidebar animations

* Clean code

* Added animations for modal

* Finished implementing better animations

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>

* Work in progress

* Migrating amplitude and crisp plugin/composable

* Started to refactor pages

* WIP

* vue3-scroll-shadow-fixes (#260)

* WIP

* WIP

* WIP

* Figured out auth & middlewares

* WI

* Refactoring stores and templates pages to comp. api

* Finishing the templates pages

* fix collapsible

* Finish reworking most templates pages

* Reworked workspaces store

* Working on home page and modal

* Fix dropdown

* Fix modal

* Fixed form creation

* Fixed most of the form/show pages

* Updated cors dependency

* fix custom domain warning

* NuxtLink migration (#262)

Co-authored-by: Forms Dev <chirag+new@notionforms.io>

* Tiny fixes + start pre-rendering

* migrate-to-nuxt-useappconfig (#263)

* migrate-to-nuxt-useappconfig

* defineAppConfig

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>

* Working on form/show and editor

* Globally import form inputs to fix resolve

* Remove vform - working on form public page

* Remove initform mixin

* Work in progress for form create guess user

* Nuxt Migration notifications (#265)

* Nuxt Migration notifications

* @input to @update:model-value

* change field type fixes

* @update:model-value

* Enable form-block-logic-editor

* vue-confetti migration

* PR request changes

* useAlert in setup

* Migrate to nuxt settings page AND remove axios (#266)

* Settings pages migration

* remove axios and use opnFetch

* Make created form reactive (#267)

* Remove verify pages and axios lib

---------

Co-authored-by: Julien Nahum <julien@nahum.net>

* Fix alert styling + bug fixes and cleaning

* Refactor notifications + add shadow

* Fix vselect issue

* Working on page pre-rendering

* Created NotionPages store

* Added sitemap on nuxt side

* Sitemap done, working on aws amplify

* Adding missing module

* Remove axios and commit backend changes to sitemap

* Fix notifications

* fix guestpage editor (#269)

Co-authored-by: Julien Nahum <julien@nahum.net>

* Remove appconfig in favor of runtimeconfig

* Fixed amplitude bugs, and added staging environment

* Added amplify file

* Change basdirectory amplify

* Fix loading bar position

* Fix custom redirect (#273)

* Dirty form handling - nuxt migration (#272)

* SEO meta nuxt migration (#274)

* SEO meta nuxt migration

* Polish seo metas, add defaults for OG and twitter

---------

Co-authored-by: Julien Nahum <julien@nahum.net>

* migrate to nuxt useClipboard (#268)

* Set middleware on pages (#278)

* Se middleware on pages

* Se middleware on account page

* add robots.txt (#276)

* 404 page migration (#277)

* Templates pages migration (#275)

* NuxtImg Migration (#279)

Co-authored-by: Julien Nahum <julien@nahum.net>

* Update package json

* Fix build script

* Add loglevel param

* Disable page pre-rendering

* Attempt to allow svgs

* Fix SVGs with NuxtImage

* Add .env file at AWS build time

* tRGIGGER deploy

* Fix issue

* ANother attrempt

* Fix typo

* Fix env?

* Attempt to simplify build

* Enable swr caching instead of prerenderign

* Better image compression

* Last attempt at nuxt images efficiency

* Improve image optimization again

* Remove NuxtImg for non asset files

* Restore templates pages cache

* Remove useless images + fix templates show page

* image optimization caching + fix hydratation issue form template page

* URL generation (front&back) + fixed authJWT for SSR

* Fix composable issue

* Fix form share page

* Embeddable form as a nuxt middleware

* Fix URL for embeddable middleware

* Debugging embeddable on amplify

* Add custom domain support

* No follow for non-production env

* Fix sentry nuxt and custom domain redirect

* remove api prefix from routes (#280)

* remove api prefix from routes

* PR changes

---------

Co-authored-by: Julien Nahum <julien@nahum.net>

* nuxt migration -file upload - WIP (#271)

Co-authored-by: Julien Nahum <julien@nahum.net>

* Fix local file upload

* Fix file submissions preview

* API redirect to back-end from nuxt

* API redirect to back-end from nuxt

* Remove old JS app, update deploy script

* Fix tests, added gh action nuxt step

* Updated package-lock.json

* Setup node in GH Nuxt action

* Setup client directory for GH workflow

---------

Co-authored-by: Forms Dev <chirag+new@notionforms.io>
Co-authored-by: Chirag Chhatrala <60499540+chiragchhatrala@users.noreply.github.com>
Co-authored-by: Rishi Raj Jain <rishi18304@iiitd.ac.in>
Co-authored-by: formsdev <136701234+formsdev@users.noreply.github.com>
This commit is contained in:
Julien Nahum
2024-01-15 12:14:47 +01:00
committed by GitHub
parent c01f566ba9
commit 0adce5a2ff
478 changed files with 27676 additions and 34120 deletions

View File

@@ -0,0 +1,106 @@
<template>
<div class="w-full flex flex-col">
<form-editor v-if="!formsLoading || form" ref="editor"
:is-edit="true"
@on-save="formInitialHash=null"
/>
<div v-else-if="!formsLoading && error" class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500">
{{ error }}
</div>
<div v-else class="text-center mt-4 py-6">
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import Breadcrumb from '~/components/global/Breadcrumb.vue'
import FormEditor from "~/components/open/forms/components/FormEditor.vue";
import {hash} from "~/lib/utils.js";
export default {
name: 'EditForm',
components: { Breadcrumb, FormEditor },
beforeRouteLeave (to, from, next) {
if (this.isDirty()) {
return useAlert().confirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null
next()
}, () => {})
}
next()
},
setup () {
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
if (!formsStore.allLoaded) {
formsStore.startLoading()
}
const updatedForm = storeToRefs(workingFormStore).content
const form = computed(() => formsStore.getByKey(useRoute().params.slug))
// Create a form.id watcher that updates working form
watch(form, (form) => {
if (form) {
updatedForm.value = useForm(form)
}
})
useOpnSeoMeta({
title: 'Edit ' + ((form && form.value) ? form.value.title : 'Your Form')
})
definePageMeta({
middleware: "auth"
})
return {
formsStore,
workingFormStore,
workspacesStore,
updatedForm,
form,
formsLoading: computed(() => formsStore.loading),
}
},
data () {
return {
error: null,
formInitialHash: null
}
},
computed: {
},
async beforeMount() {
window.onbeforeunload = () => {
if (this.isDirty()) {
return false
}
}
if (!this.form && !this.formsStore.allLoaded) {
await this.formsStore.loadAll(this.workspacesStore.currentId)
}
this.updatedForm = useForm(this.form)
this.formInitialHash = hash(JSON.stringify(this.updatedForm.data()))
if (this.updatedForm && (!this.updatedForm.notification_settings || Array.isArray(this.updatedForm.notification_settings))) {
this.updatedForm.notification_settings = {}
}
},
methods: {
isDirty () {
return this.formInitialHash && this.formInitialHash !== hash(JSON.stringify(this.updatedForm.data()))
}
}
}
</script>

View File

@@ -0,0 +1,148 @@
<template>
<div class="flex flex-col">
<div v-if="form && !isIframe && (form.logo_picture || form.cover_picture)">
<div v-if="form.cover_picture">
<div id="cover-picture" class="max-h-56 w-full overflow-hidden flex items-center justify-center">
<img alt="Form Cover Picture" :src="form.cover_picture" class="w-full"/>
</div>
</div>
<div v-if="form.logo_picture" class="w-full p-5 relative mx-auto"
:class="{'pt-20':!form.cover_picture, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form.width === 'centered', 'max-w-7xl': (form.width === 'full' && !isIframe) }"
>
<img alt="Logo Picture" :src="form.logo_picture"
:class="{'top-5':!form.cover_picture, '-top-10':form.cover_picture}"
class="w-20 h-20 object-contain absolute left-5 transition-all"
/>
</div>
</div>
<div class="w-full mx-auto px-4"
:class="{'mt-6':!isIframe, 'md:w-3/5 lg:w-1/2 md:max-w-2xl': form && (form.width === 'centered'), 'max-w-7xl': (form && form.width === 'full' && !isIframe)}"
>
<div v-if="!formLoading && !form">
<h1 class="mt-6" v-text="'Whoops'"/>
<p class="mt-6">
Unfortunately we could not find this form. It may have been deleted by it's author.
</p>
<p class="mb-10 mt-4">
<router-link :to="{name:'index'}">
Create your form for free with OpnForm
</router-link>
</p>
</div>
<div v-else-if="formLoading">
<p class="text-center mt-6 p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</p>
</div>
<template v-else>
<div v-if="recordLoading">
<p class="text-center mt-6 p-4">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</p>
</div>
<open-complete-form v-show="!recordLoading" ref="open-complete-form" :form="form" class="mb-10"
@password-entered="passwordEntered"
/>
</template>
</div>
</div>
</template>
<script setup>
import {computed} from 'vue'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm.vue'
import sha256 from 'js-sha256'
import {onBeforeRouteLeave} from 'vue-router'
import {disableDarkMode, handleDarkMode, handleTransparentMode, focusOnFirstFormElement} from '~/lib/forms/public-page'
const crisp = useCrisp()
const formsStore = useFormsStore()
const recordsStore = useRecordsStore()
const isIframe = useIsIframe()
const formLoading = computed(() => formsStore.loading)
const recordLoading = computed(() => recordsStore.loading)
const slug = useRoute().params.slug
const form = computed(() => formsStore.getByKey(slug))
const submitted = ref(false)
crisp.hideChat()
onBeforeRouteLeave((to, from) => {
crisp.showChat()
disableDarkMode()
})
const passwordEntered = function (password) {
useCookie('password-' + slug, {
maxAge: {expires: 60 * 60 * 7},
sameSite: false,
secure: true
}).value = sha256(password)
loadForm(slug).then(() => {
if (form.value?.is_password_protected) {
this.$refs['open-complete-form'].addPasswordError('Invalid password.')
}
})
}
const loadForm = async () => {
if (formsStore.loading || form.value) return Promise.resolve()
const {data, error} = await formsStore.publicLoad(slug)
if (error.value) {
formsStore.stopLoading()
return
}
formsStore.save(data.value)
formsStore.stopLoading()
// Adapt page to form: colors, custom code etc
handleDarkMode(form.value)
handleTransparentMode(form.value)
if (process.server) return
if (form.value.custom_code) {
const scriptEl = document.createRange().createContextualFragment(form.value.custom_code)
document.head.append(scriptEl)
}
if (!isIframe) focusOnFirstFormElement()
}
onMounted(() => {
loadForm(slug)
})
await loadForm(slug)
useOpnSeoMeta({
title: () => {
if (form && form.value.is_pro && form.value.seo_meta.page_title) {
return form.value.seo_meta.page_title
}
return form.value ? form.value.title : 'Create beautiful forms'
},
description () {
if (form && form.value.is_pro && form.value.seo_meta.page_description) {
return form.value.seo_meta.page_description
}
return (form && form.value.description) ? form.value.description.substring(0, 160) : null
},
ogImage () {
if (form && form.value.is_pro && form.value.seo_meta.page_thumbnail) {
return form.value.seo_meta.page_thumbnail
}
return (form && form.value.cover_picture) ? form.value.cover_picture : null
},
robots: () => {
return (form && form.value.can_be_indexed) ? null : 'noindex, nofollow'
}
})
useHead({
titleTemplate: (titleChunk) => {
if (form && form.value?.is_pro && form.value?.seo_meta.page_title) {
// Disable template if custom SEO title
return titleChunk
}
return titleChunk ? `${titleChunk} - OpnForm` : 'OpnForm';
}
})
</script>

View File

@@ -0,0 +1,199 @@
<template>
<div class="bg-white">
<template v-if="form">
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<a href="#" class="flex text-blue mb-2 font-semibold text-sm" @click.prevent="goBack">
<svg class="w-3 h-3 text-blue mt-1 mr-1" viewBox="0 0 6 10" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Go back
</a>
<div class="flex flex-wrap">
<h2 class="flex-grow text-gray-900 truncate">
{{ form.title }}
</h2>
<div class="flex">
<extra-menu :form="form"/>
<v-button v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" target="_blank"
:href="form.share_url" color="white"
class="mr-2 text-blue-600 hidden sm:block"
>
<svg class="w-6 h-6 inline -mt-1" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
<path
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
</v-button>
<v-button class="text-white" :to="{name: 'forms-slug-edit', params: {slug: slug}}">
<svg class="inline mr-1 -mt-1" width="18" height="17" viewBox="0 0 18 17" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.99998 15.6662H16.5M1.5 15.6662H2.89545C3.3031 15.6662 3.50693 15.6662 3.69874 15.6202C3.8688 15.5793 4.03138 15.512 4.1805 15.4206C4.34869 15.3175 4.49282 15.1734 4.78107 14.8852L15.25 4.4162C15.9404 3.72585 15.9404 2.60656 15.25 1.9162C14.5597 1.22585 13.4404 1.22585 12.75 1.9162L2.28105 12.3852C1.9928 12.6734 1.84867 12.8175 1.7456 12.9857C1.65422 13.1348 1.58688 13.2974 1.54605 13.4675C1.5 13.6593 1.5 13.8631 1.5 14.2708V15.6662Z"
stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
Edit form
</v-button>
</div>
</div>
<p class="text-gray-500 text-sm">
<span class="pr-1">{{ form.views_count }} view{{ form.views_count > 0 ? 's' : '' }}</span>
<span class="pr-1">- {{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? 's' : '' }}
</span>
<span>- Edited {{ form.last_edited_human }}</span>
</p>
<div v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)"
class="mt-2 flex items-center flex-wrap gap-3"
>
<span v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
Draft - not publicly accessible
</span>
<span v-else-if="form.visibility=='closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
Closed - won't accept new submissions
</span>
<span v-for="(tag,i) in form.tags" :key="tag"
class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
{{ tag }}
</span>
</div>
<p v-if="form.closes_at" class="text-yellow-500">
<span v-if="form.is_closed"> This form stopped accepting submissions on the {{
displayClosesDate
}} </span>
<span v-else> This form will stop accepting submissions on the {{ displayClosesDate }} </span>
</p>
<p v-if="form.max_submissions_count > 0" class="text-yellow-500">
<span v-if="form.max_number_of_submissions_reached"> The form is now closed because it reached its limit of {{
form.max_submissions_count
}} submissions. </span>
<span v-else> This form will stop accepting submissions after {{ form.max_submissions_count }} submissions. </span>
</p>
<form-cleanings class="mt-4" :form="form"/>
<div class="border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
<nuxt-link :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</nuxt-link>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="py-4">
<NuxtPage :form="form"/>
</div>
</div>
</div>
</template>
<div v-else-if="loading" class="text-center w-full p-5">
<Loader class="h-6 w-6 mx-auto"/>
</div>
<div v-else class="text-center w-full p-5">
Form not found.
</div>
</div>
</template>
<script setup>
import {computed} from 'vue'
import ProTag from '~/components/global/ProTag.vue'
import VButton from '~/components/global/VButton.vue'
import ExtraMenu from '../../../components/pages/forms/show/ExtraMenu.vue'
import FormCleanings from '../../../components/pages/forms/show/FormCleanings.vue'
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: 'Home'
})
const authStore = useAuthStore()
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
const slug = useRoute().params.slug
const user = computed(() => authStore.user)
const form = computed(() => formsStore.getByKey(slug))
const workspace = computed(() => workspacesStore.getByKey(form?.value?.workspace_id))
const loading = computed(() => formsStore.loading || workspacesStore.loading)
const displayClosesDate = computed(() => {
if (form.value && form.value.closes_at) {
const dateObj = new Date(form.value.closes_at)
return dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0') + ' ' +
String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
}
return ''
})
const tabsList = [
{
name: 'Submissions',
route: 'forms-slug-show-submissions'
},
{
name: 'Analytics',
route: 'forms-slug-show-stats'
},
{
name: 'Share',
route: 'forms-slug-show-share'
}
]
onMounted(() => {
workingFormStore.set(null) // Reset old working form
if (form.value) {
workingFormStore.set(form.value)
} else {
formsStore.loadAll(useWorkspacesStore().currentId)
}
})
watch(() => form?.value?.id, (id) => {
if (id) {
workingFormStore.set(form)
}
})
const goBack = () => {
useRouter().push({name: 'home'})
}
</script>

View File

@@ -0,0 +1,7 @@
<script setup>
definePageMeta({
redirect: to => {
return { name: 'forms-slug-show-submissions'}
}
})
</script>

View File

@@ -0,0 +1,55 @@
<template>
<div class="mb-20">
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
<regenerate-form-link class="sm:w-1/2 flex" :form="props.form"/>
<url-form-prefill class="sm:w-1/2" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<embed-form-as-popup-modal class="sm:w-1/2 flex" :form="props.form"/>
</div>
<share-link class="mt-4" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<embed-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<form-qr-code class="mt-6" :form="props.form" :extra-query-param="shareUrlForQueryParams"/>
<advanced-form-url-settings :form="props.form" v-model="shareFormConfig"/>
</div>
</template>
<script setup>
import ShareLink from '../../../../components/pages/forms/show/ShareLink.vue'
import EmbedCode from '../../../../components/pages/forms/show/EmbedCode.vue'
import FormQrCode from '../../../../components/pages/forms/show/FormQrCode.vue'
import UrlFormPrefill from '../../../../components/pages/forms/show/UrlFormPrefill.vue'
import RegenerateFormLink from '../../../../components/pages/forms/show/RegenerateFormLink.vue'
import AdvancedFormUrlSettings from '../../../../components/open/forms/components/AdvancedFormUrlSettings.vue'
import EmbedFormAsPopupModal from '../../../../components/pages/forms/show/EmbedFormAsPopupModal.vue'
const props = defineProps({form: {type: Object, required: true}})
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: (props.form) ? 'Share Form - ' + props.form.title : 'Share Form'
})
const shareFormConfig = ref({
hide_title: false,
auto_submit: false
})
const shareUrlForQueryParams = computed(() => {
let queryStr = ''
for (const [key, value] of Object.entries(shareFormConfig.value)) {
if (value && value !== 'false' && value !== false) {
queryStr += '&' + encodeURIComponent(key) + "=" + encodeURIComponent(value)
}
}
return queryStr.slice(1)
})
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div>
<h3 class="font-semibold mt-4 text-xl">
Form Analytics (last 30 days)
</h3>
<form-stats :form="form"/>
</div>
</template>
<script>
import FormStats from '../../../../components/open/forms/components/FormStats.vue'
export default {
components: {FormStats},
props: {
form: {type: Object, required: true},
},
setup (props) {
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: (props.form) ? 'Form Analytics - '+props.form.title : 'Form Analytics'
})
},
computed: {
}
}
</script>

View File

@@ -0,0 +1,21 @@
<template>
<div id="table-page">
<form-submissions/>
</div>
</template>
<script setup>
import FormSubmissions from '../../../../components/open/forms/components/FormSubmissions.vue'
const props = {
form: {type: Object, required: true}
}
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: (props.form) ? 'Form Submissions - ' + props.form.title : 'Form Submissions'
})
</script>

View File

@@ -0,0 +1,105 @@
<template>
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"
/>
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:error="error"
:is-guest="isGuest"
@openRegister="registerModal=true"
/>
<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 :show-register-modal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
@afterLogin="afterLogin"
/>
</div>
</template>
<script setup>
import FormEditor from "~/components/open/forms/components/FormEditor.vue"
import QuickRegister from '~/components/pages/auth/components/QuickRegister.vue'
import CreateFormBaseModal from '../../../components/pages/forms/create/CreateFormBaseModal.vue'
import {initForm} from "~/composables/forms/initForm.js"
import {fetchTemplate} from "~/stores/templates.js"
import {fetchAllWorkspaces} from "~/stores/workspaces.js";
const templatesStore = useTemplatesStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
const route = useRoute()
// Fetch the template
if (route.query.template !== undefined && route.query.template) {
const {data} = await fetchTemplate(route.query.template)
templatesStore.save(data.value)
}
// Store values
const workspace = computed(() => workspacesStore.getCurrent)
const workspacesLoading = computed(() => workspacesStore.loading)
const form = storeToRefs(workingFormStore).content
useOpnSeoMeta({
title: 'Create a new Form for free',
})
definePageMeta({
middleware: "guest"
})
// Data
const stateReady = ref(false)
const loading = ref(false)
const error = ref('')
const registerModal = ref(false)
const isGuest = ref(true)
const showInitialFormModal = ref(false)
// Component ref
const editor = ref(null)
onMounted(() => {
// Set as guest user
workspacesStore.set([{
id: null,
name: 'Guest Workspace',
is_enterprise: false,
is_pro: false
}])
form.value = initForm()
if (route.query.template !== undefined && route.query.template) {
const template = templatesStore.getByKey(route.query.template)
if (template && template.structure) {
form.value = useForm({...form.value.data(), ...template.structure})
}
} else {
// No template loaded, ask how to start
showInitialFormModal.value = true
}
stateReady.value = true
})
const afterLogin = () => {
registerModal.value = false
isGuest.value = false
fetchAllWorkspaces()
setTimeout(() => {
if (editor) {
editor.value.saveFormCreate()
}
}, 500)
}
const formGenerated = (newForm) => {
form.value = useForm({...form.value.data(), ...newForm})
}
</script>

View File

@@ -0,0 +1,117 @@
<template>
<div class="flex flex-wrap flex-col">
<transition name="fade" mode="out-in">
<div key="2">
<create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"
/>
<form-editor v-if="form && !workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:error="error"
@on-save="formInitialHash=null"
/>
<div v-else class="text-center mt-4 py-6">
<Loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import {watch} from 'vue'
import {initForm} from "~/composables/forms/initForm.js"
import FormEditor from "~/components/open/forms/components/FormEditor.vue"
import CreateFormBaseModal from '../../../components/pages/forms/create/CreateFormBaseModal.vue'
import {fetchTemplate} from "~/stores/templates.js"
import {hash} from "~/lib/utils.js"
import {onBeforeRouteLeave} from 'vue-router'
definePageMeta({
middleware: "auth"
})
useOpnSeoMeta({
title: 'Create a new Form'
})
onBeforeRouteLeave((to, from, next) => {
if (isDirty()) {
return useAlert().confirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null
next()
}, () => {})
}
next()
})
const route = useRoute()
const authStore = useAuthStore()
const templatesStore = useTemplatesStore()
const workingFormStore = useWorkingFormStore()
const workspacesStore = useWorkspacesStore()
const formStore = useFormsStore()
// Fetch the template
if (route.query.template !== undefined && route.query.template) {
const {data} = await fetchTemplate(route.query.template)
templatesStore.save(data.value)
}
const {
getCurrent: workspace,
getAll: workspaces,
workspacesLoading: workspacesLoading
} = storeToRefs(workspacesStore)
const {content: form} = storeToRefs(workingFormStore)
// State
const loading = ref(false)
const error = ref('')
const showInitialFormModal = ref(false)
const formInitialHash = ref(null)
watch(() => workspace, () => {
if (workspace) {
form.workspace_id = workspace.value.id
}
})
onMounted(() => {
if (process.client) {
window.onbeforeunload = () => {
if (isDirty()) {
return false
}
}
}
if (!formStore.allLoaded) {
formStore.loadAll(workspace.value.id)
}
form.value = initForm({workspace_id: workspace.value?.id})
formInitialHash.value = hash(JSON.stringify(form.value.data()))
if (route.query.template !== undefined && route.query.template) {
const template = templatesStore.getByKey(route.query.template)
if (template && template.structure) {
form.value = useForm({...form.value.data(), ...template.structure})
}
} else {
// No template loaded, ask how to start
showInitialFormModal.value = true
}
// workspacesStore.loadIfEmpty()
})
// Methods
const formGenerated = (newForm) => {
form.value = useForm({...form.value.data(), ...newForm})
}
const isDirty = () => {
return !loading.value && formInitialHash.value && formInitialHash.value !== hash(JSON.stringify(form.value.data()))
}
</script>