From f86139a585f18d435785ec9a6e3e1d4ea3e77dcc Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:36:41 +0000 Subject: [PATCH] Fix page title --- apps/client/package.json | 1 + .../src/features/editor/title-editor.tsx | 76 +++++++++---------- .../src/features/websocket/use-tree-socket.ts | 52 ++++++++++--- apps/client/src/lib/local-emitter.ts | 3 + pnpm-lock.yaml | 8 ++ 5 files changed, 91 insertions(+), 49 deletions(-) create mode 100644 apps/client/src/lib/local-emitter.ts diff --git a/apps/client/package.json b/apps/client/package.json index 9401a8fb..b757f932 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -37,6 +37,7 @@ "katex": "0.16.21", "lowlight": "^3.2.0", "mermaid": "^11.4.1", + "mitt": "^3.0.1", "react": "^18.3.1", "react-arborist": "3.4.0", "react-clear-modal": "^2.0.11", diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index f876fb93..0fc270e5 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -1,5 +1,5 @@ import "@/features/editor/styles/index.css"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { EditorContent, useEditor } from "@tiptap/react"; import { Document } from "@tiptap/extension-document"; import { Heading } from "@tiptap/extension-heading"; @@ -11,16 +11,16 @@ import { titleEditorAtom, } from "@/features/editor/atoms/editor-atoms"; import { useUpdatePageMutation } from "@/features/page/queries/page-query"; -import { useDebouncedValue } from "@mantine/hooks"; +import { useDebouncedCallback } from "@mantine/hooks"; import { useAtom } from "jotai"; -import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; -import { updateTreeNodeName } from "@/features/page/tree/utils"; import { useQueryEmit } from "@/features/websocket/use-query-emit.ts"; import { History } from "@tiptap/extension-history"; import { buildPageUrl } from "@/features/page/page.utils.ts"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import EmojiCommand from '@/features/editor/extensions/emoji-command.ts'; +import EmojiCommand from "@/features/editor/extensions/emoji-command.ts"; +import { UpdateEvent } from "@/features/websocket/types"; +import localEmitter from "@/lib/local-emitter.ts"; export interface TitleEditorProps { pageId: string; @@ -38,16 +38,9 @@ export function TitleEditor({ editable, }: TitleEditorProps) { const { t } = useTranslation(); - const [debouncedTitleState, setDebouncedTitleState] = useState(null); - const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 700); - const { - data: updatedPageData, - mutate: updatePageMutation, - status, - } = useUpdatePageMutation(); + const { mutateAsync: updatePageMutationAsync } = useUpdatePageMutation(); const pageEditor = useAtomValue(pageEditorAtom); const [, setTitleEditor] = useAtom(titleEditorAtom); - const [treeData, setTreeData] = useAtom(treeDataAtom); const emit = useQueryEmit(); const navigate = useNavigate(); const [activePageId, setActivePageId] = useState(pageId); @@ -68,7 +61,7 @@ export function TitleEditor({ History.configure({ depth: 20, }), - EmojiCommand + EmojiCommand, ], onCreate({ editor }) { if (editor) { @@ -77,8 +70,7 @@ export function TitleEditor({ } }, onUpdate({ editor }) { - const currentTitle = editor.getText(); - setDebouncedTitleState(currentTitle); + debounceUpdate(); setActivePageId(pageId); }, editable: editable, @@ -92,31 +84,30 @@ export function TitleEditor({ navigate(pageSlug, { replace: true }); }, [title]); - useEffect(() => { - if (debouncedTitle !== null && activePageId === pageId) { - updatePageMutation({ - pageId: pageId, - title: debouncedTitle, - }); - } - }, [debouncedTitle]); + const saveTitle = useCallback(() => { + if (!titleEditor) return; + if (titleEditor.getText() === title) return; + // may no longer be needed + if (activePageId !== pageId) return; - useEffect(() => { - if (status === "success" && updatedPageData) { - const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle); - setTreeData(newTreeData); + updatePageMutationAsync({ + pageId: pageId, + title: titleEditor.getText() ?? "", + }).then((page) => { + const event: UpdateEvent = { + operation: "updateOne", + spaceId: page.spaceId, + entity: ["pages"], + id: page.id, + payload: { title: page.title, slugId: page.slugId }, + }; - setTimeout(() => { - emit({ - operation: "updateOne", - spaceId: updatedPageData.spaceId, - entity: ["pages"], - id: pageId, - payload: { title: debouncedTitle, slugId: slugId }, - }); - }, 50); - } - }, [updatedPageData, status]); + localEmitter.emit("message", event); + emit(event); + }); + }, [pageId, title, titleEditor]); + + const debounceUpdate = useDebouncedCallback(saveTitle, 500); useEffect(() => { if (titleEditor && title !== titleEditor.getText()) { @@ -130,6 +121,13 @@ export function TitleEditor({ }, 500); }, [titleEditor]); + useEffect(() => { + return () => { + // force-save title on navigation + saveTitle(); + }; + }, [pageId]); + function handleTitleKeyDown(event) { if (!titleEditor || !pageEditor || event.shiftKey) return; diff --git a/apps/client/src/features/websocket/use-tree-socket.ts b/apps/client/src/features/websocket/use-tree-socket.ts index bb7d9c3d..b203ee1c 100644 --- a/apps/client/src/features/websocket/use-tree-socket.ts +++ b/apps/client/src/features/websocket/use-tree-socket.ts @@ -6,6 +6,7 @@ import { WebSocketEvent } from "@/features/websocket/types"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; import { useQueryClient } from "@tanstack/react-query"; import { SimpleTree } from "react-arborist"; +import localEmitter from "@/lib/local-emitter.ts"; export const useTreeSocket = () => { const [socket] = useAtom(socketAtom); @@ -18,8 +19,29 @@ export const useTreeSocket = () => { }, [treeData]); useEffect(() => { - socket?.on("message", (event: WebSocketEvent) => { + const updateNodeName = (event) => { + const initialData = initialTreeData.current; + const treeApi = new SimpleTree(initialData); + if (treeApi.find(event.id)) { + if (event.payload?.title) { + treeApi.update({ + id: event.id, + changes: { name: event.payload.title }, + }); + } + setTreeData(treeApi.data); + } + }; + + localEmitter.on("message", updateNodeName); + return () => { + localEmitter.off("message", updateNodeName); + }; + }, []); + + useEffect(() => { + socket?.on("message", (event: WebSocketEvent) => { const initialData = initialTreeData.current; const treeApi = new SimpleTree(initialData); @@ -28,29 +50,39 @@ export const useTreeSocket = () => { if (event.entity[0] === "pages") { if (treeApi.find(event.id)) { if (event.payload?.title) { - treeApi.update({ id: event.id, changes: { name: event.payload.title } }); + treeApi.update({ + id: event.id, + changes: { name: event.payload.title }, + }); } if (event.payload?.icon) { - treeApi.update({ id: event.id, changes: { icon: event.payload.icon } }); + treeApi.update({ + id: event.id, + changes: { icon: event.payload.icon }, + }); } setTreeData(treeApi.data); } } break; - case 'addTreeNode': + case "addTreeNode": if (treeApi.find(event.payload.data.id)) return; - treeApi.create({ parentId: event.payload.parentId, index: event.payload.index, data: event.payload.data }); + treeApi.create({ + parentId: event.payload.parentId, + index: event.payload.index, + data: event.payload.data, + }); setTreeData(treeApi.data); break; - case 'moveTreeNode': + case "moveTreeNode": // move node if (treeApi.find(event.payload.id)) { treeApi.move({ id: event.payload.id, parentId: event.payload.parentId, - index: event.payload.index + index: event.payload.index, }); // update node position @@ -58,7 +90,7 @@ export const useTreeSocket = () => { id: event.payload.id, changes: { position: event.payload.position, - } + }, }); setTreeData(treeApi.data); @@ -66,12 +98,12 @@ export const useTreeSocket = () => { break; case "deleteTreeNode": - if (treeApi.find(event.payload.node.id)){ + if (treeApi.find(event.payload.node.id)) { treeApi.drop({ id: event.payload.node.id }); setTreeData(treeApi.data); queryClient.invalidateQueries({ - queryKey: ['pages', event.payload.node.slugId].filter(Boolean), + queryKey: ["pages", event.payload.node.slugId].filter(Boolean), }); } break; diff --git a/apps/client/src/lib/local-emitter.ts b/apps/client/src/lib/local-emitter.ts new file mode 100644 index 00000000..643cf0ee --- /dev/null +++ b/apps/client/src/lib/local-emitter.ts @@ -0,0 +1,3 @@ +import mitt from "mitt"; +const localEmitter = mitt(); +export default localEmitter; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 043f92d4..af341679 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -281,6 +281,9 @@ importers: mermaid: specifier: ^11.4.1 version: 11.4.1 + mitt: + specifier: ^3.0.1 + version: 3.0.1 react: specifier: ^18.3.1 version: 18.3.1 @@ -6957,6 +6960,9 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -16819,6 +16825,8 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mitt@3.0.1: {} + mkdirp@1.0.4: {} mlly@1.7.3: