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
38 changed files with 586 additions and 1390 deletions

View File

@@ -1,9 +1,9 @@
<template>
<div class="fixed top-0 bottom-24 right-0 flex px-4 items-start justify-end z-50 pointer-events-none">
<notification v-slot="{ notifications, close }">
<div class="relative pointer-events-auto" v-for="notification in notifications" :key="notification.id">
<div class="fixed top-0 bottom-24 right-0 flex px-4 items-start justify-end z-50 relative pointer-events-auto">
<NuxtNotifications>
<template #body="props">
<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"
>
<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">
<span class="text-green-500 font-semibold pr-6">{{notification.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p>
<span class="text-green-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{props.item.text}}</p>
</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"
>
<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">
<span class="text-blue-500 font-semibold pr-6">{{notification.title}}</span>
<p class="text-gray-600 text-sm">T{{notification.text}}</p>
<span class="text-blue-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">T{{props.item.text}}</p>
</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"
>
<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">
<span class="text-red-500 font-semibold pr-6">{{notification.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p>
<span class="text-red-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{props.item.text}}</p>
</div>
</div>
</div>
<div
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">
<svg
@@ -77,14 +77,14 @@
<div class="-mx-3 py-2 px-4">
<div class="mx-3">
<span class="text-yellow-500 font-semibold pr-6">{{notification.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p>
<span class="text-yellow-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{props.item.text}}</p>
</div>
</div>
</div>
<div
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">
<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">
<span class="text-blue-500 font-semibold pr-6">{{notification.title}}</span>
<p class="text-gray-600 text-sm">{{notification.text}}</p>
<span class="text-blue-500 font-semibold pr-6">{{props.item.title}}</span>
<p class="text-gray-600 text-sm">{{props.item.text}}</p>
<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="gray" shade="light" size="small" @click.prevent="notification.failure();close(notification.id)">No</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="props.item.data.failure();props.close()">No</v-button>
</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
class="fill-current h-6 w-6 text-gray-300 hover:text-gray-500"
role="button"
@@ -116,8 +116,8 @@
/>
</svg>
</button>
</div>
</notification>
</template>
</NuxtNotifications>
</div>
</template>

View File

@@ -34,6 +34,11 @@ export default {
default: () => {}
}
},
setup () {
return {
useAlert: useAlert()
}
},
data () {
return {
}
@@ -44,18 +49,18 @@ export default {
},
methods: {
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 () {
axios.delete('/api/open/forms/' + this.form.id + '/records/' + this.rowid + '/delete').then(async (response) => {
if (response.data.type === 'success') {
this.$emit('deleted')
this.alertSuccess(response.data.message)
this.useAlert.success(response.data.message)
} else {
this.alertError('Something went wrong!')
this.useAlert.error('Something went wrong!')
}
}).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) {
return {
isIframe: useIsIframe(),
pendingSubmission: pendingSubmission(props.form)
pendingSubmission: pendingSubmission(props.form),
confetti: useConfetti()
}
},
@@ -214,12 +215,11 @@ export default {
// If enabled display confetti
if (this.form.confetti_on_submission) {
this.playConfetti()
this.confetti.play()
}
}).catch((error) => {
if (error.response && error.response.data && error.response.data.message) {
console.error(error)
// this.alertError(error.response.data.message)
useAlert().error(error.response.data.message)
}
this.loading = false
onFailure()

View File

@@ -56,9 +56,9 @@ export default {
document.execCommand('copy')
document.body.removeChild(el)
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 {
this.alertSuccess('Copied!')
useAlert().success('Copied!')
}
}

View File

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

View File

@@ -19,7 +19,7 @@
/>
<div class="-mt-3 mb-3 text-gray-400 dark:text-gray-500">
<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>
</div>
@@ -80,56 +80,25 @@
</editor-options-panel>
</template>
<script>
<script setup>
import { useWorkingFormStore } from '../../../../../stores/working_form'
import EditorOptionsPanel from '../../../editors/EditorOptionsPanel.vue'
import ProTag from '~/components/global/ProTag.vue'
export default {
components: { EditorOptionsPanel, ProTag },
props: {
},
setup () {
const workingFormStore = useWorkingFormStore()
return {
workingFormStore
}
},
data () {
return {
isMounted: false
}
},
const workingFormStore = useWorkingFormStore()
const form = storeToRefs(workingFormStore).content
const isMounted = ref(false)
const crisp = useCrisp()
const confetti = useConfetti()
computed: {
form: {
get () {
return this.workingFormStore.content
},
/* We add a setter */
set (value) {
this.workingFormStore.set(value)
}
}
},
onMounted(() => {
isMounted.value = true
})
watch: {},
mounted () {
this.isMounted = true
},
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'])
}
const onChangeConfettiOnSubmission = (val) => {
form.confetti_on_submission = val
if (isMounted.value && val) {
confetti.play()
}
}
</script>

View File

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

View File

@@ -1,5 +1,5 @@
<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">
<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">
@@ -13,7 +13,7 @@
option-key="identifier"
name="operator-input"
margin-bottom=""
@update:modelValue="props.updateCurrentOperator($event)"
@update:model-value="props.updateCurrentOperator($event)"
/>
</div>
</template>
@@ -24,7 +24,7 @@
<component
:is="ruleCtrl.ruleComponent"
:model-value="ruleCtrl.ruleData"
@update:modelValue="ruleCtrl.updateRuleData"
@update:model-value="ruleCtrl.updateRuleData"
/>
</template>
</query-builder>

View File

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

View File

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

View File

@@ -109,7 +109,7 @@ export default {
return this.field && this.field.type.startsWith('nf')
},
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>
<!-- 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>
</template>
<script>
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
export default {
name: 'BlockOptions',
components: { },
components: {FormBlockLogicEditor},
props: {
field: {
type: Object,

View File

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

View File

@@ -82,7 +82,7 @@
/>
<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
</v-checkbox>
@@ -337,7 +337,7 @@
{name:'Above input',value:'above_input'},
]"
:form="field" label="Field Help Position"
@input="onFieldHelpPositionChange"
@update:model-value="onFieldHelpPositionChange"
/>
<template v-if="['text','number','url','email'].includes(field.type)">
@@ -382,7 +382,7 @@
</div>
<!-- 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>
</template>
@@ -390,10 +390,11 @@
import timezones from '~/data/timezones.json'
import countryCodes from '~/data/country_codes.json'
import CountryFlag from 'vue-country-flag-next'
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
export default {
name: 'FieldOptions',
components: { CountryFlag },
components: { CountryFlag, FormBlockLogicEditor },
props: {
field: {
type: Object,
@@ -533,23 +534,23 @@ export default {
},
initRating () {
if (this.field.is_rating) {
this.$set(this.field, 'is_scale', false)
this.field.is_scale = false
if (!this.field.rating_max_value) {
this.$set(this.field, 'rating_max_value', 5)
this.field.rating_max_value = 5
}
}
},
initScale () {
if (this.field.is_scale) {
this.$set(this.field, 'is_rating', false)
this.field.is_rating = false
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) {
this.$set(this.field, 'scale_max_value', 5)
this.field.scale_max_value = 5
}
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 },
props: {
value: {
type: Object | null,
required: true
type: Object
}
},

View File

@@ -129,10 +129,10 @@ export default {
// AppSumo License
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.')
} 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.')
}

View File

@@ -97,7 +97,11 @@ export default {
props: {
show: { type: Boolean, required: true }
},
setup () {
return {
useAlert: useAlert()
}
},
data: () => ({
state: 'default',
aiForm: useForm({
@@ -118,10 +122,10 @@ export default {
this.loading = true
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)
}).catch(error => {
this.alertError(error.response.data.message)
this.useAlert.error(error.response.data.message)
this.loading = false
this.state = 'default'
})
@@ -131,18 +135,18 @@ export default {
setTimeout(() => {
axios.get('/api/forms/ai/' + generationId).then(response => {
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('close')
} 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.loading = false
} else {
this.fetchGeneratedForm(generationId)
}
}).catch(error => {
this.alertError(error.response.data.message)
this.useAlert.error(error.response.data.message)
this.state = 'default'
this.loading = false
})

View File

@@ -157,7 +157,8 @@ export default {
const formsStore = useFormsStore()
return {
formsStore,
user: computed(() => authStore.user)
user: computed(() => authStore.user),
useAlert: useAlert()
}
},
@@ -180,7 +181,7 @@ export default {
el.select()
document.execCommand('copy')
document.body.removeChild(el)
this.alertSuccess('Copied!')
this.useAlert.success('Copied!')
},
duplicateForm () {
if (this.loadingDuplicate) return
@@ -188,7 +189,7 @@ export default {
opnFetch(this.formEndpoint.replace('{id}', this.form.id) + '/duplicate',{method: 'POST'}).then((data) => {
this.formsStore.save(data.new_form)
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
})
},
@@ -198,7 +199,7 @@ export default {
opnFetch(this.formEndpoint.replace('{id}', this.form.id),{method:'DELETE'}).then(() => {
this.formsStore.remove(this.form)
this.$router.push({ name: 'home' })
this.alertSuccess('Form was deleted.')
this.useAlert.success('Form was deleted.')
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) => {
this.formsStore.addOrUpdate(response.data.form)
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
}).finally(() => {
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) => {
window.location = response.data.checkout_url
}).catch((error) => {
this.alertError(error.response.data.message)
useAlert().error(error.response.data.message)
}).finally(() => {
this.loading = false
this.close()