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
This commit is contained in:
formsdev 2023-12-31 17:09:01 +05:30 committed by GitHub
parent b4365b5e30
commit 6fd2985ff5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 586 additions and 1390 deletions

View File

@ -27,7 +27,8 @@
<NuxtPage/> <NuxtPage/>
</NuxtLayout> </NuxtLayout>
<ToolsStopImpersonation/> <ToolsStopImpersonation/>
<!-- <notifications />-->
<Notifications />
</div> </div>
</template> </template>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="fixed top-0 bottom-24 right-0 flex px-4 items-start justify-end z-50 pointer-events-none"> <div class="fixed top-0 bottom-24 right-0 flex px-4 items-start justify-end z-50 relative pointer-events-auto">
<notification v-slot="{ notifications, close }"> <NuxtNotifications>
<div class="relative pointer-events-auto" v-for="notification in notifications" :key="notification.id"> <template #body="props">
<div <div
v-if="notification.type==='success'" v-if="props.item.type==='success'"
class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4" class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4"
> >
<div class="flex justify-center items-center w-12 bg-green-500"> <div class="flex justify-center items-center w-12 bg-green-500">
@ -14,13 +14,13 @@
<div class="-mx-3 py-2 px-4"> <div class="-mx-3 py-2 px-4">
<div class="mx-3"> <div class="mx-3">
<span class="text-green-500 font-semibold pr-6">{{notification.title}}</span> <span class="text-green-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p> <p class="text-gray-600 text-sm">{{props.item.text}}</p>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-if="notification.type==='info'" v-if="props.item.type==='info'"
class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4" class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4"
> >
<div class="flex justify-center items-center w-12 bg-blue-500"> <div class="flex justify-center items-center w-12 bg-blue-500">
@ -31,13 +31,13 @@
<div class="-mx-3 py-2 px-4"> <div class="-mx-3 py-2 px-4">
<div class="mx-3"> <div class="mx-3">
<span class="text-blue-500 font-semibold pr-6">{{notification.title}}</span> <span class="text-blue-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">T{{notification.text}}</p> <p class="text-gray-600 text-sm">T{{props.item.text}}</p>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-if="notification.type==='error'" v-if="props.item.type==='error'"
class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4" class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4"
> >
<div class="flex justify-center items-center w-12 bg-red-500"> <div class="flex justify-center items-center w-12 bg-red-500">
@ -54,14 +54,14 @@
<div class="-mx-3 py-2 px-4"> <div class="-mx-3 py-2 px-4">
<div class="mx-3"> <div class="mx-3">
<span class="text-red-500 font-semibold pr-6">{{notification.title}}</span> <span class="text-red-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p> <p class="text-gray-600 text-sm">{{props.item.text}}</p>
</div> </div>
</div> </div>
</div> </div>
<div <div
class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4" class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4"
v-if="notification.type==='warning'" v-if="props.item.type==='warning'"
> >
<div class="flex justify-center items-center w-12 bg-yellow-500"> <div class="flex justify-center items-center w-12 bg-yellow-500">
<svg <svg
@ -77,14 +77,14 @@
<div class="-mx-3 py-2 px-4"> <div class="-mx-3 py-2 px-4">
<div class="mx-3"> <div class="mx-3">
<span class="text-yellow-500 font-semibold pr-6">{{notification.title}}</span> <span class="text-yellow-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p> <p class="text-gray-600 text-sm">{{props.item.text}}</p>
</div> </div>
</div> </div>
</div> </div>
<div <div
class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4" class="flex max-w-sm w-full mx-auto bg-white shadow-md rounded-lg overflow-hidden mt-4"
v-if="notification.type==='confirm'" v-if="props.item.type==='confirm'"
> >
<div class="flex justify-center items-center w-12 bg-blue-500"> <div class="flex justify-center items-center w-12 bg-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-white"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6 text-white">
@ -94,16 +94,16 @@
<div class="-mx-3 py-2 px-4"> <div class="-mx-3 py-2 px-4">
<div class="mx-3"> <div class="mx-3">
<span class="text-blue-500 font-semibold pr-6">{{notification.title}}</span> <span class="text-blue-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p> <p class="text-gray-600 text-sm">{{props.item.text}}</p>
<div class="w-full flex gap-2 mt-1"> <div class="w-full flex gap-2 mt-1">
<v-button color="blue" size="small" @click.prevent="notification.success();close(notification.id)">Yes</v-button> <v-button color="blue" size="small" @click.prevent="props.item.data.success();props.close()">Yes</v-button>
<v-button color="gray" shade="light" size="small" @click.prevent="notification.failure();close(notification.id)">No</v-button> <v-button color="gray" shade="light" size="small" @click.prevent="props.item.data.failure();props.close()">No</v-button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<button @click="close(notification.id)" class="absolute top-0 right-0 px-2 py-2 cursor-pointer"> <button @click="props.close()" class="absolute top-0 right-0 px-2 py-2 cursor-pointer">
<svg <svg
class="fill-current h-6 w-6 text-gray-300 hover:text-gray-500" class="fill-current h-6 w-6 text-gray-300 hover:text-gray-500"
role="button" role="button"
@ -116,8 +116,8 @@
/> />
</svg> </svg>
</button> </button>
</div> </template>
</notification> </NuxtNotifications>
</div> </div>
</template> </template>

