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

View File

@@ -0,0 +1,188 @@
<template>
<div>
<v-button
v-track.share_embed_form_popup_click="{form_id:form.id, form_slug:form.slug}"
class="w-full"
color="light-gray"
@click="showEmbedFormAsPopupModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2 text-blue-600 inline" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.068.157 2.148.279 3.238.364.466.037.893.281 1.153.671L12 21l2.652-3.978c.26-.39.687-.634 1.153-.67 1.09-.086 2.17-.208 3.238-.365 1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
/>
</svg>
Embed as popup
</v-button>
<modal :show="showEmbedFormAsPopupModal" @close="onClose">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.068.157 2.148.279 3.238.364.466.037.893.281 1.153.671L12 21l2.652-3.978c.26-.39.687-.634 1.153-.67 1.09-.086 2.17-.208 3.238-.365 1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
/>
</svg>
</template>
<template #title>
<span>Add the popup to your website</span>
</template>
<div class="p-4">
<h3 class="border-t text-xl font-semibold mb-2 pt-6">
Demo
</h3>
<p class="pb-6">
A live preview of your form popup was just added to this page. <span class="font-semibold text-blue-800">Click on the button on the bottom
{{ advancedOptions.position }} corner to try it</span>.
</p>
<h3 class="border-t text-xl font-semibold mb-2 pt-6">
How does it work?
</h3>
<p>Paste the following code snippet in the <b>&lt;head&gt;</b> section of your website.</p>
<div
class="border border-nt-blue-light bg-blue-50 dark:bg-notion-dark-light rounded-md p-4 mb-5 w-full mx-auto mt-4 select-all"
>
<div class="flex items-center">
<p class="select-all text-nt-blue flex-grow break-all">
{{ embedPopupCode }}
</p>
<div class="hover:bg-nt-blue-lighter rounded transition-colors cursor-pointer" @click="copyToClipboard">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-nt-blue" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
/>
</svg>
</div>
</div>
</div>
<collapse class="py-5 w-full border rounded-md px-4" :model-value="true">
<template #title>
<div class="flex">
<h3 class="font-semibold block text-lg">
Advanced options
</h3>
</div>
</template>
<div class="border-t mt-4 -mx-4" />
<toggle-switch-input v-model="advancedOptions.hide_title" name="hide_title" class="mt-4"
label="Hide Form Title"
:disabled="(form.hide_title===true)?true:null"
:help="hideTitleHelp"
/>
<color-input v-model="advancedOptions.bgcolor" name="bgcolor" class="mt-4"
label="Circle Background Color"
/>
<text-input v-model="advancedOptions.emoji" name="emoji" class="mt-4"
label="Emoji" :max-char-limit="2"
/>
<flat-select-input v-model="advancedOptions.position" name="position" class="mt-4"
label="Position"
:options="[
{name:'Bottom Right',value:'right'},
{name:'Bottom Left',value:'left'},
]"
/>
<text-input v-model="advancedOptions.width" name="width" class="mt-4"
label="Form pop max width (px)" native-type="number"
/>
</collapse>
<div class="flex justify-end mt-4">
<v-button color="gray" shade="light" @click="onClose">
Close
</v-button>
</div>
</div>
</modal>
</div>
</template>
<script setup>
import { ref, defineProps, computed } from 'vue'
import {appUrl} from "~/lib/utils.js";
const { copy } = useClipboard()
const crisp = useCrisp()
const props = defineProps({
form: { type: Object, required: true }
})
const embedScriptUrl = '/widgets/embed-min.js'
let showEmbedFormAsPopupModal = ref(false)
let advancedOptions = ref({
hide_title: false,
emoji: '💬',
position: 'right',
bgcolor: '#3B82F6',
width: '500'
})
let hideTitleHelp = computed(() => {
return props.form.hide_title ? 'This option is disabled because the form title is already hidden' : null
})
let shareUrl = computed(() => {
return (advancedOptions.value.hide_title) ? props.form.share_url + '?hide_title=true' : props.form.share_url
})
let embedPopupCode = computed(() => {
const nfData = {
formurl: shareUrl.value,
emoji: advancedOptions.value.emoji,
position: advancedOptions.value.position,
bgcolor: advancedOptions.value.bgcolor,
width: advancedOptions.value.width
}
previewPopup(nfData)
return '<script async data-nf=\'' + JSON.stringify(nfData) + '\' src=\'' + appUrl(embedScriptUrl) + '\'></scrip' + 't>'
})
onMounted(() => {
advancedOptions.value.bgcolor = props.form.color
})
const onClose = () => {
removePreview()
crisp.showChat()
showEmbedFormAsPopupModal.value = false
}
const copyToClipboard = () => {
if (process.server) return
copy(embedPopupCode.value)
useAlert().success('Copied!')
}
const removePreview = () => {
if (process.server) return
const oldP = document.head.querySelector('#nf-popup-preview')
if (oldP) {
oldP.remove()
}
const oldM = document.body.querySelector('.nf-main')
if (oldM) {
oldM.remove()
}
}
const previewPopup = (nfData) => {
if (process.server) return
if (!showEmbedFormAsPopupModal.value) {
return
}
// Remove old preview, if there
removePreview()
// Hide crisp
crisp.hideChat()
// Add new preview
const el = document.createElement('script')
el.id = 'nf-popup-preview'
el.async = true
el.src = embedScriptUrl
el.setAttribute('data-nf', JSON.stringify(nfData))
document.head.appendChild(el)
}
</script>

