fix(desktop): prevent editor reload after note creation

This commit is contained in:
tianma-if committed 2026-08-29 21:13:39 +08:00
1 parent d4821eb486
commit 87980a3854
3 files changed
+86 -2

No files matched your search

+6 -2
View File
@@ -5,7 +5,7 @@ import { isBrowserOffline } from "@/lib/network-status";
import { emptySyncQueueSummary, type SyncQueueSummary } from "@/lib/sync-queue";
import { notifyMemoIdRemapped, notifyMemoSyncAcknowledged } from "@/lib/sync-events";
import { observeLocalMemoIdMappings, putLocalMemo, replaceLocalMemoId } from "@/lib/local-mirror";
import { resolveSyncedMemoId } from "@/lib/workspace-refresh";
import { preserveRemappedMemoDetailQueries, resolveSyncedMemoId } from "@/lib/workspace-refresh";
type UseWorkspaceQueuedSyncOptions = {
isOnline: boolean;
@@ -34,6 +34,10 @@ export const useWorkspaceQueuedSync = ({
const applyMemoIdMappings = useCallback((memoIdMappings: ReadonlyMap<string, string>) => {
if (memoIdMappings.size === 0) return;
// Seed the durable-id query before changing the selection. The desktop
// editor otherwise renders its loading state between the local-id and
// remote-id queries, which looks like a full editor reload after create.
preserveRemappedMemoDetailQueries(queryClient, memoIdMappings);
notifyMemoIdRemapped(memoIdMappings);
const selectedMemoId = selectedMemoIdRef.current;
const pendingMemoId = pendingCreatedMemoIdRef.current;
@@ -51,7 +55,7 @@ export const useWorkspaceQueuedSync = ({
selectedMemoIdRef.current = nextSelectedMemoId;
setSelectedMemoId(nextSelectedMemoId);
}
}, [pendingCreatedMemoIdRef, selectedMemoIdRef, setCreatedMemoEditId, setSelectedMemoId]);
}, [pendingCreatedMemoIdRef, queryClient, selectedMemoIdRef, setCreatedMemoEditId, setSelectedMemoId]);
useEffect(() => observeLocalMemoIdMappings(localDataScope, applyMemoIdMappings), [applyMemoIdMappings, localDataScope]);
@@ -1,5 +1,6 @@
import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { QueryClient } from "@tanstack/react-query";
import {
BACKGROUND_WORKSPACE_REFRESH_INTERVAL_MS,
claimBackgroundRefreshLease,
@@ -7,6 +8,7 @@ import {
DEFERRED_MEMO_SYNC_DELAY_MS,
releaseBackgroundRefreshLease,
refreshWorkspaceData,
preserveRemappedMemoDetailQueries,
resolveCreatedMemoSelection,
resolveSyncedMemoId,
shouldNavigateHomeWhenOpeningMemo,
@@ -76,6 +78,47 @@ describe("refreshWorkspaceData", () => {
assert.equal(resolveSyncedMemoId(mappings, null), null);
});
it("preserves the active memo detail while desktop sync changes its query key", () => {
const queryClient = new QueryClient();
const temporaryMemo = {
id: "memo_local_1",
title: "",
contentMarkdown: "",
tags: [],
};
queryClient.setQueryData(["memo", temporaryMemo.id, "notebook"], { memo: temporaryMemo });
preserveRemappedMemoDetailQueries(
queryClient,
new Map([[temporaryMemo.id, "memo_remote_1"]]),
);
assert.deepEqual(
queryClient.getQueryData(["memo", "memo_remote_1", "notebook"]),
{ memo: { ...temporaryMemo, id: "memo_remote_1" } },
);
});
it("does not replace an already cached remote memo during id handoff", () => {
const queryClient = new QueryClient();
queryClient.setQueryData(["memo", "memo_local_1", "notebook"], {
memo: { id: "memo_local_1", title: "local", contentMarkdown: "", tags: [] },
});
queryClient.setQueryData(["memo", "memo_remote_1", "notebook"], {
memo: { id: "memo_remote_1", title: "remote", contentMarkdown: "", tags: [] },
});
preserveRemappedMemoDetailQueries(
queryClient,
new Map([["memo_local_1", "memo_remote_1"]]),
);
assert.equal(
queryClient.getQueryData(["memo", "memo_remote_1", "notebook"]).memo.title,
"remote",
);
});
it("remaps a created memo selection without closing over the selected memo state", () => {
assert.equal(resolveCreatedMemoSelection("memo_local_1", null, "memo_local_1", "memo_remote_1"), "memo_remote_1");
assert.equal(resolveCreatedMemoSelection("memo_other", "memo_local_1", "memo_local_1", "memo_remote_1"), "memo_remote_1");
+37
View File
@@ -1,3 +1,6 @@
import type { QueryClient } from "@tanstack/react-query";
import type { MemoDetail } from "@edgeever/shared";
export type WorkspaceRefreshMode = "background" | "manual";
export type WorkspaceMemoView = "notebook" | "trash";
@@ -86,6 +89,40 @@ export const resolveSyncedMemoId = (
memoId: string | null,
) => memoId ? memoIdMappings.get(memoId) ?? memoId : null;
type MemoDetailQueryData = { memo: MemoDetail };
/**
* Keep the active editor backed by cached detail while sync replaces a local
* memo id with its durable server id. Without this handoff, React Query briefly
* observes an empty query under the new key and swaps the editor for its loading
* state until the desktop sidecar returns the same memo again.
*/
export const preserveRemappedMemoDetailQueries = (
queryClient: Pick<QueryClient, "getQueryData" | "setQueryData">,
memoIdMappings: ReadonlyMap<string, string>,
) => {
for (const [temporaryId, remoteId] of memoIdMappings) {
if (temporaryId === remoteId) continue;
for (const view of ["notebook", "trash"] as const) {
const remoteKey = ["memo", remoteId, view] as const;
if (queryClient.getQueryData<MemoDetailQueryData>(remoteKey)?.memo) continue;
const temporary = queryClient.getQueryData<MemoDetailQueryData>([
"memo",
temporaryId,
view,
]);
if (!temporary?.memo) continue;
queryClient.setQueryData<MemoDetailQueryData>(remoteKey, {
...temporary,
memo: { ...temporary.memo, id: remoteId },
});
}
}
};
export const resolveCreatedMemoSelection = (
currentMemoId: string | null,
pendingMemoId: string | null,