mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 04:01:10 +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:
@ -8,6 +8,7 @@ import {
|
||||
IconMessage,
|
||||
IconPrinter,
|
||||
IconTrash,
|
||||
IconWifiOff,
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
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 { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.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 ExportModal from "@/components/common/export-modal";
|
||||
import { yjsConnectionStatusAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||
|
||||
interface PageHeaderMenuProps {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||
const toggleAside = useToggleAside();
|
||||
const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom);
|
||||
|
||||
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>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
|
||||
@ -25,17 +25,31 @@ import { notifications } from "@mantine/notifications";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
import { queryClient } from "@/main.tsx";
|
||||
import { buildTree } from "@/features/page/tree/utils";
|
||||
import { useEffect } from "react";
|
||||
import { validate as isValidUuid } from "uuid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function usePageQuery(
|
||||
pageInput: Partial<IPageInput>,
|
||||
): UseQueryResult<IPage, Error> {
|
||||
return useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: ["pages", pageInput.pageId],
|
||||
queryFn: () => getPageById(pageInput),
|
||||
enabled: !!pageInput.pageId,
|
||||
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() {
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import {
|
||||
getPageBreadcrumbs,
|
||||
getPageById,
|
||||
getSidebarPages,
|
||||
} from "@/features/page/services/page-service.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 emit = useQueryEmit();
|
||||
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>) {
|
||||
if (!node.data.hasChildren) return;
|
||||
@ -330,6 +349,8 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
||||
className={clsx(classes.node, node.state)}
|
||||
ref={dragHandle}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={prefetchPage}
|
||||
onMouseLeave={cancelPagePrefetch}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user