View File

@@ -0,0 +1,188 @@
<template>
<div>
<div v-if="loadingDuplicate || loadingDelete" class="pr-4 pt-2">
<Loader class="h-6 w-6 mx-auto" />
</div>
<dropdown v-else class="inline">
<template #trigger="{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
d="M8.00016 2.83366C8.4604 2.83366 8.8335 2.46056 8.8335 2.00033C8.8335 1.54009 8.4604 1.16699 8.00016 1.16699C7.53993 1.16699 7.16683 1.54009 7.16683 2.00033C7.16683 2.46056 7.53993 2.83366 8.00016 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
/>
<path
d="M13.8335 2.83366C14.2937 2.83366 14.6668 2.46056 14.6668 2.00033C14.6668 1.54009 14.2937 1.16699 13.8335 1.16699C13.3733 1.16699 13.0002 1.54009 13.0002 2.00033C13.0002 2.46056 13.3733 2.83366 13.8335 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
/>
<path
d="M2.16683 2.83366C2.62707 2.83366 3.00016 2.46056 3.00016 2.00033C3.00016 1.54009 2.62707 1.16699 2.16683 1.16699C1.70659 1.16699 1.3335 1.54009 1.3335 2.00033C1.3335 2.46056 1.70659 2.83366 2.16683 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
</v-button>
</template>
<a v-if="isMainPage && user" v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" :href="form.share_url"
target="_blank"
class="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"
>
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
<path
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
View form
</a>
<nuxt-link v-if="isMainPage" v-track.edit_form_click="{form_id:form.id, form_slug:form.slug}"
:to="{name:'forms-slug-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"
>
<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
</nuxt-link>
<a v-if="isMainPage" 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"
@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 v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
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"
@click.prevent="duplicateForm"
>
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
/>
</svg>
Duplicate form
</a>
<a v-if="!isMainPage" v-track.create_template_click="{form_id:form.id, form_slug:form.slug}" 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"
@click.prevent="showFormTemplateModal=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 v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
href="#"
class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center"
@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>
<!-- Delete Form Modal -->
<modal :show="showDeleteFormModal" icon-color="red" max-width="sm" @close="showDeleteFormModal=false">
<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>
<form-template-modal v-if="!isMainPage && user" :form="form" :show="showFormTemplateModal" @close="showFormTemplateModal=false" />
</div>
</template>
<script setup>
import { ref, defineProps, computed } from 'vue'
import Dropdown from '~/components/global/Dropdown.vue'
import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue'
const { copy } = useClipboard()
const router = useRouter()
const props = defineProps({
form: { type: Object, required: true },
isMainPage: { type: Boolean, required: false, default: false }
})
const authStore = useAuthStore()
const formsStore = useFormsStore()
const formEndpoint = '/open/forms/{id}'
let user = computed(() => authStore.user)
let loadingDuplicate = ref(false)
let loadingDelete = ref(false)
let showDeleteFormModal = ref(false)
let showFormTemplateModal = ref(false)
const copyLink = () => {
copy(props.form.share_url)
useAlert().success('Copied!')
}
const duplicateForm = () => {
if (loadingDuplicate.value) return
loadingDuplicate.value = true
opnFetch(formEndpoint.replace('{id}', props.form.id) + '/duplicate',{method: 'POST'}).then((data) => {
formsStore.save(data.new_form)
router.push({ name: 'forms-slug-show', params: { slug: data.new_form.slug } })
useAlert().success('Form was successfully duplicated.')
loadingDuplicate.value = false
})
}
const deleteForm = () => {
if (loadingDelete.value) return
loadingDelete.value = true
opnFetch(formEndpoint.replace('{id}', props.form.id),{method:'DELETE'}).then(() => {
formsStore.remove(props.form)
router.push({ name: 'home' })
useAlert().success('Form was deleted.')
loadingDelete.value = false
})
}
</script>

