mirror of
https://github.com/docmost/docmost.git
synced 2026-05-18 07:24:04 +08:00
feat: public page sharing (#1012)
* Share - WIP * - public attachment links - WIP * WIP * WIP * Share - WIP * WIP * WIP * include userRole in space object * WIP * Server render shared page meta tags * disable user select * Close Navbar on outside click on mobile * update shared page spaceId * WIP * fix * close sidebar on click * close sidebar * defaults * update copy * Store share key in lowercase * refactor page breadcrumbs * Change copy * add link ref * open link button * add meta og:title * add twitter tags * WIP * make shares/info endpoint public * fix * * add /p/ segment to share urls * minore fixes * change mobile breadcrumb icon
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import { ISharedPageTree } from "@/features/share/types/share.types.ts";
|
||||
import { NodeApi, NodeRendererProps, Tree, TreeApi } from "react-arborist";
|
||||
import {
|
||||
buildSharedPageTree,
|
||||
SharedPageTreeNode,
|
||||
} from "@/features/share/utils.ts";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useElementSize, useMergedRef } from "@mantine/hooks";
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { atom, useAtom } from "jotai/index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconPointFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { ActionIcon, Box } from "@mantine/core";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
||||
import classes from "@/features/page/tree/styles/tree.module.css";
|
||||
import styles from "./share.module.css";
|
||||
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
|
||||
interface SharedTree {
|
||||
sharedPageTree: ISharedPageTree;
|
||||
}
|
||||
|
||||
const openSharedTreeNodesAtom = atom<OpenMap>({});
|
||||
|
||||
export default function SharedTree({ sharedPageTree }: SharedTree) {
|
||||
const [tree, setTree] = useState<
|
||||
TreeApi<SharedPageTreeNode> | null | undefined
|
||||
>(null);
|
||||
const rootElement = useRef<HTMLDivElement>();
|
||||
const { ref: sizeRef, width, height } = useElementSize();
|
||||
const mergedRef = useMergedRef(rootElement, sizeRef);
|
||||
const { pageSlug } = useParams();
|
||||
const [openTreeNodes, setOpenTreeNodes] = useAtom<OpenMap>(
|
||||
openSharedTreeNodesAtom,
|
||||
);
|
||||
|
||||
const currentNodeId = extractPageSlugId(pageSlug);
|
||||
|
||||
const treeData: SharedPageTreeNode[] = useMemo(() => {
|
||||
if (!sharedPageTree?.pageTree) return;
|
||||
return buildSharedPageTree(sharedPageTree.pageTree);
|
||||
}, [sharedPageTree?.pageTree]);
|
||||
|
||||
useEffect(() => {
|
||||
const parentNodeId = treeData?.[0]?.slugId;
|
||||
|
||||
if (parentNodeId && tree) {
|
||||
const parentNode = tree.get(parentNodeId);
|
||||
|
||||
setTimeout(() => {
|
||||
if (parentNode) {
|
||||
tree.openSiblings(parentNode);
|
||||
}
|
||||
});
|
||||
|
||||
// open direct children of parent node
|
||||
parentNode?.children.forEach((node) => {
|
||||
tree.openSiblings(node);
|
||||
});
|
||||
}
|
||||
}, [treeData, tree]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentNodeId && tree) {
|
||||
setTimeout(() => {
|
||||
// focus on node and open all parents
|
||||
tree?.select(currentNodeId, { align: "auto" });
|
||||
}, 200);
|
||||
} else {
|
||||
tree?.deselectAll();
|
||||
}
|
||||
}, [currentNodeId, tree]);
|
||||
|
||||
if (!sharedPageTree || !sharedPageTree?.pageTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={mergedRef} className={classes.treeContainer}>
|
||||
{rootElement.current && (
|
||||
<Tree
|
||||
data={treeData}
|
||||
disableDrag={true}
|
||||
disableDrop={true}
|
||||
disableEdit={true}
|
||||
width={width}
|
||||
height={rootElement.current.clientHeight}
|
||||
ref={(t) => setTree(t)}
|
||||
openByDefault={false}
|
||||
disableMultiSelection={true}
|
||||
className={classes.tree}
|
||||
rowClassName={classes.row}
|
||||
rowHeight={30}
|
||||
overscanCount={10}
|
||||
dndRootElement={rootElement.current}
|
||||
onToggle={() => {
|
||||
setOpenTreeNodes(tree?.openState);
|
||||
}}
|
||||
initialOpenState={openTreeNodes}
|
||||
onClick={(e) => {
|
||||
if (tree && tree.focusedNode) {
|
||||
tree.select(tree.focusedNode);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Node}
|
||||
</Tree>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Node({ node, style, tree }: NodeRendererProps<any>) {
|
||||
const { shareId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const [, setMobileSidebarState] = useAtom(mobileSidebarAtom);
|
||||
|
||||
const pageUrl = buildSharedPageUrl({
|
||||
shareId: shareId,
|
||||
pageSlugId: node.data.slugId,
|
||||
pageTitle: node.data.name,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
style={style}
|
||||
className={clsx(classes.node, node.state, styles.treeNode)}
|
||||
component={Link}
|
||||
to={pageUrl}
|
||||
onClick={() => {
|
||||
setMobileSidebarState(false);
|
||||
}}
|
||||
>
|
||||
<PageArrow node={node} />
|
||||
<span className={classes.text}>{node.data.name || t("untitled")}</span>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageArrowProps {
|
||||
node: NodeApi<SpaceTreeNode>;
|
||||
}
|
||||
|
||||
function PageArrow({ node }: PageArrowProps) {
|
||||
return (
|
||||
<ActionIcon
|
||||
size={20}
|
||||
variant="subtle"
|
||||
c="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
node.toggle();
|
||||
}}
|
||||
>
|
||||
{node.isInternal ? (
|
||||
node.children && (node.children.length > 0 || node.data.hasChildren) ? (
|
||||
node.isOpen ? (
|
||||
<IconChevronDown stroke={2} size={16} />
|
||||
) : (
|
||||
<IconChevronRight stroke={2} size={16} />
|
||||
)
|
||||
) : (
|
||||
<IconPointFilled size={4} />
|
||||
)
|
||||
) : null}
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user