feat(editor): automatically save draft comments locally (#1868)

Resolves https://github.com/go-vikunja/vikunja/issues/1867
This commit is contained in:
kolaente
2025-11-24 23:23:58 +01:00
committed by GitHub
parent 8bcd7ec5f5
commit 719d06a991
3 changed files with 105 additions and 1 deletions

View File

@@ -183,6 +183,7 @@ import XButton from '@/components/input/Button.vue'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
import {saveEditorDraft, loadEditorDraft, clearEditorDraft} from '@/helpers/editorDraftStorage'
const props = withDefaults(defineProps<{
modelValue: string,
@@ -195,6 +196,7 @@ const props = withDefaults(defineProps<{
enableDiscardShortcut?: boolean,
enableMentions?: boolean,
mentionProjectId?: number,
storageKey?: string,
}>(), {
uploadCallback: undefined,
isEditEnabled: true,
@@ -205,6 +207,7 @@ const props = withDefaults(defineProps<{
enableDiscardShortcut: false,
enableMentions: false,
mentionProjectId: 0,
storageKey: '',
})
const emit = defineEmits(['update:modelValue', 'save'])
@@ -571,12 +574,25 @@ function bubbleNow() {
}
contentHasChanged.value = true
emit('update:modelValue', editor.value?.getHTML())
const newContent = editor.value?.getHTML()
// Save to localStorage if storageKey is provided
if (props.storageKey) {
saveEditorDraft(props.storageKey, newContent || '')
}
emit('update:modelValue', newContent)
}
function bubbleSave() {
bubbleNow()
lastSavedState = editor.value?.getHTML() ?? ''
// Clear draft from localStorage when saved
if (props.storageKey) {
clearEditorDraft(props.storageKey)
}
emit('save', lastSavedState)
if (isEditing.value) {
internalMode.value = 'preview'
@@ -585,6 +601,12 @@ function bubbleSave() {
function exitEditMode() {
editor.value?.commands.setContent(lastSavedState, {emitUpdate: false})
// Clear draft from localStorage when discarding changes
if (props.storageKey) {
clearEditorDraft(props.storageKey)
}
if (isEditing.value) {
internalMode.value = 'preview'
}
@@ -680,6 +702,20 @@ onMounted(async () => {
await nextTick()
// Load draft from localStorage if available
if (props.storageKey) {
const draft = loadEditorDraft(props.storageKey)
if (draft && isEditorContentEmpty(props.modelValue)) {
// Only load draft if current content is empty
// Set content and force edit mode for immediate editing
editor.value?.commands.setContent(draft, {emitUpdate: false})
internalMode.value = 'edit'
// Emit the model update so parent sees the restored content
emit('update:modelValue', draft)
return
}
}
setModeAndValue(props.modelValue)
})

View File

@@ -172,6 +172,7 @@
:placeholder="$t('task.comment.placeholder')"
:enable-mentions="true"
:mention-project-id="projectId"
:storage-key="commentStorageKey"
@save="addComment()"
/>
</div>
@@ -226,6 +227,7 @@ import type {ITask} from '@/modelTypes/ITask'
import {uploadFile} from '@/helpers/attachments'
import {success} from '@/message'
import {formatDateLong, formatDisplayDate} from '@/helpers/time/formatDate'
import {clearEditorDraft} from '@/helpers/editorDraftStorage'
import {fetchAvatarBlobUrl, getDisplayName} from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
import {useConfigStore} from '@/stores/config'
@@ -299,6 +301,7 @@ const actions = computed(() => {
})
const frontendUrl = computed(() => configStore.frontendUrl)
const commentStorageKey = computed(() => `task-comment-${props.taskId}`)
const currentPage = ref(1)
@@ -377,6 +380,10 @@ async function addComment() {
const comment = await taskCommentService.create(newComment)
comments.value.push(comment)
newCommentText.value = ''
// Ensure draft is cleared from localStorage
clearEditorDraft(commentStorageKey.value)
success({message: t('task.comment.addedSuccess')})
} finally {
creating.value = false

View File

@@ -0,0 +1,61 @@
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const STORAGE_KEY_PREFIX = 'editorDraft'
/**
* Save editor content to local storage
*/
export function saveEditorDraft(storageKey: string, content: string) {
if (!storageKey) {
return
}
const key = `${STORAGE_KEY_PREFIX}-${storageKey}`
try {
if (!content || isEditorContentEmpty(content)) {
// Remove empty drafts
localStorage.removeItem(key)
return
}
localStorage.setItem(key, content)
} catch (error) {
console.warn('Failed to save editor draft:', error)
}
}
/**
* Load editor content from local storage
*/
export function loadEditorDraft(storageKey: string): string | null {
if (!storageKey) {
return null
}
const key = `${STORAGE_KEY_PREFIX}-${storageKey}`
try {
return localStorage.getItem(key)
} catch (error) {
console.warn('Failed to load editor draft:', error)
return null
}
}
/**
* Clear editor content from local storage
*/
export function clearEditorDraft(storageKey: string) {
if (!storageKey) {
return
}
const key = `${STORAGE_KEY_PREFIX}-${storageKey}`
try {
localStorage.removeItem(key)
} catch (error) {
console.warn('Failed to clear editor draft:', error)
}
}