diff --git a/@codexteam/ui/dev/pages/components/Editor.vue b/@codexteam/ui/dev/pages/components/Editor.vue index c1680cdc..40c9a82a 100644 --- a/@codexteam/ui/dev/pages/components/Editor.vue +++ b/@codexteam/ui/dev/pages/components/Editor.vue @@ -40,7 +40,7 @@ placeholder="Write something or press / to select a tool" first-block-placeholder="Untitled" autofocus - :inlineToolbar="true" + :inline-toolbar="true" /> diff --git a/@codexteam/ui/package.json b/@codexteam/ui/package.json index 50db201b..2550ce0f 100644 --- a/@codexteam/ui/package.json +++ b/@codexteam/ui/package.json @@ -1,6 +1,6 @@ { "name": "@codexteam/ui", - "version": "0.2.3", + "version": "0.2.5", "type": "module", "sideEffects": [ "*.css", diff --git a/@codexteam/ui/src/vue/components/editor/useEditor.ts b/@codexteam/ui/src/vue/components/editor/useEditor.ts index e9d0b489..8fb7c533 100644 --- a/@codexteam/ui/src/vue/components/editor/useEditor.ts +++ b/@codexteam/ui/src/vue/components/editor/useEditor.ts @@ -126,8 +126,7 @@ export function useEditor(editorConfig: MaybeRefOrGetter, options: * Destroy editor instance after unmount */ onBeforeUnmount(() => { - editor?.destroy(); - editor = undefined; + destroyEditor(); }); return { diff --git a/src/application/services/useNote.ts b/src/application/services/useNote.ts index 74342032..75ba92d5 100644 --- a/src/application/services/useNote.ts +++ b/src/application/services/useNote.ts @@ -131,8 +131,9 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt /** * List of tools used in the note * Undefined when note is not loaded yet + * Empty array for drafts since they have no note tools */ - const noteTools = ref(undefined); + const noteTools = ref(currentId.value === null ? [] : undefined); /** * Router instance used to replace the current route with note id @@ -334,6 +335,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt */ function resetNote(): void { note.value = createDraft(); + noteTools.value = []; canEdit.value = true; lastUpdateContent.value = null; noteHierarchy.value = null; diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index 1d5b4ad5..a5fb4abc 100644 --- a/src/application/services/useNoteEditor.ts +++ b/src/application/services/useNoteEditor.ts @@ -3,7 +3,7 @@ import { useAppState } from './useAppState'; import type EditorTool from '@/domain/entities/EditorTool'; import { type NoteContent } from '@/domain/entities/Note'; import { editorToolsService } from '@/domain'; -import type { EditorjsConfigTool } from '@/domain/entities/EditorTool'; +import type { EditorjsToolsConfig } from '@/domain/entities/EditorTool'; import { useI18n } from 'vue-i18n'; interface UseNoteEditorOptions { @@ -17,12 +17,6 @@ interface UseNoteEditorOptions { */ noteContentResolver: () => NoteContent | undefined; - /** - * Function to check if the note is a draft - * In draft we wont wait for note tools loading - */ - isDraftResolver: () => boolean; - /** * Flag indicating that user can edit the note */ @@ -61,14 +55,16 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption /** * Reactive object with editor tools installed by user + * User is undefined while authorization is in progress, + * null when user is not authenticated, User instance otherwise */ - const { userEditorTools } = useAppState(); + const { userEditorTools, user } = useAppState(); /** * Loaded tools classes by grouped by tool.name * Undefined when tools are not loaded yet */ - let toolsUserConfig: Record | undefined = undefined; + let toolsUserConfig: EditorjsToolsConfig | undefined = undefined; /** * We can't make toolsUserConfig reactive since it contains excecutable js-classes, Vue can't handle that. @@ -76,36 +72,55 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption */ const toolsUserConfigLoaded = ref(false); + /** + * Incremented on each new load request to discard stale async results. + * Prevents race conditions when rapid note switching causes multiple + * concurrent loadToolsScripts invocations. + */ + let currentLoadId = 0; + /** * Combine note and user tools - * Undefined when user or note is not loaded + * Returns undefined when tools are not loaded yet to prevent + * premature editor rendering with an incomplete tools set */ const noteAndUserTools = computed(() => { - const isDraft = options.isDraftResolver(); - const noteTools = isDraft ? [] : toValue(options.noteTools); - const userTools = toValue(userEditorTools) ?? []; + const noteTools = toValue(options.noteTools); + const userTools = toValue(userEditorTools); + const currentUser = toValue(user); /** - * If tools are not loaded yet, return undefined + * If note tools are not loaded yet, return undefined to prevent + * premature editor rendering */ if (noteTools === undefined) { return undefined; } + /** + * If user is authenticated but their tools are not loaded yet, wait for them to load + * When user is not authenticated userTools stays undefined + */ + if (currentUser !== null && userTools === undefined) { + return undefined; + } + /** * Return unique array of tools grouped by tool.name */ - const combinedTools = [...noteTools, ...userTools]; + const combinedTools = [...noteTools, ...(userTools ?? [])]; const uniqueTools = new Map(combinedTools.map(tool => [tool.name, tool])); return Array.from(uniqueTools.values()); }); /** - * Downloads passed tools scripts and toggles-on the isEditorReady flag + * Downloads passed tools scripts and returns the loaded config object. + * Does not mutate shared state — the caller is responsible for applying the result * @param toolsConfigs - tools to download + * @returns loaded tools config */ - async function loadToolsScripts(toolsConfigs: EditorTool[]): Promise { + async function loadToolsScripts(toolsConfigs: EditorTool[]): Promise { const loadedTools = await editorToolsService.getToolsLoaded(toolsConfigs); /** @@ -114,7 +129,7 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption */ const loadedToolsWithoutParagraph = loadedTools.filter(tool => tool.tool.name !== 'paragraph'); - toolsUserConfig = Object.fromEntries( + return Object.fromEntries( loadedToolsWithoutParagraph .map(toolClassAndInfo => [ toolClassAndInfo.tool.name, @@ -124,12 +139,6 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption }, ]) ); - toolsUserConfigLoaded.value = true; - - /** - * Now all tools are loaded, we're ready to use the editor - */ - isEditorReady.value = true; } /** @@ -144,7 +153,35 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption return; } - await loadToolsScripts(tools); + const loadId = ++currentLoadId; + + isEditorReady.value = false; + toolsUserConfigLoaded.value = false; + + try { + const loadedConfig = await loadToolsScripts(tools); + + /** + * If a newer load request has superseded this one — discard stale results + * to prevent overwriting state with tools from a previous note. + */ + if (loadId !== currentLoadId) { + return; + } + + toolsUserConfig = loadedConfig; + toolsUserConfigLoaded.value = true; + } catch (error) { + throw new Error(`Failed to load tools scripts: ${error instanceof Error ? error.message : String(error)}`); + } finally { + /** + * Display the editor regardless of tool loading failures, as it can be displayed with default tools. + * Only the latest load request may mark the editor as ready + */ + if (loadId === currentLoadId) { + isEditorReady.value = true; + } + } }, { immediate: true, // load tools if they are passed to the composable immediately }); diff --git a/src/domain/entities/EditorTool.ts b/src/domain/entities/EditorTool.ts index 12f44f60..16bd55c9 100644 --- a/src/domain/entities/EditorTool.ts +++ b/src/domain/entities/EditorTool.ts @@ -78,6 +78,11 @@ export type NewToolData = Omit & { */ export type EditorjsConfigTool = ToolSettings | ToolConstructable; +/** + * Editor.js tools config — map of tool name to its class and inline toolbar flag + */ +export type EditorjsToolsConfig = Record; + /** * Editor tool info alogn with its plugin's class ready to use */ diff --git a/src/presentation/pages/HistoryVersion.vue b/src/presentation/pages/HistoryVersion.vue index 58ce0090..c0a32343 100644 --- a/src/presentation/pages/HistoryVersion.vue +++ b/src/presentation/pages/HistoryVersion.vue @@ -84,7 +84,6 @@ const canEdit = ref(false); const { isEditorReady, editorConfig } = useNoteEditor({ noteTools: historyTools, - isDraftResolver: () => false, noteContentResolver: () => historyContent.value, canEdit, }); diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index ce00bc7b..d04c68a5 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -127,7 +127,6 @@ const { updateCover } = useNoteSettings(); const { isEditorReady, editorConfig } = useNoteEditor({ noteTools, - isDraftResolver: () => noteId.value === null, noteContentResolver: () => note.value?.content, canEdit, });