fix(desktop): prevent editor reload after note creation
This commit is contained in:
1 parent
d4821eb486
commit
87980a3854
3 files changed
+86
-2
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user