fix(offline-sync): keep page titles in sync between REST and Yjs
Title now lives in the page's Yjs 'title' fragment, but two paths corrupted it: - Rename-revert: a REST/MCP title change wrote only the page.title column, never the Yjs fragment, so the next editor open replayed the stale Yjs title and reverted the rename. PageService.update now mirrors the new title into the Yjs 'title' fragment via CollaborationGateway.writePageTitle, which goes through openDirectConnection directly (Redis-independent: works with COLLAB_DISABLE_REDIS and in single-process deployments, unlike the Redis-routed handleYjsEvent path). The write is best-effort: a Yjs failure is logged and never rolls back the committed column write. Agent provenance (actor/aiChatId) is threaded into the store context. - Untitled-on-open: an empty/just-initialized 'title' fragment clobbered a non-empty page.title to '' on open. onStoreDocument now treats the title as changed only when the extracted text is non-empty, covering both the title-only and body+title save branches. Empty-retitling via collab is intentionally impossible; the REST DTO is the place to enforce non-empty. writeTitleFragment does a full clear+seed of the 'title' fragment (no duplication/concatenation) and leaves the body fragment intact. Removed the dead useTreeMutation.handleRename path. Adds unit tests for writeTitleFragment, the gateway write, the anti-empty-clobber guard, and agent provenance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,6 @@ import {
|
||||
useCreatePageMutation,
|
||||
useRemovePageMutation,
|
||||
useMovePageMutation,
|
||||
useUpdatePageMutation,
|
||||
updateCacheOnMovePage,
|
||||
} from "@/features/page/queries/page-query.ts";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
@@ -26,7 +25,6 @@ export type UseTreeMutation = {
|
||||
parentId: string | null,
|
||||
opts?: { temporary?: boolean },
|
||||
) => Promise<void>;
|
||||
handleRename: (id: string, name: string) => Promise<void>;
|
||||
handleDelete: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -38,7 +36,6 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
// children) and then immediately invokes a handler.
|
||||
const store = useStore();
|
||||
const createPageMutation = useCreatePageMutation();
|
||||
const updatePageMutation = useUpdatePageMutation();
|
||||
const removePageMutation = useRemovePageMutation();
|
||||
const movePageMutation = useMovePageMutation();
|
||||
const navigate = useNavigate();
|
||||
@@ -192,20 +189,6 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
[spaceId, createPageMutation, setData, store, navigate, spaceSlug],
|
||||
);
|
||||
|
||||
const handleRename = useCallback(
|
||||
async (id: string, name: string) => {
|
||||
setData((prev) =>
|
||||
treeModel.update(prev, id, { name } as Partial<SpaceTreeNode>),
|
||||
);
|
||||
try {
|
||||
await updatePageMutation.mutateAsync({ pageId: id, title: name });
|
||||
} catch (error) {
|
||||
console.error("Error updating page title:", error);
|
||||
}
|
||||
},
|
||||
[updatePageMutation, setData],
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
const node = treeModel.find(
|
||||
@@ -251,7 +234,7 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
[removePageMutation, setData, store, pageSlug, navigate, spaceSlug],
|
||||
);
|
||||
|
||||
return { handleMove, handleCreate, handleRename, handleDelete };
|
||||
return { handleMove, handleCreate, handleDelete };
|
||||
}
|
||||
|
||||
function isPageInNode(node: SpaceTreeNode, pageSlug: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user