55919 form editor error boundary (#494)
* fix password reset bug * form editor error boundary * fix crisp * fix layout on create and edit pages --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
a2c1757815
commit
f4386fbcbc
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<slot v-if="!error" />
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="error"
|
||||||
|
:error="error"
|
||||||
|
:clear-error="clearError"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const error = ref()
|
||||||
|
const emit = defineEmits(['on-error'])
|
||||||
|
function clearError() {
|
||||||
|
error.value = undefined
|
||||||
|
}
|
||||||
|
onErrorCaptured(err => {
|
||||||
|
error.value = err
|
||||||
|
emit('on-error', err)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
const route = useRoute()
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
error.value = undefined
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -3,7 +3,11 @@
|
||||||
size="sm"
|
size="sm"
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
>
|
>
|
||||||
<UTooltip text="Undo" :shortcuts="undoShortcut" :popper="{ placement: 'left' }">
|
<UTooltip
|
||||||
|
text="Undo"
|
||||||
|
:shortcuts="[metaSymbol,'Z']"
|
||||||
|
:popper="{ placement: 'left' }"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
:disabled="!canUndo"
|
:disabled="!canUndo"
|
||||||
color="white"
|
color="white"
|
||||||
|
|
@ -12,7 +16,11 @@
|
||||||
@click="undo"
|
@click="undo"
|
||||||
/>
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip text="Redo" :shortcuts="redoShortcut" :popper="{ placement: 'right' }">
|
<UTooltip
|
||||||
|
text="Redo"
|
||||||
|
:shortcuts="[metaSymbol,'Shift','Z']"
|
||||||
|
:popper="{ placement: 'right' }"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
:disabled="!canRedo"
|
:disabled="!canRedo"
|
||||||
icon="i-material-symbols-redo"
|
icon="i-material-symbols-redo"
|
||||||
|
|
@ -44,30 +52,10 @@ defineShortcuts({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const { metaSymbol } = useShortcuts()
|
||||||
const undoShortcut = computed(() => {
|
|
||||||
return getOS() == 'macOS' ? ['⌘', 'Z'] : ['Ctrl', 'Z']
|
|
||||||
})
|
|
||||||
|
|
||||||
const redoShortcut = computed(() => {
|
|
||||||
return getOS() == 'macOS' ? ['⌘', 'Shift', 'Z'] : ['Ctrl', 'Shift', 'Z']
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => { clearHistory() }, 500)
|
setTimeout(() => { clearHistory() }, 500)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getOS = ()=> {
|
|
||||||
if (navigator.userAgentData) {
|
|
||||||
// Modern method
|
|
||||||
return navigator.userAgentData.platform;
|
|
||||||
} else {
|
|
||||||
// Fallback for older browsers
|
|
||||||
const userAgent = navigator.userAgent.toLowerCase();
|
|
||||||
if (userAgent.indexOf("mac") > -1) return "macOS";
|
|
||||||
if (userAgent.indexOf("win") > -1) return "Windows";
|
|
||||||
if (userAgent.indexOf("linux") > -1) return "Linux";
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<FormEditorErrorHandler>
|
||||||
|
|
||||||
<div class="w-full flex grow overflow-y-scroll relative bg-gray-50">
|
<div class="w-full flex grow overflow-y-scroll relative bg-gray-50">
|
||||||
<div
|
<div
|
||||||
|
|
@ -116,6 +117,7 @@
|
||||||
@close="showFormErrorModal = false"
|
@close="showFormErrorModal = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</FormEditorErrorHandler>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
|
@ -141,10 +143,12 @@ import FormAccess from "./form-components/FormAccess.vue"
|
||||||
import { validatePropertiesLogic } from "~/composables/forms/validatePropertiesLogic.js"
|
import { validatePropertiesLogic } from "~/composables/forms/validatePropertiesLogic.js"
|
||||||
import opnformConfig from "~/opnform.config.js"
|
import opnformConfig from "~/opnform.config.js"
|
||||||
import { captureException } from "@sentry/core"
|
import { captureException } from "@sentry/core"
|
||||||
|
import FormEditorErrorHandler from '~/components/open/forms/components/FormEditorErrorHandler.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "FormEditor",
|
name: "FormEditor",
|
||||||
components: {
|
components: {
|
||||||
|
FormEditorErrorHandler,
|
||||||
UndoRedo,
|
UndoRedo,
|
||||||
FormEditorSidebar,
|
FormEditorSidebar,
|
||||||
FormEditorPreview,
|
FormEditorPreview,
|
||||||
|
|
@ -250,9 +254,6 @@ export default {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
helpUrl() {
|
|
||||||
return this.opnformConfig.links.help
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {},
|
watch: {},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<ErrorBoundary @on-error="onFormEditorError">
|
||||||
|
|
||||||
|
<template #error="{ error, clearError }">
|
||||||
|
<div class="flex-grow w-full flex items-center justify-center flex-col gap-4">
|
||||||
|
<h1 class="text-blue-800 text-2xl font-medium">Oops! Something went wrong.</h1>
|
||||||
|
<p class="text-gray-500 max-w-lg text-center">It looks like your last action caused an issue on our side. We
|
||||||
|
apologize for
|
||||||
|
the
|
||||||
|
inconvenience.</p>
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<UButton icon="i-material-symbols-undo" @click="clearEditorError(error, clearError)">Go back one step
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" icon="i-heroicons-chat-bubble-left-right-16-solid"
|
||||||
|
@click="onErrorContact(error)">
|
||||||
|
Report this error
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
const crisp = useCrisp()
|
||||||
|
const workingFormStore = useWorkingFormStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const form = storeToRefs(workingFormStore).content
|
||||||
|
const user = computed(() => authStore.user)
|
||||||
|
// Clear error and go back 1 step in history
|
||||||
|
const clearEditorError = (error, clearError) => {
|
||||||
|
crisp.enableChatbot()
|
||||||
|
workingFormStore.undo()
|
||||||
|
clearError()
|
||||||
|
}
|
||||||
|
const onFormEditorError = (error) => {
|
||||||
|
crisp.pauseChatBot()
|
||||||
|
const eventData = {
|
||||||
|
message: error.message,
|
||||||
|
// take first 200 characters
|
||||||
|
stack: error.stack.substring(0, 100)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
crisp.pushEvent('form-editor-error', eventData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to send event to crisp', e, eventData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onErrorContact = (error) => {
|
||||||
|
console.log('Contacting via crisp for an error', error)
|
||||||
|
crisp.pauseChatBot()
|
||||||
|
let errorReport = 'Hi there, I have a technical issue with the form editor.'
|
||||||
|
if (form.value.slug) {
|
||||||
|
errorReport += ` The form I am editing is: \`${form.value.slug}\`.`
|
||||||
|
}
|
||||||
|
errorReport += ` And here are technical details about the error: \`\`\`${error.stack}\`\`\``
|
||||||
|
try {
|
||||||
|
crisp.openAndShowChat(errorReport)
|
||||||
|
crisp.showMessage(`Hi there, we're very sorry to hear you experienced an issue with NoteForms.
|
||||||
|
We'll be in touch about it very soon! In the meantime, I recommend that you try going back one step, and save your changes.`, 2000)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Crisp error', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -98,6 +98,28 @@ export function useCrisp () {
|
||||||
crisp.session.setSegments(segments, overwrite)
|
crisp.session.setSegments(segments, overwrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send message as operator
|
||||||
|
function showMessage (message, delay = 500) {
|
||||||
|
if (!crisp)
|
||||||
|
return
|
||||||
|
setTimeout(() => {
|
||||||
|
crisp.message.show('text', message)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseChatBot () {
|
||||||
|
if (!crisp)
|
||||||
|
return
|
||||||
|
crisp.session.setData({ 'enum': 'pause_chatbot' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableChatbot () {
|
||||||
|
if (!crisp)
|
||||||
|
return
|
||||||
|
crisp.session.setData({ 'enum': 'start_chatbot' })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
crisp,
|
crisp,
|
||||||
onCrispInit,
|
onCrispInit,
|
||||||
|
|
@ -111,6 +133,9 @@ export function useCrisp () {
|
||||||
sendTextMessage,
|
sendTextMessage,
|
||||||
pushEvent,
|
pushEvent,
|
||||||
setSegments,
|
setSegments,
|
||||||
setUser
|
setUser,
|
||||||
|
pauseChatBot,
|
||||||
|
enableChatbot,
|
||||||
|
showMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col flex-grow">
|
||||||
<form-editor
|
<form-editor
|
||||||
v-if="(!formsLoading || form ) && !error "
|
v-if="(!formsLoading || form ) && !error "
|
||||||
ref="editor"
|
ref="editor"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap flex-col">
|
<div class="flex flex-wrap flex-col flex-grow">
|
||||||
<div key="2">
|
<div key="2"
|
||||||
|
class="w-full flex flex-grow flex-col"
|
||||||
|
>
|
||||||
<create-form-base-modal
|
<create-form-base-modal
|
||||||
:show="showInitialFormModal"
|
:show="showInitialFormModal"
|
||||||
@form-generated="formGenerated"
|
@form-generated="formGenerated"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue