From 9224b1dabcb54a0b06a3144b2e5ca1cd6b87b789 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Mon, 29 Jun 2026 21:26:49 +0300 Subject: [PATCH 01/11] fix/fix the note id value --- src/application/services/useNote.ts | 9 +++++---- src/presentation/pages/Note.vue | 14 ++++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 74342032..30892b39 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 @@ -244,8 +244,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'); } @@ -257,7 +258,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt isNoteSaving.value = true; - if (currentId.value === null) { + if (currentNoteId === null) { /** * @todo try-catch domain errors */ @@ -285,7 +286,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ void getNoteHierarchy(noteCreated.id); } else { - await noteService.updateNoteContentAndTools(currentId.value, content, specifiedNoteTools); + await noteService.updateNoteContentAndTools(currentNoteId as NoteId, content, specifiedNoteTools); } /** diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index ce00bc7b..2c0a4654 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'; @@ -153,7 +153,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 +175,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) { + await updateCover(noteIdAtCallTime as NoteId, updatedNoteCover); } } } From a3495dfbe5cb96580d0a30ecb2a50b1463d6f775 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Mon, 27 Jul 2026 21:44:10 +0300 Subject: [PATCH 02/11] feat: asign lastUpdateContent on load --- src/application/services/useNote.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 30892b39..b1a8c81f 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -202,6 +202,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; @@ -290,9 +291,12 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt } /** - * 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; + if (currentId.value === currentNoteId) { + lastUpdateContent.value = content; + } isNoteSaving.value = false; } From 035ca0ac97378d34a5f4b9af5960a13a254795d7 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 28 Jul 2026 17:19:04 +0300 Subject: [PATCH 03/11] chore: lint --- src/application/services/useNote.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index b1a8c81f..e442e076 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -287,7 +287,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ void getNoteHierarchy(noteCreated.id); } else { - await noteService.updateNoteContentAndTools(currentNoteId as NoteId, content, specifiedNoteTools); + await noteService.updateNoteContentAndTools(currentNoteId, content, specifiedNoteTools); } /** From de354cd0189b21dcd8dc55477974060ae1a79709 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 4 Aug 2026 22:30:34 +0300 Subject: [PATCH 04/11] merge from fix/note-content-overrides-when-switching-between-notes --- src/application/services/useNote.ts | 22 +--------------------- src/presentation/pages/Note.vue | 1 + 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 74342032..39bd9874 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -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 */ @@ -255,8 +249,6 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ const specifiedNoteTools = resolveToolsByContent(content); - isNoteSaving.value = true; - if (currentId.value === null) { /** * @todo try-catch domain errors @@ -292,8 +284,6 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt * Store just saved content in memory */ lastUpdateContent.value = content; - - isNoteSaving.value = false; } /** @@ -366,7 +356,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 +367,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/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index ce00bc7b..5b94de8a 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -54,6 +54,7 @@ From 29a5bf67d6a5609dcb0bb7e6636a2063566aaa88 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 4 Aug 2026 22:47:30 +0300 Subject: [PATCH 05/11] feat: add guard to prevent race conditional --- src/presentation/pages/Note.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index 118eafdd..0725b5bb 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -176,7 +176,7 @@ async function noteChanged(data: NoteContent): Promise { paddingTop: '100px', }); } - if (updatedNoteCover !== null && noteIdAtCallTime !== null) { + if (updatedNoteCover !== null && noteIdAtCallTime !== null && noteIdAtCallTime === props.id) { await updateCover(noteIdAtCallTime as NoteId, updatedNoteCover); } } From 0700c87fac04f547852102dce7529a16f00d51ca Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Wed, 5 Aug 2026 17:20:44 +0300 Subject: [PATCH 06/11] refactor: reset isEditorReady flag on note id change --- src/application/services/useNoteEditor.ts | 1 - src/presentation/pages/Note.vue | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index 6074c91b..2ad29871 100644 --- a/src/application/services/useNoteEditor.ts +++ b/src/application/services/useNoteEditor.ts @@ -148,7 +148,6 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption const loadId = ++currentLoadId; - isEditorReady.value = false; toolsUserConfigLoaded.value = false; try { diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index 0725b5bb..98b377ea 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -217,6 +217,8 @@ const verticalMenuItems = computed(() => { watch( () => props.id, () => { + isEditorReady.value = false; + /** If new child note is created, refresh editor with empty data */ if (props.id === null) { useHead({ From 6d9d3e08216cd693cc8694ba07c923ec87df24a3 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 11 Aug 2026 13:43:28 +0300 Subject: [PATCH 07/11] refactor: move isEditorReady reset into useNoteEditor --- src/application/services/useNoteEditor.ts | 20 ++++++++++++++++++++ src/presentation/pages/Note.vue | 4 +--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index 2ad29871..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 diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index 98b377ea..7a4c5ac3 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -54,7 +54,6 @@ @@ -127,6 +126,7 @@ function redirectToNoteSettings(): void { const { updateCover } = useNoteSettings(); const { isEditorReady, editorConfig } = useNoteEditor({ + noteId, noteTools, isDraftResolver: () => noteId.value === null, noteContentResolver: () => note.value?.content, @@ -217,8 +217,6 @@ const verticalMenuItems = computed(() => { watch( () => props.id, () => { - isEditorReady.value = false; - /** If new child note is created, refresh editor with empty data */ if (props.id === null) { useHead({ From 5992c3fb9ac153a174f6cf6ad35d100d084dcf6c Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 11 Aug 2026 15:17:23 +0300 Subject: [PATCH 08/11] chore: update save() arguments in HistoryVersion --- src/presentation/pages/HistoryVersion.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From b664171aae2b585ef98b0be55a55866c2ca31ed6 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Tue, 18 Aug 2026 11:16:40 +0300 Subject: [PATCH 09/11] feat: add counter for load() to prevent race condition --- src/application/services/useNote.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 60ae4b6f..25992576 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -141,6 +141,13 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt const route = useRoute(); + /** + * Incremented on each new load request to discard stale async results + * Prevents race conditions when rapidly switching between notes causes + * multiple concurrent load() invocations to resolve out of order + */ + let currentLoadId = 0; + /** * Note Title identifier */ @@ -192,9 +199,19 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt * @param id - Note identifier got from composable argument */ async function load(id: NoteId): Promise { + const loadId = ++currentLoadId; + try { const response = await noteService.getNoteById(id); + /** + * If a newer load request has superseded this one — discard stale results + * to prevent mismatched content/tools state when switching notes quickly + */ + if (loadId !== currentLoadId) { + return; + } + note.value = response.note; lastUpdateContent.value = response.note.content; canEdit.value = response.accessRights.canEdit; From feba7b6ca29235c451bb9de3e27ffdc4c5c4842d Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Wed, 19 Aug 2026 17:27:25 +0300 Subject: [PATCH 10/11] fix: prevent editor updating after saving new note --- src/application/services/useNote.ts | 35 +++++++++++++++++++++++ src/application/services/useNoteEditor.ts | 24 +++++++++++++--- src/presentation/pages/Note.vue | 3 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 25992576..45c51296 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -96,6 +96,13 @@ interface UseNoteComposableState { * Note hierarchy */ noteHierarchy: Ref; + + /** + * Returns the id of the note created by the most recent save() on a new note + * Used to distinguish "same note just got an id after save" from + * "switched to a different existing note" + */ + getLastCreatedNoteId: () => NoteId | null; } interface UseNoteComposableOptions { @@ -184,6 +191,12 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ const noteHierarchy = ref(null); + /** + * Id of the note created by the most recent save() on a new note + * Used to skip the reload after save so the editor doesn't get recreated + */ + let lastCreatedNoteId: NoteId | null = null; + /** * get note hierarchy * @param id - note id @@ -274,6 +287,19 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ const noteCreated = await noteService.createNote(content, specifiedNoteTools, parentId); + /** + * Remember the created note id so the editor can avoid + * recreating itself when the route switches from "new note" to the newly created note id + */ + lastCreatedNoteId = noteCreated.id; + + /** + * Store the saved content so the navbar title reflects it + */ + if (currentId.value === currentNoteId) { + lastUpdateContent.value = content; + } + /** * Replace the current route with note id */ @@ -389,6 +415,14 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt return; } + /** + * If the note was just created via save() and is still a draft (no id yet), + * skip the reload to avoid recreating the editor with the same content. + */ + if (newId === lastCreatedNoteId && note.value !== null && !('id' in note.value)) { + return; + } + void load(newId); }); @@ -416,5 +450,6 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt noteParents, parentNote, noteHierarchy, + getLastCreatedNoteId: () => lastCreatedNoteId, }; } diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index 0de65c51..1bd31031 100644 --- a/src/application/services/useNoteEditor.ts +++ b/src/application/services/useNoteEditor.ts @@ -34,6 +34,13 @@ interface UseNoteEditorOptions { * Flag indicating that user can edit the note */ canEdit: Ref; + + /** + * Returns the id of the note created by the most recent save() on a new note + * Used to distinguish "same note just got an id after save" from + * "switched to a different existing note" + */ + getLastCreatedNoteId?: () => NoteId | null; } interface UseNoteEditorComposableState { @@ -91,13 +98,22 @@ 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 + * Reset the editor when the note changes. + + * Exception — new note save: the route switches from null to createdId + * for the same note, so the editor must NOT be recreated + * (avoids blinking and losing the cursor). */ watch( () => toValue(options.noteId), - () => { + (newId, oldId) => { + /** + * Same note just got an id after save — keep the editor as-is + */ + if (oldId === null && newId !== null && options.getLastCreatedNoteId !== undefined && newId === options.getLastCreatedNoteId()) { + return; + } + isEditorReady.value = false; }, { immediate: true } diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index 7a4c5ac3..3570a734 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -99,7 +99,7 @@ const props = defineProps<{ const noteId = toRef(props, 'id'); -const { note, noteTools, save, noteTitle, canEdit, noteParents, noteHierarchy } = useNote({ +const { note, noteTools, save, noteTitle, canEdit, noteParents, noteHierarchy, getLastCreatedNoteId } = useNote({ id: noteId, }); @@ -131,6 +131,7 @@ const { isEditorReady, editorConfig } = useNoteEditor({ isDraftResolver: () => noteId.value === null, noteContentResolver: () => note.value?.content, canEdit, + getLastCreatedNoteId, }); /** From 897291c8a3c02dc1b8c0ebf4a3f0505358c46265 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Wed, 19 Aug 2026 17:30:39 +0300 Subject: [PATCH 11/11] chore: lint fix --- src/application/services/useNoteEditor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index 1bd31031..eaaeea4b 100644 --- a/src/application/services/useNoteEditor.ts +++ b/src/application/services/useNoteEditor.ts @@ -99,7 +99,6 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption /** * Reset the editor when the note changes. - * Exception — new note save: the route switches from null to createdId * for the same note, so the editor must NOT be recreated * (avoids blinking and losing the cursor).