mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 04:32:41 +10:00
fix: editor improvements (#583)
* delete unused component * return page prosemirror content * prefetch pages * use prosemirro json content on editor * cache page query with id and slug as key * Show notice on collaboration disconnection * enable scroll while typing * enable immediatelyRender * avoid image break in PDF print * Comment editor rendering props
This commit is contained in:
@ -48,6 +48,8 @@ const CommentEditor = forwardRef(
|
|||||||
},
|
},
|
||||||
content: defaultContent,
|
content: defaultContent,
|
||||||
editable,
|
editable,
|
||||||
|
immediatelyRender: true,
|
||||||
|
shouldRerenderOnTransaction: false,
|
||||||
autofocus: (autofocus && "end") || false,
|
autofocus: (autofocus && "end") || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from "jotai";
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from "@tiptap/core";
|
||||||
|
|
||||||
export const pageEditorAtom = atom<Editor | null>(null);
|
export const pageEditorAtom = atom<Editor | null>(null);
|
||||||
|
|
||||||
export const titleEditorAtom = atom<Editor | null>(null);
|
export const titleEditorAtom = atom<Editor | null>(null);
|
||||||
|
|
||||||
|
export const yjsConnectionStatusAtom = atom<string>("");
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export interface FullEditorProps {
|
|||||||
pageId: string;
|
pageId: string;
|
||||||
slugId: string;
|
slugId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
content: string;
|
||||||
spaceSlug: string;
|
spaceSlug: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
}
|
}
|
||||||
@ -21,6 +22,7 @@ export function FullEditor({
|
|||||||
pageId,
|
pageId,
|
||||||
title,
|
title,
|
||||||
slugId,
|
slugId,
|
||||||
|
content,
|
||||||
spaceSlug,
|
spaceSlug,
|
||||||
editable,
|
editable,
|
||||||
}: FullEditorProps) {
|
}: FullEditorProps) {
|
||||||
@ -40,7 +42,7 @@ export function FullEditor({
|
|||||||
spaceSlug={spaceSlug}
|
spaceSlug={spaceSlug}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
<MemoizedPageEditor pageId={pageId} editable={editable} />
|
<MemoizedPageEditor pageId={pageId} editable={editable} content={content} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { IndexeddbPersistence } from "y-indexeddb";
|
import { IndexeddbPersistence } from "y-indexeddb";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider";
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
import { EditorContent, EditorProvider, useEditor } from "@tiptap/react";
|
||||||
import {
|
import {
|
||||||
collabExtensions,
|
collabExtensions,
|
||||||
mainExtensions,
|
mainExtensions,
|
||||||
@ -18,14 +18,16 @@ import { useAtom } from "jotai";
|
|||||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom";
|
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom";
|
||||||
import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url";
|
import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url";
|
||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||||
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms";
|
import {
|
||||||
|
pageEditorAtom,
|
||||||
|
yjsConnectionStatusAtom,
|
||||||
|
} from "@/features/editor/atoms/editor-atoms";
|
||||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||||
import {
|
import {
|
||||||
activeCommentIdAtom,
|
activeCommentIdAtom,
|
||||||
showCommentPopupAtom,
|
showCommentPopupAtom,
|
||||||
} from "@/features/comment/atoms/comment-atom";
|
} from "@/features/comment/atoms/comment-atom";
|
||||||
import CommentDialog from "@/features/comment/components/comment-dialog";
|
import CommentDialog from "@/features/comment/components/comment-dialog";
|
||||||
import EditorSkeleton from "@/features/editor/components/editor-skeleton";
|
|
||||||
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
|
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
|
||||||
import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx";
|
import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx";
|
||||||
import TableMenu from "@/features/editor/components/table/table-menu.tsx";
|
import TableMenu from "@/features/editor/components/table/table-menu.tsx";
|
||||||
@ -43,9 +45,14 @@ import DrawioMenu from "./components/drawio/drawio-menu";
|
|||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
content: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
export default function PageEditor({
|
||||||
|
pageId,
|
||||||
|
editable,
|
||||||
|
content,
|
||||||
|
}: PageEditorProps) {
|
||||||
const [token] = useAtom(authTokensAtom);
|
const [token] = useAtom(authTokensAtom);
|
||||||
const collaborationURL = useCollaborationUrl();
|
const collaborationURL = useCollaborationUrl();
|
||||||
const [currentUser] = useAtom(currentUserAtom);
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
@ -56,8 +63,11 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
|||||||
const ydoc = useMemo(() => new Y.Doc(), [pageId]);
|
const ydoc = useMemo(() => new Y.Doc(), [pageId]);
|
||||||
const [isLocalSynced, setLocalSynced] = useState(false);
|
const [isLocalSynced, setLocalSynced] = useState(false);
|
||||||
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
||||||
const documentName = `page.${pageId}`;
|
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
|
||||||
|
yjsConnectionStatusAtom,
|
||||||
|
);
|
||||||
const menuContainerRef = useRef(null);
|
const menuContainerRef = useRef(null);
|
||||||
|
const documentName = `page.${pageId}`;
|
||||||
|
|
||||||
const localProvider = useMemo(() => {
|
const localProvider = useMemo(() => {
|
||||||
const provider = new IndexeddbPersistence(documentName, ydoc);
|
const provider = new IndexeddbPersistence(documentName, ydoc);
|
||||||
@ -76,12 +86,21 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
|||||||
document: ydoc,
|
document: ydoc,
|
||||||
token: token?.accessToken,
|
token: token?.accessToken,
|
||||||
connect: false,
|
connect: false,
|
||||||
|
onStatus: (status) => {
|
||||||
|
if (status.status === "connected") {
|
||||||
|
setYjsConnectionStatus(status.status);
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
provider.on("synced", () => {
|
provider.on("synced", () => {
|
||||||
setRemoteSynced(true);
|
setRemoteSynced(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.on("disconnect", () => {
|
||||||
|
setYjsConnectionStatus(WebSocketStatus.Disconnected);
|
||||||
|
});
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
}, [ydoc, pageId, token?.accessToken]);
|
}, [ydoc, pageId, token?.accessToken]);
|
||||||
|
|
||||||
@ -97,15 +116,18 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
|||||||
}, [remoteProvider, localProvider]);
|
}, [remoteProvider, localProvider]);
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
... mainExtensions,
|
...mainExtensions,
|
||||||
... collabExtensions(remoteProvider, currentUser.user),
|
...collabExtensions(remoteProvider, currentUser.user),
|
||||||
];
|
];
|
||||||
|
|
||||||
const editor = useEditor(
|
const editor = useEditor(
|
||||||
{
|
{
|
||||||
extensions,
|
extensions,
|
||||||
editable,
|
editable,
|
||||||
|
immediatelyRender: true,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
|
scrollThreshold: 80,
|
||||||
|
scrollMargin: 80,
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keydown: (_view, event) => {
|
keydown: (_view, event) => {
|
||||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||||
@ -157,36 +179,51 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
|||||||
setAsideState({ tab: "", isAsideOpen: false });
|
setAsideState({ tab: "", isAsideOpen: false });
|
||||||
}, [pageId]);
|
}, [pageId]);
|
||||||
|
|
||||||
const isSynced = isLocalSynced || isRemoteSynced;
|
useEffect(() => {
|
||||||
|
if (editable) {
|
||||||
|
if (yjsConnectionStatus === WebSocketStatus.Connected) {
|
||||||
|
editor.setEditable(true);
|
||||||
|
} else {
|
||||||
|
// disable edits if connection fails
|
||||||
|
editor.setEditable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [yjsConnectionStatus]);
|
||||||
|
|
||||||
|
const isSynced = isLocalSynced && isRemoteSynced;
|
||||||
|
|
||||||
return isSynced ? (
|
return isSynced ? (
|
||||||
<div>
|
<div>
|
||||||
{isSynced && (
|
<div ref={menuContainerRef}>
|
||||||
<div ref={menuContainerRef}>
|
<EditorContent editor={editor} />
|
||||||
<EditorContent editor={editor} />
|
|
||||||
|
|
||||||
{editor && editor.isEditable && (
|
{editor && editor.isEditable && (
|
||||||
<div>
|
<div>
|
||||||
<EditorBubbleMenu editor={editor} />
|
<EditorBubbleMenu editor={editor} />
|
||||||
<TableMenu editor={editor} />
|
<TableMenu editor={editor} />
|
||||||
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
|
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
|
||||||
<ImageMenu editor={editor} />
|
<ImageMenu editor={editor} />
|
||||||
<VideoMenu editor={editor} />
|
<VideoMenu editor={editor} />
|
||||||
<CalloutMenu editor={editor} />
|
<CalloutMenu editor={editor} />
|
||||||
<ExcalidrawMenu editor={editor} />
|
<ExcalidrawMenu editor={editor} />
|
||||||
<DrawioMenu editor={editor} />
|
<DrawioMenu editor={editor} />
|
||||||
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCommentPopup && (
|
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
|
||||||
<CommentDialog editor={editor} pageId={pageId} />
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<div
|
||||||
)}
|
onClick={() => editor.commands.focus("end")}
|
||||||
<div onClick={() => editor.commands.focus('end')} style={{ paddingBottom: '20vh' }}></div>
|
style={{ paddingBottom: "20vh" }}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EditorSkeleton />
|
<EditorProvider
|
||||||
|
editable={false}
|
||||||
|
extensions={mainExtensions}
|
||||||
|
content={content}
|
||||||
|
></EditorProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-image, .node-video, .node-excalidraw, .node-drawio {
|
.node-image, .node-video, .node-excalidraw, .node-drawio {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function TitleEditor({
|
|||||||
}: TitleEditorProps) {
|
}: TitleEditorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
||||||
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500);
|
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 700);
|
||||||
const {
|
const {
|
||||||
data: updatedPageData,
|
data: updatedPageData,
|
||||||
mutate: updatePageMutation,
|
mutate: updatePageMutation,
|
||||||
@ -81,6 +81,8 @@ export function TitleEditor({
|
|||||||
},
|
},
|
||||||
editable: editable,
|
editable: editable,
|
||||||
content: title,
|
content: title,
|
||||||
|
immediatelyRender: true,
|
||||||
|
shouldRerenderOnTransaction: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
IconMessage,
|
IconMessage,
|
||||||
IconPrinter,
|
IconPrinter,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
|
IconWifiOff,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||||
@ -23,18 +24,31 @@ import { extractPageSlugId } from "@/lib";
|
|||||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||||
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||||
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ExportModal from "@/components/common/export-modal";
|
import ExportModal from "@/components/common/export-modal";
|
||||||
|
import { yjsConnectionStatusAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||||
const toggleAside = useToggleAside();
|
const toggleAside = useToggleAside();
|
||||||
|
const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{yjsConnectionStatus === "disconnected" && (
|
||||||
|
<Tooltip
|
||||||
|
label="Real-time editor connection lost. Retrying..."
|
||||||
|
openDelay={250}
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<ActionIcon variant="default" c="red" style={{ border: "none" }}>
|
||||||
|
<IconWifiOff size={20} stroke={2} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip label="Comments" openDelay={250} withArrow>
|
<Tooltip label="Comments" openDelay={250} withArrow>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@ -25,17 +25,31 @@ import { notifications } from "@mantine/notifications";
|
|||||||
import { IPagination } from "@/lib/types.ts";
|
import { IPagination } from "@/lib/types.ts";
|
||||||
import { queryClient } from "@/main.tsx";
|
import { queryClient } from "@/main.tsx";
|
||||||
import { buildTree } from "@/features/page/tree/utils";
|
import { buildTree } from "@/features/page/tree/utils";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { validate as isValidUuid } from "uuid";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function usePageQuery(
|
export function usePageQuery(
|
||||||
pageInput: Partial<IPageInput>,
|
pageInput: Partial<IPageInput>,
|
||||||
): UseQueryResult<IPage, Error> {
|
): UseQueryResult<IPage, Error> {
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["pages", pageInput.pageId],
|
queryKey: ["pages", pageInput.pageId],
|
||||||
queryFn: () => getPageById(pageInput),
|
queryFn: () => getPageById(pageInput),
|
||||||
enabled: !!pageInput.pageId,
|
enabled: !!pageInput.pageId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.data) {
|
||||||
|
if (isValidUuid(pageInput.pageId)) {
|
||||||
|
queryClient.setQueryData(["pages", query.data.slugId], query.data);
|
||||||
|
} else {
|
||||||
|
queryClient.setQueryData(["pages", query.data.id], query.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [query.data]);
|
||||||
|
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreatePageMutation() {
|
export function useCreatePageMutation() {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||||
import {
|
import {
|
||||||
getPageBreadcrumbs,
|
getPageBreadcrumbs,
|
||||||
|
getPageById,
|
||||||
getSidebarPages,
|
getSidebarPages,
|
||||||
} from "@/features/page/services/page-service.ts";
|
} from "@/features/page/services/page-service.ts";
|
||||||
import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
||||||
@ -232,6 +233,24 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
|||||||
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
||||||
const emit = useQueryEmit();
|
const emit = useQueryEmit();
|
||||||
const { spaceSlug } = useParams();
|
const { spaceSlug } = useParams();
|
||||||
|
const timerRef = useRef(null);
|
||||||
|
|
||||||
|
const prefetchPage = () => {
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: ["pages", node.data.slugId],
|
||||||
|
queryFn: () => getPageById({ pageId: node.data.slugId }),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelPagePrefetch = () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
window.clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function handleLoadChildren(node: NodeApi<SpaceTreeNode>) {
|
async function handleLoadChildren(node: NodeApi<SpaceTreeNode>) {
|
||||||
if (!node.data.hasChildren) return;
|
if (!node.data.hasChildren) return;
|
||||||
@ -330,6 +349,8 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
|||||||
className={clsx(classes.node, node.state)}
|
className={clsx(classes.node, node.state)}
|
||||||
ref={dragHandle}
|
ref={dragHandle}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onMouseEnter={prefetchPage}
|
||||||
|
onMouseLeave={cancelPagePrefetch}
|
||||||
>
|
>
|
||||||
<PageArrow node={node} onExpandTree={() => handleLoadChildren(node)} />
|
<PageArrow node={node} onExpandTree={() => handleLoadChildren(node)} />
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
.control {
|
|
||||||
font-weight: 500;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-xs);
|
|
||||||
color: var(--mantine-color-text);
|
|
||||||
font-size: var(--mantine-font-size-sm);
|
|
||||||
|
|
||||||
@mixin hover {
|
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
|
||||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
|
||||||
padding-left: 4px;
|
|
||||||
margin-left: var(--mantine-spacing-sm);
|
|
||||||
font-size: var(--mantine-font-size-sm);
|
|
||||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron {
|
|
||||||
transition: transform 200ms ease;
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import React, { ReactNode, useState } from "react";
|
|
||||||
import {
|
|
||||||
Group,
|
|
||||||
Box,
|
|
||||||
Collapse,
|
|
||||||
ThemeIcon,
|
|
||||||
UnstyledButton,
|
|
||||||
rem,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { IconChevronRight } from "@tabler/icons-react";
|
|
||||||
import classes from "./tree-collapse.module.css";
|
|
||||||
|
|
||||||
interface TreeCollapseProps {
|
|
||||||
icon?: React.FC<any>;
|
|
||||||
label: string;
|
|
||||||
initiallyOpened?: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TreeCollapse({
|
|
||||||
icon: Icon,
|
|
||||||
label,
|
|
||||||
initiallyOpened,
|
|
||||||
children,
|
|
||||||
}: TreeCollapseProps) {
|
|
||||||
const [opened, setOpened] = useState(initiallyOpened || false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<UnstyledButton
|
|
||||||
onClick={() => setOpened((o) => !o)}
|
|
||||||
className={classes.control}
|
|
||||||
>
|
|
||||||
<Group justify="space-between" gap={0}>
|
|
||||||
<Box style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<ThemeIcon variant="light" size={20}>
|
|
||||||
<Icon style={{ width: rem(18), height: rem(18) }} />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Box ml="md">{label}</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<IconChevronRight
|
|
||||||
className={classes.chevron}
|
|
||||||
stroke={1.5}
|
|
||||||
style={{
|
|
||||||
width: rem(16),
|
|
||||||
height: rem(16),
|
|
||||||
transform: opened ? "rotate(90deg)" : "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</UnstyledButton>
|
|
||||||
|
|
||||||
<Collapse in={opened}>
|
|
||||||
<div className={classes.item}>{children}</div>
|
|
||||||
</Collapse>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -25,7 +25,7 @@ export default function Page() {
|
|||||||
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
|
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
|
||||||
|
|
||||||
const spaceRules = space?.membership?.permissions;
|
const spaceRules = space?.membership?.permissions;
|
||||||
const spaceAbility = useSpaceAbility(spaceRules);
|
const spaceAbility = useSpaceAbility(spaceRules);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@ -55,8 +55,10 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FullEditor
|
<FullEditor
|
||||||
|
key={page.id}
|
||||||
pageId={page.id}
|
pageId={page.id}
|
||||||
title={page.title}
|
title={page.title}
|
||||||
|
content={page.content}
|
||||||
slugId={page.slugId}
|
slugId={page.slugId}
|
||||||
spaceSlug={page?.space?.slug}
|
spaceSlug={page?.space?.slug}
|
||||||
editable={spaceAbility.can(
|
editable={spaceAbility.can(
|
||||||
|
|||||||
@ -26,4 +26,8 @@ export class PageInfoDto extends PageIdDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeSpace: boolean;
|
includeSpace: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includeContent: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export class PageController {
|
|||||||
async getPage(@Body() dto: PageInfoDto, @AuthUser() user: User) {
|
async getPage(@Body() dto: PageInfoDto, @AuthUser() user: User) {
|
||||||
const page = await this.pageRepo.findById(dto.pageId, {
|
const page = await this.pageRepo.findById(dto.pageId, {
|
||||||
includeSpace: true,
|
includeSpace: true,
|
||||||
|
includeContent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
|
|||||||
@ -38,7 +38,6 @@
|
|||||||
"@tiptap/extension-link": "^2.10.3",
|
"@tiptap/extension-link": "^2.10.3",
|
||||||
"@tiptap/extension-list-item": "^2.10.3",
|
"@tiptap/extension-list-item": "^2.10.3",
|
||||||
"@tiptap/extension-list-keymap": "^2.10.3",
|
"@tiptap/extension-list-keymap": "^2.10.3",
|
||||||
"@tiptap/extension-mention": "^2.10.3",
|
|
||||||
"@tiptap/extension-placeholder": "^2.10.3",
|
"@tiptap/extension-placeholder": "^2.10.3",
|
||||||
"@tiptap/extension-subscript": "^2.10.3",
|
"@tiptap/extension-subscript": "^2.10.3",
|
||||||
"@tiptap/extension-superscript": "^2.10.3",
|
"@tiptap/extension-superscript": "^2.10.3",
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -79,9 +79,6 @@ importers:
|
|||||||
'@tiptap/extension-list-keymap':
|
'@tiptap/extension-list-keymap':
|
||||||
specifier: ^2.10.3
|
specifier: ^2.10.3
|
||||||
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))
|
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))
|
||||||
'@tiptap/extension-mention':
|
|
||||||
specifier: ^2.10.3
|
|
||||||
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(@tiptap/suggestion@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3))
|
|
||||||
'@tiptap/extension-placeholder':
|
'@tiptap/extension-placeholder':
|
||||||
specifier: ^2.10.3
|
specifier: ^2.10.3
|
||||||
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)
|
version: 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)
|
||||||
@ -3590,13 +3587,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
|
|
||||||
'@tiptap/extension-mention@2.10.3':
|
|
||||||
resolution: {integrity: sha512-h0+BrTS2HdjMfsuy6zkFIqmVGYL8w3jIG0gYaDHjWwwe/Lf2BDgOu3bZWcSr/3bKiJIwwzpOJrXssqta4TZ0yQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.7.0
|
|
||||||
'@tiptap/pm': ^2.7.0
|
|
||||||
'@tiptap/suggestion': ^2.7.0
|
|
||||||
|
|
||||||
'@tiptap/extension-ordered-list@2.10.3':
|
'@tiptap/extension-ordered-list@2.10.3':
|
||||||
resolution: {integrity: sha512-/SFuEDnbJxy3jvi72LeyiPHWkV+uFc0LUHTUHSh20vwyy+tLrzncJfXohGbTIv5YxYhzExQYZDRD4VbSghKdlw==}
|
resolution: {integrity: sha512-/SFuEDnbJxy3jvi72LeyiPHWkV+uFc0LUHTUHSh20vwyy+tLrzncJfXohGbTIv5YxYhzExQYZDRD4VbSghKdlw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -12362,12 +12352,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
||||||
|
|
||||||
'@tiptap/extension-mention@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)(@tiptap/suggestion@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3))':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
|
||||||
'@tiptap/pm': 2.10.3
|
|
||||||
'@tiptap/suggestion': 2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))(@tiptap/pm@2.10.3)
|
|
||||||
|
|
||||||
'@tiptap/extension-ordered-list@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))':
|
'@tiptap/extension-ordered-list@2.10.3(@tiptap/core@2.10.3(@tiptap/pm@2.10.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
|
||||||
|
|||||||
Reference in New Issue
Block a user