diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 74342032..60ae4b6f 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -55,7 +55,7 @@ interface UseNoteComposableState { /** * Creates/updates the note */ - save: (content: NoteContent, parentId: NoteId | undefined) => Promise; + save: (content: NoteContent, parentId: NoteId | undefined, currentNoteId: NoteId | null) => Promise; /** * Returns list of tools used in note @@ -141,12 +141,6 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt const route = useRoute(); - /** - * Is there any note currently saving - * Used to prevent re-load note after draft is saved - */ - const isNoteSaving = ref(false); - /** * Note Title identifier */ @@ -202,6 +196,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt const response = await noteService.getNoteById(id); note.value = response.note; + lastUpdateContent.value = response.note.content; canEdit.value = response.accessRights.canEdit; noteTools.value = response.tools; parentNote.value = response.parentNote; @@ -244,8 +239,9 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt * Saves the note * @param content - Note content (Editor.js data) * @param parentId - Id of the parent note. If null, then it's a root note + * @param currentNoteId - Id of the current note */ - async function save(content: NoteContent, parentId: NoteId | undefined): Promise { + async function save(content: NoteContent, parentId: NoteId | undefined, currentNoteId: NoteId | null): Promise { if (note.value === null) { throw new Error('Note is not loaded yet'); } @@ -255,9 +251,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ const specifiedNoteTools = resolveToolsByContent(content); - isNoteSaving.value = true; - - if (currentId.value === null) { + if (currentNoteId === null) { /** * @todo try-catch domain errors */ @@ -285,15 +279,16 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ void getNoteHierarchy(noteCreated.id); } else { - await noteService.updateNoteContentAndTools(currentId.value, content, specifiedNoteTools); + await noteService.updateNoteContentAndTools(currentNoteId, content, specifiedNoteTools); } /** - * Store just saved content in memory + * Store just saved content in memory only if the current note hasn't changed + * This prevents race conditions when switching between notes quickly */ - lastUpdateContent.value = content; - - isNoteSaving.value = false; + if (currentId.value === currentNoteId) { + lastUpdateContent.value = content; + } } /** @@ -366,7 +361,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt } } - watch(currentId, (newId, prevId) => { + watch(currentId, (newId, _prevId) => { /** * One note is open, user clicks on "+" to create another new note * Clear existing note @@ -377,16 +372,6 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt return; } - const isDraftSaving = prevId === null && isNoteSaving.value; - - /** - * Case for newly created note, - * we don't need to re-load it - */ - if (isDraftSaving) { - return; - } - void load(newId); }); diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index 6074c91b..0de65c51 100644 --- a/src/application/services/useNoteEditor.ts +++ b/src/application/services/useNoteEditor.ts @@ -1,12 +1,19 @@ +import type { MaybeRefOrGetter } from 'vue'; import { type Ref, computed, ref, toValue, watch } from 'vue'; import { useAppState } from './useAppState'; import type EditorTool from '@/domain/entities/EditorTool'; +import type { NoteId } from '@/domain/entities/Note'; import { type NoteContent } from '@/domain/entities/Note'; import { editorToolsService } from '@/domain'; import type { EditorjsToolsConfig } from '@/domain/entities/EditorTool'; import { useI18n } from 'vue-i18n'; interface UseNoteEditorOptions { + /** + * Null for new note, id for reading existing note + */ + noteId: MaybeRefOrGetter; + /** * Tools used in the note */ @@ -83,6 +90,19 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption */ let currentLoadId = 0; + /** + * Reset editor state when the note changes + * Prevents showing the editor with stale tools or content + * from a previously opened note + */ + watch( + () => toValue(options.noteId), + () => { + isEditorReady.value = false; + }, + { immediate: true } + ); + /** * Combine note and user tools * Undefined when user or note is not loaded @@ -148,7 +168,6 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption const loadId = ++currentLoadId; - isEditorReady.value = false; toolsUserConfigLoaded.value = false; try { diff --git a/src/presentation/pages/HistoryVersion.vue b/src/presentation/pages/HistoryVersion.vue index 58ce0090..0d9fd743 100644 --- a/src/presentation/pages/HistoryVersion.vue +++ b/src/presentation/pages/HistoryVersion.vue @@ -83,6 +83,7 @@ const { noteTitle, save } = useNote({ const canEdit = ref(false); const { isEditorReady, editorConfig } = useNoteEditor({ + noteId, noteTools: historyTools, isDraftResolver: () => false, noteContentResolver: () => historyContent.value, @@ -104,7 +105,7 @@ async function useThisVersion() { const editorElement = editor.value ? editor.value.element : null; if (historyContent.value !== undefined) { - await save(historyContent.value, undefined); + await save(historyContent.value, undefined, props.noteId); /** * In case if we do not have note id, we can change its cover, and we need successful data for cover * We need to do it after saving in case of note creation diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index ce00bc7b..7a4c5ac3 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -68,7 +68,7 @@ import { computed, ref, toRef, watch } from 'vue'; import { Button, Editor, PageBlock, VerticalMenu, type VerticalMenuItem } from '@codexteam/ui/vue'; import useNote from '@/application/services/useNote'; import { useRoute, useRouter } from 'vue-router'; -import { NoteContent } from '@/domain/entities/Note'; +import { NoteContent, type NoteId } from '@/domain/entities/Note'; import { useHead } from 'unhead'; import { useI18n } from 'vue-i18n'; import { makeElementScreenshot } from '@/infrastructure/utils/screenshot'; @@ -126,6 +126,7 @@ function redirectToNoteSettings(): void { const { updateCover } = useNoteSettings(); const { isEditorReady, editorConfig } = useNoteEditor({ + noteId, noteTools, isDraftResolver: () => noteId.value === null, noteContentResolver: () => note.value?.content, @@ -153,7 +154,13 @@ async function noteChanged(data: NoteContent): Promise { const editorElement = editor.value ? editor.value.element : null; if (!isEmpty) { - await save(data, props.parentId); + /** + * Capture the current note id at the time of the call + * to avoid race conditions when fast switching between notes + */ + const noteIdAtCallTime = props.id; + + await save(data, props.parentId, noteIdAtCallTime); /** * In case if we do not have note id, we can change its cover, and we need successful data for cover * We need to do it after saving in case of note creation @@ -169,8 +176,8 @@ async function noteChanged(data: NoteContent): Promise { paddingTop: '100px', }); } - if (updatedNoteCover !== null && props.id !== null) { - await updateCover(props.id, updatedNoteCover); + if (updatedNoteCover !== null && noteIdAtCallTime !== null && noteIdAtCallTime === props.id) { + await updateCover(noteIdAtCallTime as NoteId, updatedNoteCover); } } }