feat: add discard and reload confirmation modal (#2154)

This commit is contained in:
William Guinaudie 2026-02-19 14:27:27 +01:00 committed by GitHub
parent 8144560dd7
commit bf8138ec3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 43 additions and 8 deletions

View File

@ -33,6 +33,7 @@
:enable-discard-shortcut="true"
:enable-mentions="true"
:mention-project-id="modelValue.projectId"
:storage-key="descriptionStorageKey"
@update:modelValue="saveWithDelay"
@save="save"
/>
@ -40,14 +41,15 @@
</template>
<script setup lang="ts">
import {ref, computed, watchEffect, onMounted, onBeforeUnmount} from 'vue'
import {ref, computed, watchEffect, onBeforeUnmount} from 'vue'
import {onBeforeRouteLeave} from 'vue-router'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import { clearEditorDraft } from '@/helpers/editorDraftStorage'
import type { ITask } from '@/modelTypes/ITask'
import { useTaskStore } from '@/stores/tasks'
export type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
@ -78,9 +80,7 @@ const loading = computed(() => taskStore.isLoading)
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
onMounted(() => {
window.addEventListener('beforeunload', save)
})
const descriptionStorageKey = computed(() => `task-description-${props.modelValue.id}`)
async function saveWithDelay() {
if (description.value === props.modelValue.description) {
@ -106,7 +106,6 @@ onBeforeUnmount(async () => {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
window.removeEventListener('beforeunload', save)
})
onBeforeRouteLeave(() => save())
@ -130,6 +129,9 @@ async function save() {
})
emit('update:modelValue', updated)
// Clear draft from localStorage when saved successfully
clearEditorDraft(descriptionStorageKey.value)
saved.value = true
setTimeout(() => {
saved.value = false

View File

@ -28,6 +28,7 @@
:class="{'disabled': !canWrite}"
:contenteditable="canWrite ? true : undefined"
:spellcheck="false"
@input="handleTitleInput"
@blur="save(($event.target as HTMLInputElement).textContent as string)"
@keydown.enter.prevent.stop="!$event.isComposing && ($event.target as HTMLInputElement).blur()"
@keydown.esc.prevent.stop="!$event.isComposing && cancel($event.target as HTMLInputElement)"
@ -64,7 +65,7 @@
</template>
<script setup lang="ts">
import {ref, computed} from 'vue'
import {ref, computed, onMounted, onBeforeUnmount, watch} from 'vue'
import {useRouter} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
@ -109,6 +110,36 @@ const saving = ref(false)
const showSavedMessage = ref(false)
// Track if title has unsaved changes
const titleHasChanges = ref(false)
function handleBeforeUnload(e: BeforeUnloadEvent) {
if (titleHasChanges.value) {
e.preventDefault()
// Modern browsers ignore custom messages but this is still required
e.returnValue = ''
return ''
}
}
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
// Reset titleHasChanges when the task changes
watch(() => props.task.id, () => {
titleHasChanges.value = false
})
function handleTitleInput(event: Event) {
const target = event.target as HTMLInputElement
titleHasChanges.value = target.textContent !== props.task.title
}
async function save(title: string) {
// We only want to save if the title was actually changed.
// so we only continue if the task title changed.
@ -123,6 +154,7 @@ async function save(title: string) {
title,
})
emit('update:task', newTask)
titleHasChanges.value = false
showSavedMessage.value = true
setTimeout(() => {
showSavedMessage.value = false
@ -134,6 +166,7 @@ async function save(title: string) {
async function cancel(element: HTMLInputElement) {
element.textContent = props.task.title
titleHasChanges.value = false
element.blur()
}
</script>