From db3aedf39de14c29a9b72b5803d9bc208863a531 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 24 Dec 2025 16:36:34 +0200 Subject: [PATCH] fix(note_detail): spaced update sometimes overwrites when note type is changed --- apps/client/src/widgets/react/hooks.tsx | 10 +++--- .../src/widgets/type_widgets/AiChat.tsx | 4 ++- .../src/widgets/type_widgets/MindMap.tsx | 31 ++++++++++--------- .../type_widgets/canvas/persistence.ts | 18 ++++++----- .../src/widgets/type_widgets/code/Code.tsx | 1 + .../type_widgets/relation_map/RelationMap.tsx | 1 + .../type_widgets/text/EditableText.tsx | 31 ++++++++++--------- 7 files changed, 55 insertions(+), 41 deletions(-) diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 39d02911e..df951df4b 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1,5 +1,5 @@ import { CKTextEditor } from "@triliumnext/ckeditor5"; -import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons"; +import { FilterLabelsByType, KeyboardActionNames, NoteType, OptionNames, RelationNames } from "@triliumnext/commons"; import { Tooltip } from "bootstrap"; import Mark from "mark.js"; import { RefObject, VNode } from "preact"; @@ -94,7 +94,8 @@ export interface SavedData { }[]; } -export function useEditorSpacedUpdate({ note, noteContext, getData, onContentChange, dataSaved, updateInterval }: { +export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval }: { + noteType: NoteType; note: FNote, noteContext: NoteContext | null | undefined, getData: () => Promise | SavedData | undefined, @@ -110,15 +111,16 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha const data = await getData(); // for read only notes - if (data === undefined) return; + if (data === undefined || note.type !== noteType) return; protected_session_holder.touchProtectedSessionIfNecessary(note); + await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId); noteSavedDataStore.set(note.noteId, data.content); dataSaved?.(data); }; - }, [ note, getData, dataSaved ]); + }, [ note, getData, dataSaved, noteType, parentComponent ]); const spacedUpdate = useSpacedUpdate(callback); // React to note/blob changes. diff --git a/apps/client/src/widgets/type_widgets/AiChat.tsx b/apps/client/src/widgets/type_widgets/AiChat.tsx index 677d593ab..88c5e9c82 100644 --- a/apps/client/src/widgets/type_widgets/AiChat.tsx +++ b/apps/client/src/widgets/type_widgets/AiChat.tsx @@ -1,13 +1,15 @@ import { useEffect, useRef } from "preact/hooks"; + +import LlmChatPanel from "../llm_chat"; import { useEditorSpacedUpdate, useLegacyWidget } from "../react/hooks"; import { type TypeWidgetProps } from "./type_widget"; -import LlmChatPanel from "../llm_chat"; export default function AiChat({ note, noteContext }: TypeWidgetProps) { const dataRef = useRef(); const spacedUpdate = useEditorSpacedUpdate({ note, noteContext, + noteType: "aiChat", getData: async () => ({ content: JSON.stringify(dataRef.current) }), diff --git a/apps/client/src/widgets/type_widgets/MindMap.tsx b/apps/client/src/widgets/type_widgets/MindMap.tsx index a76c26bf3..3b5631338 100644 --- a/apps/client/src/widgets/type_widgets/MindMap.tsx +++ b/apps/client/src/widgets/type_widgets/MindMap.tsx @@ -1,17 +1,19 @@ -import { useCallback, useEffect, useRef } from "preact/hooks"; -import { TypeWidgetProps } from "./type_widget"; -import { MindElixirData, MindElixirInstance, Operation, Options, default as VanillaMindElixir } from "mind-elixir"; -import { HTMLAttributes, RefObject } from "preact"; -// allow node-menu plugin css to be bundled by webpack -import nodeMenu from "@mind-elixir/node-menu"; import "mind-elixir/style"; import "@mind-elixir/node-menu/dist/style.css"; import "./MindMap.css"; + +// allow node-menu plugin css to be bundled by webpack +import nodeMenu from "@mind-elixir/node-menu"; +import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons"; +import { snapdom } from "@zumer/snapdom"; +import { default as VanillaMindElixir,MindElixirData, MindElixirInstance, Operation, Options } from "mind-elixir"; +import { HTMLAttributes, RefObject } from "preact"; +import { useCallback, useEffect, useRef } from "preact/hooks"; + +import utils from "../../services/utils"; import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks"; import { refToJQuerySelector } from "../react/react_utils"; -import utils from "../../services/utils"; -import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons"; -import { snapdom, SnapdomOptions } from "@zumer/snapdom"; +import { TypeWidgetProps } from "./type_widget"; const NEW_TOPIC_NAME = ""; @@ -50,6 +52,7 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { const spacedUpdate = useEditorSpacedUpdate({ note, + noteType: "mindMap", noteContext, getData: async () => { if (!apiRef.current) return; @@ -75,7 +78,7 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { position: 0 } ] - } + }; }, onContentChange: (content) => { let newContent: MindElixirData; @@ -87,7 +90,7 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { console.debug("Wrong JSON content: ", content); } } else { - newContent = VanillaMindElixir.new(NEW_TOPIC_NAME) + newContent = VanillaMindElixir.new(NEW_TOPIC_NAME); } apiRef.current?.init(newContent!); } @@ -138,7 +141,7 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) { onKeyDown }} /> - ) + ); } function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) { @@ -190,7 +193,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef if (operation.name !== "beginEdit") { onChange(); } - } + }; bus.addListener("operation", operationListener); bus.addListener("changeDirection", onChange); @@ -203,5 +206,5 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef return (
- ) + ); } diff --git a/apps/client/src/widgets/type_widgets/canvas/persistence.ts b/apps/client/src/widgets/type_widgets/canvas/persistence.ts index 9a4172ecb..28c10e752 100644 --- a/apps/client/src/widgets/type_widgets/canvas/persistence.ts +++ b/apps/client/src/widgets/type_widgets/canvas/persistence.ts @@ -1,12 +1,13 @@ +import { exportToSvg, getSceneVersion } from "@excalidraw/excalidraw"; +import { ExcalidrawElement, NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types"; +import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem } from "@excalidraw/excalidraw/types"; import { RefObject } from "preact"; +import { useRef } from "preact/hooks"; + import NoteContext from "../../../components/note_context"; import FNote from "../../../entities/fnote"; -import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem } from "@excalidraw/excalidraw/types"; -import { useRef } from "preact/hooks"; -import { SavedData, useEditorSpacedUpdate } from "../../react/hooks"; -import { ExcalidrawElement, NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types"; -import { exportToSvg, getSceneVersion } from "@excalidraw/excalidraw"; import server from "../../../services/server"; +import { SavedData, useEditorSpacedUpdate } from "../../react/hooks"; interface AttachmentMetadata { title: string; @@ -39,6 +40,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte const spacedUpdate = useEditorSpacedUpdate({ note, noteContext, + noteType: "canvas", onContentChange(newContent) { const api = apiRef.current; if (!api) return; @@ -66,7 +68,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte // load the library state loadLibrary(note).then(({ libraryItems, metadata }) => { // Update the library and save to independent variables - api.updateLibrary({ libraryItems: libraryItems, merge: false }); + api.updateLibrary({ libraryItems, merge: false }); // save state of library to compare it to the new state later. libraryCache.current = libraryItems; @@ -158,7 +160,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte spacedUpdate.resetUpdateTimer(); spacedUpdate.scheduleUpdate(); } - } + }; } async function getData(api: ExcalidrawImperativeAPI) { @@ -202,7 +204,7 @@ async function getData(api: ExcalidrawImperativeAPI) { return { content, svg: svgString - } + }; } function loadData(api: ExcalidrawImperativeAPI, content: CanvasContent, theme: AppState["theme"]) { diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx index eec9e52e3..1c08ee808 100644 --- a/apps/client/src/widgets/type_widgets/code/Code.tsx +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -79,6 +79,7 @@ export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentC const mime = useNoteProperty(note, "mime"); const spacedUpdate = useEditorSpacedUpdate({ note, + noteType: "code", noteContext, getData: () => ({ content: editorRef.current?.getText() ?? "" }), onContentChange: (content) => { diff --git a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx index 83a03153c..96ede43f7 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx +++ b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx @@ -58,6 +58,7 @@ export default function RelationMap({ note, noteContext, ntxId, parentComponent const spacedUpdate = useEditorSpacedUpdate({ note, noteContext, + noteType: "relationMap", getData() { return { content: JSON.stringify(data), diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.tsx b/apps/client/src/widgets/type_widgets/text/EditableText.tsx index 5097a393a..e5ea9c498 100644 --- a/apps/client/src/widgets/type_widgets/text/EditableText.tsx +++ b/apps/client/src/widgets/type_widgets/text/EditableText.tsx @@ -1,23 +1,25 @@ +import "./EditableText.css"; + +import { CKTextEditor, EditorWatchdog, TemplateDefinition } from "@triliumnext/ckeditor5"; +import { deferred } from "@triliumnext/commons"; +import { RefObject } from "preact"; import { useCallback, useEffect, useRef, useState } from "preact/hooks"; + +import appContext from "../../../components/app_context"; +import { buildSelectedBackgroundColor } from "../../../components/touch_bar"; import dialog from "../../../services/dialog"; +import { t } from "../../../services/i18n"; +import link, { parseNavigationStateFromUrl } from "../../../services/link"; +import note_create from "../../../services/note_create"; +import options from "../../../services/options"; import toast from "../../../services/toast"; import utils, { hasTouchBar, isMobile } from "../../../services/utils"; import { useEditorSpacedUpdate, useLegacyImperativeHandlers, useNoteLabel, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; +import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar"; import { TypeWidgetProps } from "../type_widget"; import CKEditorWithWatchdog, { CKEditorApi } from "./CKEditorWithWatchdog"; -import "./EditableText.css"; -import { CKTextEditor, EditorWatchdog, TemplateDefinition } from "@triliumnext/ckeditor5"; -import options from "../../../services/options"; -import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils"; import getTemplates, { updateTemplateCache } from "./snippets.js"; -import appContext from "../../../components/app_context"; -import link, { parseNavigationStateFromUrl } from "../../../services/link"; -import note_create from "../../../services/note_create"; -import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar"; -import { RefObject } from "preact"; -import { buildSelectedBackgroundColor } from "../../../components/touch_bar"; -import { deferred } from "@triliumnext/commons"; -import { t } from "../../../services/i18n"; +import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils"; /** * The editor can operate into two distinct modes: @@ -39,6 +41,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext const spacedUpdate = useEditorSpacedUpdate({ note, noteContext, + noteType: "text", getData() { const editor = watchdogRef.current?.editor; if (!editor) { @@ -376,7 +379,7 @@ function EditableTextTouchBar({ watchdogRef, refreshTouchBarRef }: { watchdogRef const editor = watchdogRef.current?.editor; switch (selectedIndex) { case 0: - editor?.execute("paragraph") + editor?.execute("paragraph"); break; case 1: editor?.execute("heading", { value: "heading2" }); @@ -396,7 +399,7 @@ function EditableTextTouchBar({ watchdogRef, refreshTouchBarRef }: { watchdogRef - ) + ); } function TouchBarCommandButton({ watchdogRef, icon, command }: { watchdogRef: RefObject, icon: string, command: string }) {