store content in local storage first and sync with backend on certain events

This commit is contained in:
Abhinav Raut
2025-12-23 01:05:57 +05:30
parent cfec05c2ae
commit 67ade61250
+82 -33
View File
@@ -1,5 +1,5 @@
import { ref, watch } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { watchDebounced, useStorage, useEventListener } from '@vueuse/core'
import { useConversationStore } from '@/stores/conversation'
import { MACRO_CONTEXT } from '@/constants/conversation'
import api from '@/api'
@@ -35,6 +35,7 @@ const validateAttachments = (attachments) => {
/**
* Composable for managing draft state and persistence
* Saves to localStorage immediately, syncs to backend on conversation switch/send/unload
* @param key - Reactive reference to current draft key
* @param uploadedFiles - Optional reactive reference to uploaded files array
*/
@@ -44,9 +45,57 @@ export function useDraftManager (key, uploadedFiles = null) {
const textContent = ref('')
const isLoading = ref(false)
const isDirty = ref(false)
const skipNextSave = ref(false)
const loadedAttachments = ref([])
const loadedMacroActions = ref([])
// Reactive localStorage for all drafts
const localDrafts = useStorage('libredesk_drafts', {})
/**
* Save draft to localStorage only
*/
const saveDraftLocal = (draftKey) => {
if (!draftKey) return
const macroActions = conversationStore.getMacro(MACRO_CONTEXT.REPLY)?.actions || []
const draftMeta = {}
if (macroActions.length > 0) draftMeta.macro_actions = macroActions
if (uploadedFiles?.value?.length > 0) draftMeta.attachments = uploadedFiles.value
localDrafts.value[draftKey] = { content: htmlContent.value, meta: draftMeta }
isDirty.value = true
}
/**
* Get draft from localStorage
*/
const getLocalDraft = (draftKey) => localDrafts.value[draftKey] || null
/**
* Remove draft from localStorage
*/
const removeLocalDraft = (draftKey) => {
if (localDrafts.value[draftKey]) {
delete localDrafts.value[draftKey]
}
}
/**
* Sync localStorage draft to backend
*/
const syncDraftToBackend = async (draftKey) => {
if (!draftKey || !isDirty.value) return
const localDraft = getLocalDraft(draftKey)
if (!localDraft) return
try {
await api.saveDraft(draftKey, localDraft)
isDirty.value = false
} catch (error) {
// Silent fail - will retry on next sync
}
}
/**
* Reset all draft state to initial values
*/
@@ -60,13 +109,22 @@ export function useDraftManager (key, uploadedFiles = null) {
}
/**
* Load draft from backend for a given key
* Load draft from backend
*/
const loadDraft = async (draftKey) => {
if (!draftKey) return
isLoading.value = true
isDirty.value = false
skipNextSave.value = true
try {
// Check if there's an unsynced localStorage draft (e.g., from page refresh)
const localDraft = getLocalDraft(draftKey)
if (localDraft) {
await api.saveDraft(draftKey, localDraft)
removeLocalDraft(draftKey)
}
// Load from backend (source of truth)
const response = await api.getDraft(draftKey)
const draft = response.data.data
htmlContent.value = draft.content || ''
@@ -81,37 +139,17 @@ export function useDraftManager (key, uploadedFiles = null) {
}
/**
* Save draft to backend
*/
const saveDraft = async (draftKey) => {
if (!draftKey || isLoading.value || !isDirty.value) return
try {
const macroActions = conversationStore.getMacro(MACRO_CONTEXT.REPLY)?.actions || []
const draftMeta = {}
if (macroActions.length > 0) {
draftMeta.macro_actions = macroActions
}
if (uploadedFiles?.value?.length > 0) {
draftMeta.attachments = uploadedFiles.value
}
await api.saveDraft(draftKey, { content: htmlContent.value, meta: draftMeta })
isDirty.value = false
} catch (error) {
// Silent fail for drafts
}
}
/**
* Clear draft and local state
* Clear draft from both localStorage and backend
*/
const clearDraft = async (draftKey) => {
if (!draftKey) return
removeLocalDraft(draftKey)
isLoading.value = true
try {
await api.deleteDraft(draftKey)
resetState()
} catch (error) {
// Silent fail for drafts
// Silent fail
} finally {
isLoading.value = false
}
@@ -124,16 +162,17 @@ export function useDraftManager (key, uploadedFiles = null) {
return textContent.value?.trim() !== ''
}
// Watch for key changes to save / load draft
// Watch for key changes - sync to backend before switching
watch(
key,
async (newKey, oldKey) => {
// Save old draft if dirty
// Sync old draft to backend before switching
if (newKey !== oldKey && isDirty.value && hasDraftContent()) {
await saveDraft(oldKey)
await syncDraftToBackend(oldKey)
removeLocalDraft(oldKey)
}
// Load new draft or clear state
// Load new draft from backend
if (newKey && newKey !== oldKey) {
await loadDraft(newKey)
} else if (!newKey && oldKey) {
@@ -143,7 +182,7 @@ export function useDraftManager (key, uploadedFiles = null) {
{ immediate: true }
)
// Auto-save draft when content, macro, or uploaded files change (debounced)
// Debounced watcher - save to localStorage only
const watchSources = [
htmlContent,
textContent,
@@ -155,15 +194,25 @@ export function useDraftManager (key, uploadedFiles = null) {
watchDebounced(
watchSources,
async () => {
() => {
if (skipNextSave.value) {
skipNextSave.value = false
return
}
if (!isLoading.value && key.value) {
isDirty.value = true
await saveDraft(key.value)
saveDraftLocal(key.value)
}
},
{ debounce: 250, deep: true }
)
// Sync to backend when page is hidden (tab switch)
useEventListener(document, 'visibilitychange', async () => {
if (document.visibilityState === 'hidden' && isDirty.value && key.value) {
await syncDraftToBackend(key.value)
}
})
return {
htmlContent,
textContent,