Skip to content
39 changes: 12 additions & 27 deletions src/application/services/useNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface UseNoteComposableState {
/**
* Creates/updates the note
*/
save: (content: NoteContent, parentId: NoteId | undefined) => Promise<void>;
save: (content: NoteContent, parentId: NoteId | undefined, currentNoteId: NoteId | null) => Promise<void>;

/**
* Returns list of tools used in note
Expand Down Expand Up @@ -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<boolean>(false);

/**
* Note Title identifier
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
async function save(content: NoteContent, parentId: NoteId | undefined, currentNoteId: NoteId | null): Promise<void> {
if (note.value === null) {
throw new Error('Note is not loaded yet');
}
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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);
});

Expand Down
21 changes: 20 additions & 1 deletion src/application/services/useNoteEditor.ts
Original file line number Diff line number Diff line change
@@ -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<NoteId | null>;

/**
* Tools used in the note
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -148,7 +168,6 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption

const loadId = ++currentLoadId;

isEditorReady.value = false;
toolsUserConfigLoaded.value = false;

try {
Expand Down
3 changes: 2 additions & 1 deletion src/presentation/pages/HistoryVersion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const { noteTitle, save } = useNote({
const canEdit = ref(false);

const { isEditorReady, editorConfig } = useNoteEditor({
noteId,
noteTools: historyTools,
isDraftResolver: () => false,
noteContentResolver: () => historyContent.value,
Expand All @@ -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
Expand Down
15 changes: 11 additions & 4 deletions src/presentation/pages/Note.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -126,6 +126,7 @@ function redirectToNoteSettings(): void {
const { updateCover } = useNoteSettings();

const { isEditorReady, editorConfig } = useNoteEditor({
noteId,
noteTools,
isDraftResolver: () => noteId.value === null,
noteContentResolver: () => note.value?.content,
Expand Down Expand Up @@ -153,7 +154,13 @@ async function noteChanged(data: NoteContent): Promise<void> {
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
Expand All @@ -169,8 +176,8 @@ async function noteChanged(data: NoteContent): Promise<void> {
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);
}
}
}
Expand Down
Loading