View File

@@ -0,0 +1,107 @@
<template>
<v-transition>
<div v-if="hasCleanings && !hideWarning" class="border border-gray-300 dark:border-gray-600 rounded-md bg-white p-2"
:class="{'hover:bg-yellow-50 dark:hover:bg-yellow-900':!collapseOpened}"
>
<collapse v-model="collapseOpened">
<template #title>
<p class="text-yellow-500 dark:text-yellow-400 font-semibold text-sm p-1 pr-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6 inline"
>
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
Some features that are included in our {{ form.is_pro ? 'Enterprise' : 'Pro' }} plan are disabled when
publicly sharing this form<span v-if="specifyFormOwner"> (only owners of this form can see this)</span>.
</p>
</template>
<div class="border-t mt-1 p-4 pb-2 -mx-2">
<p class="text-gray-500 text-sm" v-html="cleaningContent" />
<p class="text-gray-500 text-sm mb-4 font-semibold">
<NuxtLink :to="{name:'pricing'}">
{{ form.is_pro ? 'Upgrade your OpnForms plan today' : 'Start your free OpnForms trial' }}
</NuxtLink>
to unlock all of our features and build powerful forms.
</p>
<div class="flex flex-wrap items-end w-full">
<div class="flex-grow flex pr-2">
<v-button v-track.upgrade_from_form_cleanings_click size="small" class="inline-block" :to="{name:'pricing'}">
{{ form.is_pro ? 'Upgrade plan' : 'Start free trial' }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 inline -mt-[2px]"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</v-button>
<v-button color="white" size="small" class="ml-2" @click.prevent="openCrisp">
Contact us
</v-button>
</div>
<v-button v-if="hideable" color="white" size="small" class="mt-2" @click.prevent="hideWarning=true">
Hide warning
</v-button>
</div>
</div>
</collapse>
</div>
</v-transition>
</template>
<script>
import Collapse from '~/components/global/Collapse.vue'
import VButton from '~/components/global/VButton.vue'
import VTransition from '~/components/global/transitions/VTransition.vue'
export default {
name: 'FormCleanings',
components: { VTransition, VButton, Collapse },
props: {
form: { type: Object, required: true },
specifyFormOwner: { type: Boolean, default: false },
hideable: { type: Boolean, default: false }
},
data () {
return {
collapseOpened: false,
hideWarning: false
}
},
computed: {
hasCleanings () {
return this.form.cleanings && Object.keys(this.form.cleanings).length > 0
},
cleanings () {
return this.form.cleanings
},
cleaningContent () {
let message = ''
Object.keys(this.cleanings).forEach((key) => {
let fieldName = key.charAt(0).toUpperCase() + key.slice(1)
if (fieldName !== 'Form') {
fieldName = '"' + fieldName + '" field'
}
let fieldInfo = '<span class="font-semibold">' + fieldName + '</span><br/><ul class=\'list-disc list-inside\'>'
this.cleanings[key].forEach((msg) => {
fieldInfo = fieldInfo + '<li>' + msg + '</li>'
})
if (fieldInfo) {
message = message + fieldInfo + '<ul/><br/>'
}
})
return message
}
},
watch: {},
mounted () {
},
methods: {
openCrisp () {
this.$crisp.push(['do', 'chat:show'])
this.$crisp.push(['do', 'chat:open'])
}
}
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<div>
<h3 class="font-semibold text-xl">QR Code</h3>
<p>Scan the QR code to open the form (Right click to copy the image)</p>
<div class="flex items-center">
<img v-if="QrUrl" :src="QrUrl" class="m-auto" />
</div>
</div>
</template>
<script>
import QRCode from 'qrcode'
export default {
name: 'FormQrCode',
props: {
form: { type: Object, required: true },
extraQueryParam: { type: String, default: '' }
},
data () {
return {
QrUrl: null
}
},
computed: {
shareUrl () {
return (this.extraQueryParam) ? this.form.share_url + "?" + this.extraQueryParam : this.form.share_url + this.extraQueryParam
}
},
watch: {
shareUrl () {
this.generateQR()
}
},
mounted () {
this.generateQR()
},
methods: {
generateQR () {
QRCode.toDataURL(this.shareUrl).then(url => {
this.QrUrl = url
})
}
}
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<div>
<v-button
class="w-full"
color="light-gray"
v-track.regenerate_form_link_click="{form_id:form.id, form_slug:form.slug}"
@click="showGenerateFormLinkModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2 text-blue-600 inline" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
Regenerate link
</v-button>
<!-- Regenerate form link modal -->
<modal :show="showGenerateFormLinkModal" @close="showGenerateFormLinkModal=false">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue-600" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</template>
<template #title>
Generate new form link
</template>
<div class="p-4">
<p>
You can choose between two different URL formats for your form.
<span class="font-semibold">Be careful, changing your form URL is not a reversible operation</span>.
Make sure to udpate your form URL everywhere where it's used.
</p>
<div class="border-t py-4 mt-4">
<h3 class="text-xl text-gray-700 font-semibold">
Human Readable URL
</h3>
<p>If your users are going to see this url, you might want to make nice and readable. Example:</p>
<p class="text-gray-600 border p-4 bg-gray-50 rounded-md mt-4">
https://opnform.com/forms/contact
</p>
<div class="text-center mt-4">
<v-button :loading="loadingNewLink" color="outline-blue" @click="regenerateLink('slug')">
Generate a Human Readable URL
</v-button>
</div>
</div>
<div class="border-t pt-4 mt-4">
<h3 class="text-xl text-gray-700 font-semibold">
Random ID URL
</h3>
<p>
If your user are not going to see your form url (if it's embedded), and if you prefer to have a random
non-guessable URL. Example:
</p>
<p class="text-gray-600 p-4 border bg-gray-50 rounded-md mt-4">
https://opnform.com/forms/b4417f9c-34ae-4421-8006-832ee47786e7
</p>
<div class="text-center mt-4">
<v-button :loading="loadingNewLink" color="outline-blue" @click="regenerateLink('uuid')">
Generate a Random ID URL
</v-button>
</div>
</div>
</div>
</modal>
</div>
</template>
<script>
import { computed } from 'vue'
import { useFormsStore } from '../../../../stores/forms'
export default {
name: 'RegenerateFormLink',
components: {},
props: {
form: { type: Object, required: true }
},
setup () {
const formsStore = useFormsStore()
return {
formsStore
}
},
data: () => ({
loadingNewLink: false,
showGenerateFormLinkModal: false,
}),
computed: {
formEndpoint: () => '/open/forms/{id}',
},
methods: {
regenerateLink(option) {
if (this.loadingNewLink) return
this.loadingNewLink = true
opnFetch(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option, {method:'PUT'}).then((data) => {
this.formsStore.addOrUpdate(data.form)
this.$router.push({name: 'forms-slug-show-share', params: {slug: data.form.slug}})
useAlert().success(data.message)
this.loadingNewLink = false
}).finally(() => {
this.showGenerateFormLinkModal = false
})
},
}
}
</script>

View File

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

View File

@@ -0,0 +1,99 @@
<template>
<div class="flex">
<v-button
class="w-full"
color="light-gray"
v-track.url_form_prefill_click="{form_id:form.id, form_slug:form.slug}"
@click="showUrlFormPrefillModal=true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2 text-blue-600 inline" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-7a2 2 0 012-2h2m3-4H9a2 2 0 00-2 2v7a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-1m-1 4l-3 3m0 0l-3-3m3 3V3"
/>
</svg>
Url pre-fill
</v-button>
<modal :show="showUrlFormPrefillModal" @close="showUrlFormPrefillModal=false">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-7a2 2 0 012-2h2m3-4H9a2 2 0 00-2 2v7a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-1m-1 4l-3 3m0 0l-3-3m3 3V3"
/>
</svg>
</template>
<template #title>
<span>Url Form Prefill</span>
</template>
<div class="p-4" ref="content">
<p>
Create dynamic links when sharing your form (whether it's embedded or not), that allows you to prefill
your form fields. You can use this to personalize the form when sending it to multiple contacts for instance.
</p>
<h3 class="mt-6 border-t text-xl font-semibold mb-4 pt-6">
How does it work?
</h3>
<p>
Complete your form below and fill only the fields you want to prefill. You can even leave the required fields empty.
</p>
<div class="rounded-lg p-5 bg-gray-100 dark:bg-gray-900 mt-4">
<open-form v-if="form" :theme="theme" :loading="false" :show-hidden="true" :form="form" :fields="form.properties" @submit="generateUrl">
<template #submit-btn="{submitForm}">
<v-button class="mt-2 px-8 mx-1" @click.prevent="submitForm">
Generate Pre-filled URL
</v-button>
</template>
</open-form>
</div>
<template v-if="prefillFormData">
<h3 class="mt-6 text-xl font-semibold mb-4 pt-6">
Your Prefill url
</h3>
<form-url-prefill :form="form" :form-data="prefillFormData" :extra-query-param="extraQueryParam" />
</template>
</div>
</modal>
</div>
</template>
<script>
import FormUrlPrefill from '../../../open/forms/components/FormUrlPrefill.vue'
import ProTag from '~/components/global/ProTag.vue'
import OpenForm from '../../../open/forms/OpenForm.vue'
import { themes } from '~/lib/forms/form-themes.js'
export default {
name: 'UrlFormPrefill',
components: { FormUrlPrefill, ProTag, OpenForm },
props: {
form: { type: Object, required: true },
extraQueryParam: { type: String, default: '' }
},
data: () => ({
prefillFormData: null,
theme: themes.default,
showUrlFormPrefillModal: false,
}),
computed: {},
methods: {
generateUrl (formData, onFailure) {
this.prefillFormData = formData
this.$nextTick().then(() => {
if (this.$refs.content) {
this.$refs.content.parentElement.parentElement.parentElement.scrollTop = this.$refs.content.offsetHeight
}
})
}
}
}
</script>