mirror of
https://github.com/docmost/docmost
synced 2025-03-28 21:13:28 +00:00
Fix page title
This commit is contained in:
parent
210d1474ea
commit
f86139a585
@ -37,6 +37,7 @@
|
|||||||
"katex": "0.16.21",
|
"katex": "0.16.21",
|
||||||
"lowlight": "^3.2.0",
|
"lowlight": "^3.2.0",
|
||||||
"mermaid": "^11.4.1",
|
"mermaid": "^11.4.1",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-arborist": "3.4.0",
|
"react-arborist": "3.4.0",
|
||||||
"react-clear-modal": "^2.0.11",
|
"react-clear-modal": "^2.0.11",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import "@/features/editor/styles/index.css";
|
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 { EditorContent, useEditor } from "@tiptap/react";
|
||||||
import { Document } from "@tiptap/extension-document";
|
import { Document } from "@tiptap/extension-document";
|
||||||
import { Heading } from "@tiptap/extension-heading";
|
import { Heading } from "@tiptap/extension-heading";
|
||||||
@ -11,16 +11,16 @@ import {
|
|||||||
titleEditorAtom,
|
titleEditorAtom,
|
||||||
} from "@/features/editor/atoms/editor-atoms";
|
} from "@/features/editor/atoms/editor-atoms";
|
||||||
import { useUpdatePageMutation } from "@/features/page/queries/page-query";
|
import { useUpdatePageMutation } from "@/features/page/queries/page-query";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedCallback } from "@mantine/hooks";
|
||||||
import { useAtom } from "jotai";
|
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 { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||||
import { History } from "@tiptap/extension-history";
|
import { History } from "@tiptap/extension-history";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
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 {
|
export interface TitleEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -38,16 +38,9 @@ export function TitleEditor({
|
|||||||
editable,
|
editable,
|
||||||
}: TitleEditorProps) {
|
}: TitleEditorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
const { mutateAsync: updatePageMutationAsync } = useUpdatePageMutation();
|
||||||
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 700);
|
|
||||||
const {
|
|
||||||
data: updatedPageData,
|
|
||||||
mutate: updatePageMutation,
|
|
||||||
status,
|
|
||||||
} = useUpdatePageMutation();
|
|
||||||
const pageEditor = useAtomValue(pageEditorAtom);
|
const pageEditor = useAtomValue(pageEditorAtom);
|
||||||
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
||||||
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
|
||||||
const emit = useQueryEmit();
|
const emit = useQueryEmit();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activePageId, setActivePageId] = useState(pageId);
|
const [activePageId, setActivePageId] = useState(pageId);
|
||||||
@ -68,7 +61,7 @@ export function TitleEditor({
|
|||||||
History.configure({
|
History.configure({
|
||||||
depth: 20,
|
depth: 20,
|
||||||
}),
|
}),
|
||||||
EmojiCommand
|
EmojiCommand,
|
||||||
],
|
],
|
||||||
onCreate({ editor }) {
|
onCreate({ editor }) {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
@ -77,8 +70,7 @@ export function TitleEditor({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUpdate({ editor }) {
|
onUpdate({ editor }) {
|
||||||
const currentTitle = editor.getText();
|
debounceUpdate();
|
||||||
setDebouncedTitleState(currentTitle);
|
|
||||||
setActivePageId(pageId);
|
setActivePageId(pageId);
|
||||||
},
|
},
|
||||||
editable: editable,
|
editable: editable,
|
||||||
@ -92,31 +84,30 @@ export function TitleEditor({
|
|||||||
navigate(pageSlug, { replace: true });
|
navigate(pageSlug, { replace: true });
|
||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
useEffect(() => {
|
const saveTitle = useCallback(() => {
|
||||||
if (debouncedTitle !== null && activePageId === pageId) {
|
if (!titleEditor) return;
|
||||||
updatePageMutation({
|
if (titleEditor.getText() === title) return;
|
||||||
pageId: pageId,
|
// may no longer be needed
|
||||||
title: debouncedTitle,
|
if (activePageId !== pageId) return;
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [debouncedTitle]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
updatePageMutationAsync({
|
||||||
if (status === "success" && updatedPageData) {
|
pageId: pageId,
|
||||||
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
|
title: titleEditor.getText() ?? "",
|
||||||
setTreeData(newTreeData);
|
}).then((page) => {
|
||||||
|
const event: UpdateEvent = {
|
||||||
|
operation: "updateOne",
|
||||||
|
spaceId: page.spaceId,
|
||||||
|
entity: ["pages"],
|
||||||
|
id: page.id,
|
||||||
|
payload: { title: page.title, slugId: page.slugId },
|
||||||
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
localEmitter.emit("message", event);
|
||||||
emit({
|
emit(event);
|
||||||
operation: "updateOne",
|
});
|
||||||
spaceId: updatedPageData.spaceId,
|
}, [pageId, title, titleEditor]);
|
||||||
entity: ["pages"],
|
|
||||||
id: pageId,
|
const debounceUpdate = useDebouncedCallback(saveTitle, 500);
|
||||||
payload: { title: debouncedTitle, slugId: slugId },
|
|
||||||
});
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}, [updatedPageData, status]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (titleEditor && title !== titleEditor.getText()) {
|
if (titleEditor && title !== titleEditor.getText()) {
|
||||||
@ -130,6 +121,13 @@ export function TitleEditor({
|
|||||||
}, 500);
|
}, 500);
|
||||||
}, [titleEditor]);
|
}, [titleEditor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// force-save title on navigation
|
||||||
|
saveTitle();
|
||||||
|
};
|
||||||
|
}, [pageId]);
|
||||||
|
|
||||||
function handleTitleKeyDown(event) {
|
function handleTitleKeyDown(event) {
|
||||||
if (!titleEditor || !pageEditor || event.shiftKey) return;
|
if (!titleEditor || !pageEditor || event.shiftKey) return;
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { WebSocketEvent } from "@/features/websocket/types";
|
|||||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { SimpleTree } from "react-arborist";
|
import { SimpleTree } from "react-arborist";
|
||||||
|
import localEmitter from "@/lib/local-emitter.ts";
|
||||||
|
|
||||||
export const useTreeSocket = () => {
|
export const useTreeSocket = () => {
|
||||||
const [socket] = useAtom(socketAtom);
|
const [socket] = useAtom(socketAtom);
|
||||||
@ -18,8 +19,29 @@ export const useTreeSocket = () => {
|
|||||||
}, [treeData]);
|
}, [treeData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket?.on("message", (event: WebSocketEvent) => {
|
const updateNodeName = (event) => {
|
||||||
|
const initialData = initialTreeData.current;
|
||||||
|
const treeApi = new SimpleTree<SpaceTreeNode>(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 initialData = initialTreeData.current;
|
||||||
const treeApi = new SimpleTree<SpaceTreeNode>(initialData);
|
const treeApi = new SimpleTree<SpaceTreeNode>(initialData);
|
||||||
|
|
||||||
@ -28,29 +50,39 @@ export const useTreeSocket = () => {
|
|||||||
if (event.entity[0] === "pages") {
|
if (event.entity[0] === "pages") {
|
||||||
if (treeApi.find(event.id)) {
|
if (treeApi.find(event.id)) {
|
||||||
if (event.payload?.title) {
|
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) {
|
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);
|
setTreeData(treeApi.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'addTreeNode':
|
case "addTreeNode":
|
||||||
if (treeApi.find(event.payload.data.id)) return;
|
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);
|
setTreeData(treeApi.data);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'moveTreeNode':
|
case "moveTreeNode":
|
||||||
// move node
|
// move node
|
||||||
if (treeApi.find(event.payload.id)) {
|
if (treeApi.find(event.payload.id)) {
|
||||||
treeApi.move({
|
treeApi.move({
|
||||||
id: event.payload.id,
|
id: event.payload.id,
|
||||||
parentId: event.payload.parentId,
|
parentId: event.payload.parentId,
|
||||||
index: event.payload.index
|
index: event.payload.index,
|
||||||
});
|
});
|
||||||
|
|
||||||
// update node position
|
// update node position
|
||||||
@ -58,7 +90,7 @@ export const useTreeSocket = () => {
|
|||||||
id: event.payload.id,
|
id: event.payload.id,
|
||||||
changes: {
|
changes: {
|
||||||
position: event.payload.position,
|
position: event.payload.position,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setTreeData(treeApi.data);
|
setTreeData(treeApi.data);
|
||||||
@ -66,12 +98,12 @@ export const useTreeSocket = () => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case "deleteTreeNode":
|
case "deleteTreeNode":
|
||||||
if (treeApi.find(event.payload.node.id)){
|
if (treeApi.find(event.payload.node.id)) {
|
||||||
treeApi.drop({ id: event.payload.node.id });
|
treeApi.drop({ id: event.payload.node.id });
|
||||||
setTreeData(treeApi.data);
|
setTreeData(treeApi.data);
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['pages', event.payload.node.slugId].filter(Boolean),
|
queryKey: ["pages", event.payload.node.slugId].filter(Boolean),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
3
apps/client/src/lib/local-emitter.ts
Normal file
3
apps/client/src/lib/local-emitter.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import mitt from "mitt";
|
||||||
|
const localEmitter = mitt();
|
||||||
|
export default localEmitter;
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -281,6 +281,9 @@ importers:
|
|||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.4.1
|
specifier: ^11.4.1
|
||||||
version: 11.4.1
|
version: 11.4.1
|
||||||
|
mitt:
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@ -6957,6 +6960,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
mitt@3.0.1:
|
||||||
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
mkdirp@1.0.4:
|
mkdirp@1.0.4:
|
||||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -16819,6 +16825,8 @@ snapshots:
|
|||||||
minipass: 3.3.6
|
minipass: 3.3.6
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
mlly@1.7.3:
|
mlly@1.7.3:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user