View File

@ -34,6 +34,11 @@ export default {
default: () => {} default: () => {}
} }
}, },
setup () {
return {
useAlert: useAlert()
}
},
data () { data () {
return { return {
} }
@ -44,18 +49,18 @@ export default {
}, },
methods: { methods: {
onDeleteClick () { onDeleteClick () {
this.alertConfirm('Do you really want to delete this record?', this.deleteRecord) this.useAlert.confirm('Do you really want to delete this record?', this.deleteRecord)
}, },
async deleteRecord () { async deleteRecord () {
axios.delete('/api/open/forms/' + this.form.id + '/records/' + this.rowid + '/delete').then(async (response) => { axios.delete('/api/open/forms/' + this.form.id + '/records/' + this.rowid + '/delete').then(async (response) => {
if (response.data.type === 'success') { if (response.data.type === 'success') {
this.$emit('deleted') this.$emit('deleted')
this.alertSuccess(response.data.message) this.useAlert.success(response.data.message)
} else { } else {
this.alertError('Something went wrong!') this.useAlert.error('Something went wrong!')
} }
}).catch((error) => { }).catch((error) => {
this.alertError(error.response.data.message) this.useAlert.error(error.response.data.message)
}) })
} }
} }

View File

@ -129,7 +129,8 @@ export default {
setup(props) { setup(props) {
return { return {
isIframe: useIsIframe(), isIframe: useIsIframe(),
pendingSubmission: pendingSubmission(props.form) pendingSubmission: pendingSubmission(props.form),
confetti: useConfetti()
} }
}, },
@ -214,12 +215,11 @@ export default {
// If enabled display confetti // If enabled display confetti
if (this.form.confetti_on_submission) { if (this.form.confetti_on_submission) {
this.playConfetti() this.confetti.play()
} }
}).catch((error) => { }).catch((error) => {
if (error.response && error.response.data && error.response.data.message) { if (error.response && error.response.data && error.response.data.message) {
console.error(error) useAlert().error(error.response.data.message)
// this.alertError(error.response.data.message)
} }
this.loading = false this.loading = false
onFailure() onFailure()

View File

@ -56,9 +56,9 @@ export default {
document.execCommand('copy') document.execCommand('copy')
document.body.removeChild(el) document.body.removeChild(el)
if(this.isDraft){ if(this.isDraft){
this.alertWarning('Copied! But other people won\'t be able to see the form since it\'s currently in draft mode') useAlert().warning('Copied! But other people won\'t be able to see the form since it\'s currently in draft mode')
} else { } else {
this.alertSuccess('Copied!') useAlert().success('Copied!')
} }
} }

View File

@ -217,9 +217,9 @@ export default {
methods: { methods: {
displayFormModificationAlert (responseData) { displayFormModificationAlert (responseData) {
if (responseData.form && responseData.form.cleanings && Object.keys(responseData.form.cleanings).length > 0) { if (responseData.form && responseData.form.cleanings && Object.keys(responseData.form.cleanings).length > 0) {
// this.alertWarning(responseData.message) useAlert().warning(responseData.message)
} else { } else {
// this.alertSuccess(responseData.message) useAlert().success(responseData.message)
} }
}, },
openCrisp () { openCrisp () {

View File

@ -19,7 +19,7 @@
/> />
<div class="-mt-3 mb-3 text-gray-400 dark:text-gray-500"> <div class="-mt-3 mb-3 text-gray-400 dark:text-gray-500">
<small> <small>
Need another theme? <a href="#" @click.prevent="openChat">Send us some suggestions!</a> Need another theme? <a href="#" @click.prevent="crisp.openAndShowChat">Send us some suggestions!</a>
</small> </small>
</div> </div>
@ -80,56 +80,25 @@
</editor-options-panel> </editor-options-panel>
</template> </template>
<script> <script setup>
import { useWorkingFormStore } from '../../../../../stores/working_form' import { useWorkingFormStore } from '../../../../../stores/working_form'
import EditorOptionsPanel from '../../../editors/EditorOptionsPanel.vue' import EditorOptionsPanel from '../../../editors/EditorOptionsPanel.vue'
import ProTag from '~/components/global/ProTag.vue' import ProTag from '~/components/global/ProTag.vue'
export default { const workingFormStore = useWorkingFormStore()
components: { EditorOptionsPanel, ProTag }, const form = storeToRefs(workingFormStore).content
props: { const isMounted = ref(false)
}, const crisp = useCrisp()
setup () { const confetti = useConfetti()
const workingFormStore = useWorkingFormStore()
return {
workingFormStore
}
},
data () {
return {
isMounted: false
}
},
computed: { onMounted(() => {
form: { isMounted.value = true
get () { })
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
}
},
watch: {}, const onChangeConfettiOnSubmission = (val) => {
form.confetti_on_submission = val
mounted () { if (isMounted.value && val) {
this.isMounted = true confetti.play()
},
methods: {
onChangeConfettiOnSubmission (val) {
this.form.confetti_on_submission = val
if (this.isMounted && val) {
this.playConfetti()
}
},
openChat () {
window.$crisp.push(['do', 'chat:show'])
window.$crisp.push(['do', 'chat:open'])
}
} }
} }
</script> </script>

View File

@ -6,20 +6,20 @@
</div> </div>
<SelectInput v-model="content.operator" class="w-full" :options="operators" <SelectInput v-model="content.operator" class="w-full" :options="operators"
:name="'operator_'+property.id" placeholder="Comparison operator" :name="'operator_'+property.id" placeholder="Comparison operator"
@update:modelValue="operatorChanged()" @update:model-value="operatorChanged()"
/> />
<template v-if="hasInput"> <template v-if="hasInput">
<component v-bind="inputComponentData" :is="inputComponentData.component" v-model="content.value" class="w-full" <component v-bind="inputComponentData" :is="inputComponentData.component" v-model="content.value" class="w-full"
:name="'value_'+property.id" placeholder="Filter Value" :name="'value_'+property.id" placeholder="Filter Value"
@update:modelValue="emitInput()" @update:model-value="emitInput()"
/> />
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import OpenFilters from '../../../../../../data/open_filters.json' import OpenFilters from '../../../../../data/open_filters.json'
export default { export default {
components: { }, components: { },

View File

@ -1,5 +1,5 @@
<template> <template>
<query-builder v-model="query" :rules="rules" :config="config" @update:modelValue="onChange"> <query-builder v-model="query" :rules="rules" :config="config" @update:model-value="onChange">
<template #groupOperator="props"> <template #groupOperator="props">
<div class="query-builder-group-slot__group-selection flex items-center px-5 border-b py-1 mb-1 flex"> <div class="query-builder-group-slot__group-selection flex items-center px-5 border-b py-1 mb-1 flex">
<p class="mr-2 font-semibold"> <p class="mr-2 font-semibold">
@ -13,7 +13,7 @@
option-key="identifier" option-key="identifier"
name="operator-input" name="operator-input"
margin-bottom="" margin-bottom=""
@update:modelValue="props.updateCurrentOperator($event)" @update:model-value="props.updateCurrentOperator($event)"
/> />
</div> </div>
</template> </template>
@ -24,7 +24,7 @@
<component <component
:is="ruleCtrl.ruleComponent" :is="ruleCtrl.ruleComponent"
:model-value="ruleCtrl.ruleData" :model-value="ruleCtrl.ruleData"
@update:modelValue="ruleCtrl.updateRuleData" @update:model-value="ruleCtrl.updateRuleData"
/> />
</template> </template>
</query-builder> </query-builder>

View File

@ -69,12 +69,11 @@
<script> <script>
import ConditionEditor from './ConditionEditor.vue' import ConditionEditor from './ConditionEditor.vue'
import Modal from '../../../../global/Modal.vue' import Modal from '../../../../global/Modal.vue'
import SelectInput from '../../../../forms/SelectInput.vue'
import clonedeep from 'clone-deep' import clonedeep from 'clone-deep'
export default { export default {
name: 'FormBlockLogicEditor', name: 'FormBlockLogicEditor',
components: { SelectInput, Modal, ConditionEditor }, components: { Modal, ConditionEditor },
props: { props: {
field: { field: {
type: Object, type: Object,

View File

@ -93,7 +93,8 @@ export default {
user : computed(() => authStore.user), user : computed(() => authStore.user),
templates : computed(() => templatesStore.content), templates : computed(() => templatesStore.content),
industries : computed(() => templatesStore.industries), industries : computed(() => templatesStore.industries),
types : computed(() => templatesStore.types) types : computed(() => templatesStore.types),
useAlert: useAlert()
} }
}, },
@ -156,7 +157,7 @@ export default {
this.templateForm.form = this.form this.templateForm.form = this.form
await this.templateForm.post('/api/templates').then((response) => { await this.templateForm.post('/api/templates').then((response) => {
if (response.data.message) { if (response.data.message) {
this.alertSuccess(response.data.message) this.useAlert.success(response.data.message)
} }
this.templatesStore.save(response.data.data) this.templatesStore.save(response.data.data)
this.$emit('close') this.$emit('close')
@ -166,7 +167,7 @@ export default {
this.templateForm.form = this.form this.templateForm.form = this.form
await this.templateForm.put('/api/templates/' + this.template.id).then((response) => { await this.templateForm.put('/api/templates/' + this.template.id).then((response) => {
if (response.data.message) { if (response.data.message) {
this.alertSuccess(response.data.message) this.useAlert.success(response.data.message)
} }
this.templatesStore.save(response.data.data) this.templatesStore.save(response.data.data)
this.$emit('close') this.$emit('close')
@ -176,7 +177,7 @@ export default {
if (!this.template) return if (!this.template) return
axios.delete('/api/templates/' + this.template.id).then((response) => { axios.delete('/api/templates/' + this.template.id).then((response) => {
if (response.data.message) { if (response.data.message) {
this.alertSuccess(response.data.message) this.useAlert.success(response.data.message)
} }
this.$router.push({ name: 'templates' }) this.$router.push({ name: 'templates' })
this.templatesStore.remove(this.template) this.templatesStore.remove(this.template)

View File

@ -109,7 +109,7 @@ export default {
return this.field && this.field.type.startsWith('nf') return this.field && this.field.type.startsWith('nf')
}, },
typeCanBeChanged () { typeCanBeChanged () {
return ['text', 'email', 'phone', 'number', 'select', 'multi_select'].includes(this.field.type) return ['text', 'email', 'phone_number', 'number', 'select', 'multi_select'].includes(this.field.type)
} }
}, },

View File

@ -86,14 +86,16 @@
</div> </div>
<!-- Logic Block --> <!-- Logic Block -->
<!-- <form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />--> <form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />
</div> </div>
</template> </template>
<script> <script>
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
export default { export default {
name: 'BlockOptions', name: 'BlockOptions',
components: { }, components: {FormBlockLogicEditor},
props: { props: {
field: { field: {
type: Object, type: Object,

View File

@ -41,11 +41,11 @@ export default {
computed: { computed: {
changeTypeOptions () { changeTypeOptions () {
let newTypes = [] let newTypes = []
if (['text', 'email', 'phone', 'number'].includes(this.field.type)) { if (['text', 'email', 'phone_number', 'number'].includes(this.field.type)) {
newTypes = [ newTypes = [
{ name: 'Text Input', value: 'text' }, { name: 'Text Input', value: 'text' },
{ name: 'Email Input', value: 'email' }, { name: 'Email Input', value: 'email' },
{ name: 'Phone Input', value: 'phone' }, { name: 'Phone Input', value: 'phone_number' },
{ name: 'Number Input', value: 'number' } { name: 'Number Input', value: 'number' }
] ]
} }

View File

@ -82,7 +82,7 @@
/> />
<v-checkbox v-model="field.is_scale" class="mt-4" <v-checkbox v-model="field.is_scale" class="mt-4"
:name="field.id+'_is_scale'" @input="initScale" :name="field.id+'_is_scale'" @update:model-value="initScale"
> >
Scale Scale
</v-checkbox> </v-checkbox>
@ -337,7 +337,7 @@
{name:'Above input',value:'above_input'}, {name:'Above input',value:'above_input'},
]" ]"
:form="field" label="Field Help Position" :form="field" label="Field Help Position"
@input="onFieldHelpPositionChange" @update:model-value="onFieldHelpPositionChange"
/> />
<template v-if="['text','number','url','email'].includes(field.type)"> <template v-if="['text','number','url','email'].includes(field.type)">
@ -382,7 +382,7 @@
</div> </div>
<!-- Logic Block --> <!-- Logic Block -->
<!-- <form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />--> <form-block-logic-editor class="py-2 px-4 border-b" :form="form" :field="field" />
</div> </div>
</template> </template>
@ -390,10 +390,11 @@
import timezones from '~/data/timezones.json' import timezones from '~/data/timezones.json'
import countryCodes from '~/data/country_codes.json' import countryCodes from '~/data/country_codes.json'
import CountryFlag from 'vue-country-flag-next' import CountryFlag from 'vue-country-flag-next'
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
export default { export default {
name: 'FieldOptions', name: 'FieldOptions',
components: { CountryFlag }, components: { CountryFlag, FormBlockLogicEditor },
props: { props: {
field: { field: {
type: Object, type: Object,
@ -533,23 +534,23 @@ export default {
}, },
initRating () { initRating () {
if (this.field.is_rating) { if (this.field.is_rating) {
this.$set(this.field, 'is_scale', false) this.field.is_scale = false
if (!this.field.rating_max_value) { if (!this.field.rating_max_value) {
this.$set(this.field, 'rating_max_value', 5) this.field.rating_max_value = 5
} }
} }
}, },
initScale () { initScale () {
if (this.field.is_scale) { if (this.field.is_scale) {
this.$set(this.field, 'is_rating', false) this.field.is_rating = false
if (!this.field.scale_min_value) { if (!this.field.scale_min_value) {
this.$set(this.field, 'scale_min_value', 1) this.field.scale_min_value = 1
} }
if (!this.field.scale_max_value) { if (!this.field.scale_max_value) {
this.$set(this.field, 'scale_max_value', 5) this.field.scale_max_value = 5
} }
if (!this.field.scale_step_value) { if (!this.field.scale_step_value) {
this.$set(this.field, 'scale_step_value', 1) this.field.scale_step_value = 1
} }
} }
}, },

View File

@ -14,8 +14,7 @@ export default {
components: { OpenTag }, components: { OpenTag },
props: { props: {
value: { value: {
type: Object | null, type: Object
required: true
} }
}, },

View File

@ -129,10 +129,10 @@ export default {
// AppSumo License // AppSumo License
if (data.appsumo_license === false) { if (data.appsumo_license === false) {
this.alertError('Invalid AppSumo license. This probably happened because this license was already' + useAlert().error('Invalid AppSumo license. This probably happened because this license was already' +
' attached to another OpnForm account. Please contact support.') ' attached to another OpnForm account. Please contact support.')
} else if (data.appsumo_license === true) { } else if (data.appsumo_license === true) {
this.alertSuccess('Your AppSumo license was successfully activated! You now have access to all the' + useAlert().success('Your AppSumo license was successfully activated! You now have access to all the' +
' features of the AppSumo deal.') ' features of the AppSumo deal.')
} }

View File

@ -97,7 +97,11 @@ export default {
props: { props: {
show: { type: Boolean, required: true } show: { type: Boolean, required: true }
}, },
setup () {
return {
useAlert: useAlert()
}
},
data: () => ({ data: () => ({
state: 'default', state: 'default',
aiForm: useForm({ aiForm: useForm({
@ -118,10 +122,10 @@ export default {
this.loading = true this.loading = true
this.aiForm.post('/api/forms/ai/generate').then(response => { this.aiForm.post('/api/forms/ai/generate').then(response => {
this.alertSuccess(response.data.message) this.useAlert.success(response.data.message)
this.fetchGeneratedForm(response.data.ai_form_completion_id) this.fetchGeneratedForm(response.data.ai_form_completion_id)
}).catch(error => { }).catch(error => {
this.alertError(error.response.data.message) this.useAlert.error(error.response.data.message)
this.loading = false this.loading = false
this.state = 'default' this.state = 'default'
}) })
@ -131,18 +135,18 @@ export default {
setTimeout(() => { setTimeout(() => {
axios.get('/api/forms/ai/' + generationId).then(response => { axios.get('/api/forms/ai/' + generationId).then(response => {
if (response.data.ai_form_completion.status === 'completed') { if (response.data.ai_form_completion.status === 'completed') {
this.alertSuccess(response.data.message) this.useAlert.success(response.data.message)
this.$emit('form-generated', JSON.parse(response.data.ai_form_completion.result)) this.$emit('form-generated', JSON.parse(response.data.ai_form_completion.result))
this.$emit('close') this.$emit('close')
} else if (response.data.ai_form_completion.status === 'failed') { } else if (response.data.ai_form_completion.status === 'failed') {
this.alertError('Something went wrong, please try again.') this.useAlert.error('Something went wrong, please try again.')
this.state = 'default' this.state = 'default'
this.loading = false this.loading = false
} else { } else {
this.fetchGeneratedForm(generationId) this.fetchGeneratedForm(generationId)
} }
}).catch(error => { }).catch(error => {
this.alertError(error.response.data.message) this.useAlert.error(error.response.data.message)
this.state = 'default' this.state = 'default'
this.loading = false this.loading = false
}) })

View File

@ -157,7 +157,8 @@ export default {
const formsStore = useFormsStore() const formsStore = useFormsStore()
return { return {
formsStore, formsStore,
user: computed(() => authStore.user) user: computed(() => authStore.user),
useAlert: useAlert()
} }
}, },
@ -180,7 +181,7 @@ export default {
el.select() el.select()
document.execCommand('copy') document.execCommand('copy')
document.body.removeChild(el) document.body.removeChild(el)
this.alertSuccess('Copied!') this.useAlert.success('Copied!')
}, },
duplicateForm () { duplicateForm () {
if (this.loadingDuplicate) return if (this.loadingDuplicate) return
@ -188,7 +189,7 @@ export default {
opnFetch(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate',{method: 'POST'}).then((data) => { opnFetch(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate',{method: 'POST'}).then((data) => {
this.formsStore.save(data.new_form) this.formsStore.save(data.new_form)
this.$router.push({ name: 'forms-show', params: { slug: data.new_form.slug } }) this.$router.push({ name: 'forms-show', params: { slug: data.new_form.slug } })
this.alertSuccess('Form was successfully duplicated.') this.useAlert.success('Form was successfully duplicated.')
this.loadingDuplicate = false this.loadingDuplicate = false
}) })
}, },
@ -198,7 +199,7 @@ export default {
opnFetch(this.formEndpoint.replace('{id}', this.form.id),{method:'DELETE'}).then(() => { opnFetch(this.formEndpoint.replace('{id}', this.form.id),{method:'DELETE'}).then(() => {
this.formsStore.remove(this.form) this.formsStore.remove(this.form)
this.$router.push({ name: 'home' }) this.$router.push({ name: 'home' })
this.alertSuccess('Form was deleted.') this.useAlert.success('Form was deleted.')
this.loadingDelete = false this.loadingDelete = false
}) })
} }

View File

@ -106,7 +106,7 @@ export default {
axios.put(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option).then((response) => { axios.put(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option).then((response) => {
this.formsStore.addOrUpdate(response.data.form) this.formsStore.addOrUpdate(response.data.form)
this.$router.push({name: 'forms-slug-show-share', params: {slug: response.data.form.slug}}) this.$router.push({name: 'forms-slug-show-share', params: {slug: response.data.form.slug}})
this.alertSuccess(response.data.message) useAlert().success(response.data.message)
this.loadingNewLink = false this.loadingNewLink = false
}).finally(() => { }).finally(() => {
this.showGenerateFormLinkModal = false this.showGenerateFormLinkModal = false

View File

@ -84,7 +84,7 @@ export default {
axios.get('/api/subscription/new/' + this.plan + '/' + (!this.yearly ? 'monthly' : 'yearly') + '/checkout/with-trial').then((response) => { axios.get('/api/subscription/new/' + this.plan + '/' + (!this.yearly ? 'monthly' : 'yearly') + '/checkout/with-trial').then((response) => {
window.location = response.data.checkout_url window.location = response.data.checkout_url
}).catch((error) => { }).catch((error) => {
this.alertError(error.response.data.message) useAlert().error(error.response.data.message)
}).finally(() => { }).finally(() => {
this.loading = false this.loading = false
this.close() this.close()

51
client/composables/useAlert.js vendored Normal file
View File

@ -0,0 +1,51 @@
const { notify } = useNotification()
export const useAlert = () => {
function success(message, autoClose = 10000) {
notify({
title: 'Success',
text: message,
type: 'success',
duration: autoClose
})
}
function error(message, autoClose = 10000) {
notify({
title: 'Error',
text: message,
type: 'error',
duration: autoClose
})
}
function warning(message, autoClose = 10000) {
notify({
title: 'Warning',
text: message,
type: 'warning',
duration: autoClose
})
}
function confirm(message, success, failure = ()=>{}, autoClose = 10000) {
notify({
title: 'Confirm',
text: message,
type: 'confirm',
duration: autoClose,
data: {
success,
failure
}
})
}
return {
success,
error,
warning,
confirm
}
}

22
client/composables/useConfetti.js vendored Normal file
View File

@ -0,0 +1,22 @@
import { ref, onUnmounted } from 'vue'
export const useConfetti = () => {
let timeoutId = ref(null)
const nuxtApp = useNuxtApp()
const $confetti = nuxtApp.vueApp.config.globalProperties.$confetti
function play(duration=3000) {
$confetti.start({ defaultSize: 6 })
timeoutId = setTimeout(() => {
$confetti.stop()
}, duration)
}
onUnmounted(() => {
if (timeoutId) clearTimeout(timeoutId)
})
return {
play
}
}

View File

@ -2,9 +2,9 @@ export default {
methods: { methods: {
displayFormModificationAlert (responseData) { displayFormModificationAlert (responseData) {
if (responseData.form && responseData.form.cleanings && Object.keys(responseData.form.cleanings).length > 0) { if (responseData.form && responseData.form.cleanings && Object.keys(responseData.form.cleanings).length > 0) {
this.alertWarning(responseData.message) useAlert().warning(responseData.message)
} else { } else {
this.alertSuccess(responseData.message) useAlert().success(responseData.message)
} }
} }
} }

View File

@ -29,7 +29,8 @@ export default defineNuxtConfig({
modules: [ modules: [
'@pinia/nuxt', '@pinia/nuxt',
'@vueuse/nuxt', '@vueuse/nuxt',
'@vueuse/motion/nuxt' '@vueuse/motion/nuxt',
'nuxt3-notifications'
], ],
postcss: { postcss: {
plugins: { plugins: {

1621
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "export NODE_TLS_REJECT_UNAUTHORIZED=0; nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
@ -50,7 +50,7 @@
"vue-notion": "^3.0.0-beta.1", "vue-notion": "^3.0.0-beta.1",
"vue-signature-pad": "^3.0.2", "vue-signature-pad": "^3.0.2",
"vue3-editor": "^0.1.1", "vue3-editor": "^0.1.1",
"vue3-vt-notifications": "^1.0.0", "nuxt3-notifications": "^1.1.9",
"vuedraggable": "next" "vuedraggable": "next"
} }
} }

View File

@ -25,7 +25,7 @@ export default {
beforeRouteLeave (to, from, next) { beforeRouteLeave (to, from, next) {
if (this.isDirty()) { if (this.isDirty()) {
return this.alertConfirm('Changes you made may not be saved. Are you sure want to leave?', () => { return useAlert().confirm('Changes you made may not be saved. Are you sure want to leave?', () => {
window.onbeforeunload = null window.onbeforeunload = null
next() next()
}, () => {}) }, () => {})

View File

@ -30,7 +30,7 @@ import CreateFormBaseModal from '../../../components/pages/forms/create/CreateFo
// beforeRouteLeave (to, from, next) { // beforeRouteLeave (to, from, next) {
// if (this.isDirty()) { // if (this.isDirty()) {
// return this.alertConfirm('Changes you made may not be saved. Are you sure want to leave?', () => { // return useAlert().confirm('Changes you made may not be saved. Are you sure want to leave?', () => {
// window.onbeforeunload = null // window.onbeforeunload = null
// next() // next()
// }, () => {}) // }, () => {})

View File

@ -41,14 +41,14 @@ export default {
this.loading = true this.loading = true
axios.delete('/api/user').then(async (response) => { axios.delete('/api/user').then(async (response) => {
this.loading = false this.loading = false
this.alertSuccess(response.data.message) useAlert().success(response.data.message)
// Log out the user. // Log out the user.
await this.authStore.logout() await this.authStore.logout()
// Redirect to login. // Redirect to login.
this.$router.push({ name: 'login' }) this.$router.push({ name: 'login' })
}).catch((error) => { }).catch((error) => {
this.alertError(error.response.data.message) useAlert().error(error.response.data.message)
this.loading = false this.loading = false
}) })
} }

View File

@ -76,7 +76,7 @@ export default {
this.workspacesStore.set([]) this.workspacesStore.set([])
this.$router.push({ name: 'home' }) this.$router.push({ name: 'home' })
}).catch((error) => { }).catch((error) => {
this.alertError(error.response.data.message) useAlert().error(error.response.data.message)
this.loading = false this.loading = false
}) })

View File

@ -51,7 +51,7 @@ export default {
const url = response.data.portal_url const url = response.data.portal_url
window.location = url window.location = url
}).catch((error) => { }).catch((error) => {
this.alertError(error.response.data.message) useAlert().error(error.response.data.message)
}).finally(() => { }).finally(() => {
this.billingLoading = false this.billingLoading = false
}) })

View File

@ -169,9 +169,9 @@ export default {
.filter(domain => domain && domain.length > 0) .filter(domain => domain && domain.length > 0)
}).then((response) => { }).then((response) => {
this.workspacesStore.addOrUpdate(response.data) this.workspacesStore.addOrUpdate(response.data)
this.alertSuccess('Custom domains saved.') useAlert().success('Custom domains saved.')
}).catch((error) => { }).catch((error) => {
this.alertError('Failed to update custom domains: ' + error.response.data.message) useAlert().error('Failed to update custom domains: ' + error.response.data.message)
}).finally(() => { }).finally(() => {
this.customDomainsLoading = false this.customDomainsLoading = false
}) })
@ -182,12 +182,12 @@ export default {
}, },
deleteWorkspace (workspace) { deleteWorkspace (workspace) {
if (this.workspaces.length <= 1) { if (this.workspaces.length <= 1) {
this.alertError('You cannot delete your only workspace.') useAlert().error('You cannot delete your only workspace.')
return return
} }
this.alertConfirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => { useAlert().confirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
this.workspacesStore.delete(workspace.id).then(() => { this.workspacesStore.delete(workspace.id).then(() => {
this.alertSuccess('Workspace successfully removed.') useAlert().success('Workspace successfully removed.')
}) })
}) })
}, },

View File

@ -24,7 +24,7 @@ export default {
mounted () { mounted () {
this.$router.push({ name: 'pricing' }) this.$router.push({ name: 'pricing' })
this.alertError('Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.') useAlert().error('Unfortunately we could not confirm your subscription. Please try again and contact us if the issue persists.')
}, },
computed: {} computed: {}

View File

@ -63,10 +63,10 @@ export default {
this.$router.push({ name: 'home' }) this.$router.push({ name: 'home' })
if (this.user.has_enterprise_subscription) { if (this.user.has_enterprise_subscription) {
this.alertSuccess('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Enterprise ' + useAlert().success('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Enterprise ' +
'features. No need to invite your teammates, just ask them to create a OpnForm account and to connect the same Notion workspace. Feel free to contact us if you have any question 🙌') 'features. No need to invite your teammates, just ask them to create a OpnForm account and to connect the same Notion workspace. Feel free to contact us if you have any question 🙌')
} else { } else {
this.alertSuccess('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Pro ' + useAlert().success('Awesome! Your subscription to OpnForm is now confirmed! You now have access to all Pro ' +
'features. Feel free to contact us if you have any question 🙌') 'features. Feel free to contact us if you have any question 🙌')
} }
} }

View File

@ -261,7 +261,7 @@ const copyTemplateUrl = () => {
el.select() el.select()
document.execCommand('copy') document.execCommand('copy')
document.body.removeChild(el) document.body.removeChild(el)
this.alertSuccess('Copied!') useAlert().success('Copied!')
} }
// metaTitle() { // metaTitle() {

5
client/plugins/vue-confetti.client.js vendored Normal file
View File

@ -0,0 +1,5 @@
import VueConfetti from 'vue-confetti'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(VueConfetti)
})

View File

@ -20,6 +20,11 @@ export const useWorkspacesStore = defineStore('workspaces', () => {
storedWorkspaceId.value = id storedWorkspaceId.value = id
} }
const set = (items) => {
contentStore.content.value = new Map
save(items)
}
const save = (items) => { const save = (items) => {
contentStore.save(items) contentStore.save(items)
if ((getCurrent.value == null) && contentStore.length.value) { if ((getCurrent.value == null) && contentStore.length.value) {
@ -39,6 +44,7 @@ export const useWorkspacesStore = defineStore('workspaces', () => {
currentId, currentId,
getCurrent, getCurrent,
setCurrentId, setCurrentId,
set,
save, save,
remove remove
} }