From da118d794d06069358294d5afd3c3e70dd28fcf8 Mon Sep 17 00:00:00 2001 From: AdminTeamCoderz <164670747+AdminTeamCoderz@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:20:27 +0100 Subject: [PATCH 1/2] fix(editor): keep the unsaved indicator in sync with actual content state --- .../src/components/documents/useDocumentActions.ts | 9 ++++++--- packages/editor/src/Editor.tsx | 12 +++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/documents/useDocumentActions.ts b/apps/web/src/components/documents/useDocumentActions.ts index b32a759..8a8007b 100644 --- a/apps/web/src/components/documents/useDocumentActions.ts +++ b/apps/web/src/components/documents/useDocumentActions.ts @@ -282,19 +282,22 @@ export function useDocumentActions( }; }, [options?.listenForSaveRequests, tabId]); + // Dirty means the user edited AND the content still differs from a saved + // state — the same condition that enables saving, so the indicator and the + // save button cannot disagree (e.g. after undoing back to the original). useEffect(() => { if (isLoading) return; if (isDocumentTab) { - setTabDirty(tab.id, hasUserEdit); + setTabDirty(tab.id, hasUserEdit && !isUpToDate); } - }, [isDocumentTab, isLoading, hasUserEdit, setTabDirty, tab?.id]); + }, [isDocumentTab, isLoading, hasUserEdit, isUpToDate, setTabDirty, tab?.id]); const backgroundClassName = (() => { if (!handle) return undefined; if (isJustSaved) { return 'bg-green-100/80 dark:bg-green-900/40'; } - if (!isLoading && hasUserEdit) { + if (!isLoading && hasUserEdit && !isUpToDate) { // Document has unsaved changes return 'bg-yellow-100/70 dark:bg-yellow-900/30'; } diff --git a/packages/editor/src/Editor.tsx b/packages/editor/src/Editor.tsx index 68306c2..a3d71ca 100644 --- a/packages/editor/src/Editor.tsx +++ b/packages/editor/src/Editor.tsx @@ -77,10 +77,16 @@ export const Editor: React.FC<{ [documentId, tabId, updateEditorStoreState], ); + // Deferred: listeners for this event mount in the same commit as the editor + // but their effects run later, so a synchronous dispatch is lost and the + // first keystroke would be mistaken for the loaded baseline. useEffect(() => { - const editorState = editor.getEditorState(); - const serializedEditorState = serializeEditorState(editorState); - updateChecksum(serializedEditorState); + const timeout = setTimeout(() => { + const editorState = editor.getEditorState(); + const serializedEditorState = serializeEditorState(editorState); + updateChecksum(serializedEditorState); + }, 0); + return () => clearTimeout(timeout); }, [editor, updateChecksum]); const onChangeHandler = useCallback( From 7da7834047e4f2f17d0cd52db27a24073db93cdd Mon Sep 17 00:00:00 2001 From: AdminTeamCoderz <164670747+AdminTeamCoderz@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:40:11 +0100 Subject: [PATCH 2/2] fix(editor): converge dirty state across hook instances and confirm silent saves --- .../documents/useDocumentActions.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/documents/useDocumentActions.ts b/apps/web/src/components/documents/useDocumentActions.ts index 8a8007b..9d6d7b9 100644 --- a/apps/web/src/components/documents/useDocumentActions.ts +++ b/apps/web/src/components/documents/useDocumentActions.ts @@ -19,6 +19,7 @@ import { useSaveLocalRevisionMutation, } from '@/queries/revisions'; import { useActions, useSelector } from '@/store'; +import { toast } from 'sonner'; interface UseDocumentActionsOptions { listenForSaveRequests?: boolean; @@ -71,11 +72,20 @@ export function useDocumentActions( const isPreviouslySaved = !!cloudRevision; const isUnchangedSinceLoad = !!loadedChecksum && loadedChecksum === checksum; - const isUpToDate = - isUnchangedSinceLoad || - (isPreviouslySaved && - document?.currentRevisionId === cloudRevision.id && - cloudRevision.checksum === checksum); + const isCurrentHeadContent = + isPreviouslySaved && + document?.currentRevisionId === cloudRevision.id && + cloudRevision.checksum === checksum; + const isUpToDate = isUnchangedSinceLoad || isCurrentHeadContent; + + // Saves can be performed by any of this hook's instances (tab button, + // Cmd+S, tab context menu), and each instance keeps its own baseline. + // When the current content is confirmed as the saved head, re-baseline so + // an instance that did not perform the save cannot keep a stale view. + if (checksum && isCurrentHeadContent && (loadedChecksum !== checksum || hasUserEdit)) { + setLoadedChecksum(checksum); + setHasUserEdit(false); + } const isLoading = !document || @@ -147,6 +157,11 @@ export function useDocumentActions( }); await saveLocalRevision({ serializedEditorState: JSON.parse(revision.content) }); } + // This path reuses an existing revision, so the save mutation's own + // toast never fires; confirm explicit saves here. + if (!isAutosave) { + toast.success('Document saved'); + } } else { const serializedEditorState = await queryClient .ensureQueryData(