mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
feat(beta): public spaces (#2473)
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const openSharedTreeNodesAtom = atom<Record<string, boolean>>({});
|
||||
@@ -1,11 +1,10 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { ISharedPageTree } from "@/features/share/types/share.types";
|
||||
import { SharedPageTreeNode } from "@/features/share/utils";
|
||||
|
||||
export const sharedPageTreeAtom = atom<ISharedPageTree | null>(null);
|
||||
export const sharedTreeDataAtom = atom<SharedPageTreeNode[] | null>(null);
|
||||
export const sharedPageFullWidthAtom = atomWithStorage<boolean>(
|
||||
"sharedPageFullWidth",
|
||||
false,
|
||||
);
|
||||
export const sharedPageTreeAtom = atom<ISharedPageTree | null>(
|
||||
null as ISharedPageTree | null,
|
||||
);
|
||||
export const sharedTreeDataAtom = atom<SharedPageTreeNode[] | null>(
|
||||
null as SharedPageTreeNode[] | null,
|
||||
);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { atomWithWebStorage } from "@/lib/jotai-helper.ts";
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const tableOfContentAsideAtom = atomWithWebStorage<boolean>(
|
||||
"showTOC",
|
||||
true,
|
||||
);
|
||||
|
||||
export const mobileTableOfContentAsideAtom = atom<boolean>(false);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Affix, Button } from "@mantine/core";
|
||||
|
||||
export default function ShareBranding() {
|
||||
return (
|
||||
<Affix position={{ bottom: 20, right: 20 }}>
|
||||
<Button
|
||||
variant="default"
|
||||
component="a"
|
||||
target="_blank"
|
||||
href="https://docmost.com?ref=public-share"
|
||||
>
|
||||
Powered by Docmost
|
||||
</Button>
|
||||
</Affix>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,67 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import ShareShell from "@/features/share/components/share-shell.tsx";
|
||||
import "@fontsource-variable/inter";
|
||||
import "@/styles/public-typography.css";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Outlet, useParams } from "react-router-dom";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { buildSharedPageTree } from "@/features/share/utils.ts";
|
||||
import {
|
||||
sharedPageTreeAtom,
|
||||
sharedTreeDataAtom,
|
||||
} from "@/features/share/atoms/shared-page-atom.ts";
|
||||
import DocsShell from "@/features/public-space/components/docs/docs-shell.tsx";
|
||||
import { DocsSurface } from "@/features/public-space/components/docs/docs-surface-context.tsx";
|
||||
import { useDocsAccent } from "@/features/public-space/theme/docs-theme.ts";
|
||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { ShareSearchSpotlight } from "@/features/search/components/share-search-spotlight.tsx";
|
||||
import { shareSearchSpotlight } from "@/features/search/constants";
|
||||
|
||||
export default function ShareLayout() {
|
||||
const { shareId } = useParams();
|
||||
const { data } = useGetSharedPageTreeQuery(shareId);
|
||||
|
||||
// Shares have no appearance settings; apply the default docs accent.
|
||||
useDocsAccent(undefined);
|
||||
|
||||
const setSharedPageTree = useSetAtom(sharedPageTreeAtom);
|
||||
const setSharedTreeData = useSetAtom(sharedTreeDataAtom);
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
if (!data?.pageTree) return null;
|
||||
return buildSharedPageTree(data.pageTree);
|
||||
}, [data?.pageTree]);
|
||||
|
||||
useEffect(() => {
|
||||
setSharedPageTree(data || null);
|
||||
setSharedTreeData(treeData);
|
||||
}, [data, treeData, setSharedPageTree, setSharedTreeData]);
|
||||
|
||||
const surface = useMemo<DocsSurface>(
|
||||
() => ({
|
||||
treeData,
|
||||
hasSidebar: (data?.pageTree?.length ?? 0) > 1,
|
||||
getNodeUrl: (node) =>
|
||||
buildSharedPageUrl({
|
||||
shareId,
|
||||
pageSlugId: node.slugId,
|
||||
pageTitle: node.name,
|
||||
}),
|
||||
showBranding: Boolean(data),
|
||||
showEditPage: true,
|
||||
brandingRef: "public-share",
|
||||
}),
|
||||
[data, treeData, shareId],
|
||||
);
|
||||
|
||||
return (
|
||||
<ShareShell>
|
||||
<DocsShell
|
||||
surface={surface}
|
||||
onSearchOpen={shareId ? shareSearchSpotlight.open : undefined}
|
||||
searchSpotlight={
|
||||
shareId ? <ShareSearchSpotlight shareId={shareId} /> : undefined
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</ShareShell>
|
||||
</DocsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
Group,
|
||||
ScrollArea,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { useParams } from "react-router-dom";
|
||||
import SharedTree from "@/features/share/components/shared-tree.tsx";
|
||||
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
|
||||
import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||
import { ThemeToggle } from "@/components/theme-toggle.tsx";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
sharedPageFullWidthAtom,
|
||||
sharedPageTreeAtom,
|
||||
sharedTreeDataAtom,
|
||||
} from "@/features/share/atoms/shared-page-atom";
|
||||
import { buildSharedPageTree } from "@/features/share/utils";
|
||||
import {
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
sidebarWidthAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import {
|
||||
mobileTableOfContentAsideAtom,
|
||||
tableOfContentAsideAtom,
|
||||
} from "@/features/share/atoms/sidebar-atom.ts";
|
||||
import { IconArrowsHorizontal, IconList } from "@tabler/icons-react";
|
||||
import { useToggleToc } from "@/features/share/hooks/use-toggle-toc.ts";
|
||||
import classes from "./share.module.css";
|
||||
import {
|
||||
SearchControl,
|
||||
SearchMobileControl,
|
||||
} from "@/features/search/components/search-control.tsx";
|
||||
import { ShareSearchSpotlight } from "@/features/search/components/share-search-spotlight.tsx";
|
||||
import { shareSearchSpotlight } from "@/features/search/constants";
|
||||
import ShareBranding from '@/features/share/components/share-branding.tsx';
|
||||
import { MAIN_CONTENT_ID, SkipToMain } from "@/components/ui/skip-to-main.tsx";
|
||||
|
||||
const MemoizedSharedTree = React.memo(SharedTree);
|
||||
|
||||
export default function ShareShell({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
|
||||
|
||||
const [tocOpened] = useAtom(tableOfContentAsideAtom);
|
||||
const [mobileTocOpened] = useAtom(mobileTableOfContentAsideAtom);
|
||||
const toggleTocMobile = useToggleToc(mobileTableOfContentAsideAtom);
|
||||
const toggleToc = useToggleToc(tableOfContentAsideAtom);
|
||||
const [fullWidth, setFullWidth] = useAtom(sharedPageFullWidthAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const startResizing = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const resize = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isResizing || !sidebarRef.current) return;
|
||||
const newWidth =
|
||||
e.clientX - sidebarRef.current.getBoundingClientRect().left;
|
||||
if (newWidth < 220) {
|
||||
setSidebarWidth(220);
|
||||
return;
|
||||
}
|
||||
if (newWidth > 600) {
|
||||
setSidebarWidth(600);
|
||||
return;
|
||||
}
|
||||
setSidebarWidth(newWidth);
|
||||
},
|
||||
[isResizing, setSidebarWidth],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("mousemove", resize);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", resize);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
|
||||
const { shareId } = useParams();
|
||||
const { data } = useGetSharedPageTreeQuery(shareId);
|
||||
const readOnlyEditor = useAtomValue(readOnlyEditorAtom);
|
||||
|
||||
// @ts-ignore
|
||||
const setSharedPageTree = useSetAtom(sharedPageTreeAtom);
|
||||
// @ts-ignore
|
||||
const setSharedTreeData = useSetAtom(sharedTreeDataAtom);
|
||||
|
||||
// Build and set the tree data when it changes
|
||||
const treeData = useMemo(() => {
|
||||
if (!data?.pageTree) return null;
|
||||
return buildSharedPageTree(data.pageTree);
|
||||
}, [data?.pageTree]);
|
||||
|
||||
useEffect(() => {
|
||||
setSharedPageTree(data || null);
|
||||
setSharedTreeData(treeData);
|
||||
}, [data, treeData, setSharedPageTree, setSharedTreeData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkipToMain />
|
||||
<AppShell
|
||||
header={{ height: 50 }}
|
||||
{...(data?.pageTree?.length > 1 && {
|
||||
navbar: {
|
||||
width: sidebarWidth,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileOpened,
|
||||
desktop: !desktopOpened,
|
||||
},
|
||||
},
|
||||
})}
|
||||
aside={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileTocOpened,
|
||||
desktop: !tocOpened,
|
||||
},
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Group wrap="nowrap" justify="space-between" py="sm" px="xl">
|
||||
<Group wrap="nowrap">
|
||||
{data?.pageTree?.length > 1 && (
|
||||
<>
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{shareId && (
|
||||
<Group visibleFrom="sm">
|
||||
<SearchControl onClick={shareSearchSpotlight.open} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Group>
|
||||
<>
|
||||
{shareId && (
|
||||
<Group hiddenFrom="sm">
|
||||
<SearchMobileControl onSearch={shareSearchSpotlight.open} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Tooltip label={t("Table of contents")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
onClick={toggleTocMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
aria-label={t("Table of contents")}
|
||||
>
|
||||
<IconList size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Table of contents")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
aria-label={t("Table of contents")}
|
||||
onClick={toggleToc}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
>
|
||||
<IconList size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Full width")} withArrow>
|
||||
<ActionIcon
|
||||
variant={fullWidth ? "light" : "default"}
|
||||
style={fullWidth ? undefined : { border: "none" }}
|
||||
aria-label={t("Full width")}
|
||||
aria-pressed={fullWidth}
|
||||
onClick={() => setFullWidth((v) => !v)}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
>
|
||||
<IconArrowsHorizontal size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
<ThemeToggle />
|
||||
</Group>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
{data?.pageTree?.length > 1 && (
|
||||
<AppShell.Navbar p="md" className={classes.navbar} ref={sidebarRef}>
|
||||
<div
|
||||
className={classes.resizeHandle}
|
||||
onMouseDown={startResizing}
|
||||
/>
|
||||
<MemoizedSharedTree sharedPageTree={data} />
|
||||
</AppShell.Navbar>
|
||||
)}
|
||||
|
||||
<AppShell.Main id={MAIN_CONTENT_ID} tabIndex={-1}>
|
||||
{children}
|
||||
|
||||
{data && shareId && !(data.features?.length > 0) && <ShareBranding />}
|
||||
</AppShell.Main>
|
||||
|
||||
<AppShell.Aside
|
||||
p="md"
|
||||
withBorder={mobileTocOpened}
|
||||
className={classes.aside}
|
||||
>
|
||||
<ScrollArea style={{ height: "80vh" }} scrollbarSize={5} type="scroll">
|
||||
<div style={{ paddingBottom: "50px" }}>
|
||||
{readOnlyEditor && (
|
||||
<TableOfContents isShare={true} editor={readOnlyEditor} />
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</AppShell.Aside>
|
||||
|
||||
<ShareSearchSpotlight shareId={shareId} />
|
||||
</AppShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,39 +6,3 @@
|
||||
border-bottom: 0.05em solid var(--mantine-color-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
.treeNode {
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.aside {
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
width: 3px;
|
||||
cursor: col-resize;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
width: 5px;
|
||||
background: light-dark(
|
||||
var(--mantine-color-gray-4),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import { ISharedPageTree } from "@/features/share/types/share.types.ts";
|
||||
import {
|
||||
buildSharedPageTree,
|
||||
SharedPageTreeNode,
|
||||
} from "@/features/share/utils.ts";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconFileDescription,
|
||||
IconPointFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { ActionIcon, Box } from "@mantine/core";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
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";
|
||||
import EmojiPicker from "@/components/ui/emoji-picker.tsx";
|
||||
import {
|
||||
DocTree,
|
||||
type DocTreeApi,
|
||||
type RenderRowProps,
|
||||
} from "@/features/page/tree/components/doc-tree";
|
||||
import { openSharedTreeNodesAtom } from "@/features/share/atoms/open-shared-tree-nodes-atom";
|
||||
|
||||
interface SharedTreeProps {
|
||||
sharedPageTree: ISharedPageTree;
|
||||
}
|
||||
|
||||
export default function SharedTree({ sharedPageTree }: SharedTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
const treeRef = useRef<DocTreeApi | null>(null);
|
||||
const { pageSlug } = useParams();
|
||||
const [openTreeNodes, setOpenTreeNodes] = useAtom(openSharedTreeNodesAtom);
|
||||
|
||||
const currentNodeId = extractPageSlugId(pageSlug);
|
||||
|
||||
const treeData: SharedPageTreeNode[] = useMemo(() => {
|
||||
if (!sharedPageTree?.pageTree) return [] as SharedPageTreeNode[];
|
||||
return buildSharedPageTree(sharedPageTree.pageTree);
|
||||
}, [sharedPageTree?.pageTree]);
|
||||
|
||||
const openIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
Object.keys(openTreeNodes).filter((k) => openTreeNodes[k]),
|
||||
),
|
||||
[openTreeNodes],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-open the first level of the shared tree on initial load.
|
||||
const root = treeData?.[0];
|
||||
if (!root) return;
|
||||
setOpenTreeNodes((prev) => {
|
||||
if (prev[root.slugId]) return prev;
|
||||
const next = { ...prev, [root.slugId]: true };
|
||||
for (const child of root.children ?? []) {
|
||||
next[child.slugId] = true;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [treeData, setOpenTreeNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentNodeId) {
|
||||
treeRef.current?.select(currentNodeId, { scrollIntoView: true });
|
||||
}
|
||||
}, [currentNodeId, treeData]);
|
||||
|
||||
// Stable callbacks so memo(DocTreeRow) actually saves work — see I2 in the
|
||||
// post-implementation code review.
|
||||
const handleToggle = useCallback(
|
||||
(id: string, isOpen: boolean) =>
|
||||
setOpenTreeNodes((prev) => ({ ...prev, [id]: isOpen })),
|
||||
[setOpenTreeNodes],
|
||||
);
|
||||
const getDragLabel = useCallback(
|
||||
(n: SharedPageTreeNode) => n.name || "untitled",
|
||||
[],
|
||||
);
|
||||
|
||||
if (!sharedPageTree || !sharedPageTree?.pageTree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.treeContainer}>
|
||||
<DocTree<SharedPageTreeNode>
|
||||
readOnly
|
||||
ref={treeRef}
|
||||
data={treeData}
|
||||
openIds={openIds}
|
||||
selectedId={currentNodeId}
|
||||
renderRow={SharedTreeRow}
|
||||
onMove={noopMove}
|
||||
onToggle={handleToggle}
|
||||
getDragLabel={getDragLabel}
|
||||
aria-label={t("Pages")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Module-scope noop so it's a stable reference across renders.
|
||||
const noopMove = () => {};
|
||||
|
||||
function SharedTreeRow({
|
||||
node,
|
||||
isOpen,
|
||||
hasChildren,
|
||||
isSelected,
|
||||
rowRef,
|
||||
tabIndex,
|
||||
treeItemProps,
|
||||
toggleOpen,
|
||||
}: RenderRowProps<SharedPageTreeNode>) {
|
||||
const { shareId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const [, setMobileSidebarState] = useAtom(mobileSidebarAtom);
|
||||
|
||||
const pageUrl = buildSharedPageUrl({
|
||||
shareId: shareId,
|
||||
pageSlugId: node.slugId,
|
||||
pageTitle: node.name,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={rowRef as React.Ref<HTMLAnchorElement>}
|
||||
tabIndex={tabIndex}
|
||||
{...treeItemProps}
|
||||
data-selected={isSelected || undefined}
|
||||
className={clsx(classes.node, styles.treeNode)}
|
||||
component={Link}
|
||||
to={pageUrl}
|
||||
onClick={() => {
|
||||
setMobileSidebarState(false);
|
||||
}}
|
||||
>
|
||||
<SharedPageArrow
|
||||
isOpen={isOpen}
|
||||
hasChildren={hasChildren}
|
||||
onToggle={toggleOpen}
|
||||
/>
|
||||
<div style={{ marginRight: "4px" }}>
|
||||
<EmojiPicker
|
||||
onEmojiSelect={() => {}}
|
||||
icon={
|
||||
node.icon ? (
|
||||
node.icon
|
||||
) : (
|
||||
<IconFileDescription size="18" />
|
||||
)
|
||||
}
|
||||
readOnly={true}
|
||||
removeEmojiAction={() => {}}
|
||||
actionIconProps={{ tabIndex: -1 }}
|
||||
/>
|
||||
</div>
|
||||
<span className={classes.text}>{node.name || t("untitled")}</span>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface SharedPageArrowProps {
|
||||
isOpen: boolean;
|
||||
hasChildren: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
function SharedPageArrow({
|
||||
isOpen,
|
||||
hasChildren,
|
||||
onToggle,
|
||||
}: SharedPageArrowProps) {
|
||||
if (!hasChildren) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "var(--mantine-color-gray-6)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IconPointFilled size={4} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
size={20}
|
||||
variant="subtle"
|
||||
c="gray"
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
{isOpen ? (
|
||||
<IconChevronDown stroke={2} size={16} />
|
||||
) : (
|
||||
<IconChevronRight stroke={2} size={16} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { sharedTreeDataAtom } from "@/features/share/atoms/shared-page-atom";
|
||||
import { SharedPageTreeNode } from "@/features/share/utils";
|
||||
import { findSubpagesInTree } from "@/features/share/utils";
|
||||
|
||||
export function useSharedPageSubpages(pageId: string | undefined) {
|
||||
const treeData = useAtomValue(sharedTreeDataAtom);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!treeData || !pageId) return [];
|
||||
|
||||
function findSubpages(nodes: SharedPageTreeNode[]): SharedPageTreeNode[] {
|
||||
for (const node of nodes) {
|
||||
if (node.value === pageId || node.slugId === pageId) {
|
||||
return node.children || [];
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const subpages = findSubpages(node.children);
|
||||
if (subpages.length > 0) {
|
||||
return subpages;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return findSubpages(treeData);
|
||||
}, [treeData, pageId]);
|
||||
return useMemo(
|
||||
() => findSubpagesInTree(treeData, pageId),
|
||||
[treeData, pageId],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
export function useToggleToc(tocAtom: any) {
|
||||
const [tocState, setTocState] = useAtom(tocAtom);
|
||||
return () => {
|
||||
setTocState(!tocState);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,27 @@ export function buildSharedPageTree(
|
||||
}
|
||||
|
||||
|
||||
// Returns the children of the node matching the page id or slugId.
|
||||
export function findSubpagesInTree(
|
||||
tree: SharedPageTreeNode[] | null | undefined,
|
||||
pageId: string | undefined,
|
||||
): SharedPageTreeNode[] {
|
||||
if (!tree || !pageId) return [];
|
||||
|
||||
for (const node of tree) {
|
||||
if (node.value === pageId || node.slugId === pageId) {
|
||||
return node.children || [];
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
const subpages = findSubpagesInTree(node.children, pageId);
|
||||
if (subpages.length > 0) {
|
||||
return subpages;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Recursively checks if a page exists in the shared page tree.
|
||||
export function isPageInTree(
|
||||
tree: SharedPageTreeNode[],
|
||||
|
||||
Reference in New Issue
Block a user