mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
feat(beta): public spaces
This commit is contained in:
@@ -85,6 +85,9 @@ export function AudioMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`audio-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
|
||||
@@ -121,12 +121,14 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`callout-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
placement: "bottom",
|
||||
// offset: 233, // // offset: [0, 10],
|
||||
// zIndex: 99,
|
||||
flip: false,
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
|
||||
@@ -257,6 +257,9 @@ export function ColumnsMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey="columns-menu"
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
|
||||
@@ -273,6 +273,9 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`drawio-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
|
||||
@@ -156,6 +156,9 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`image-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
|
||||
@@ -26,7 +26,12 @@ import { INTERNAL_LINK_REGEX } from "@/lib/constants";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { useSharePageQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { usePublicSpacePageQuery } from "@/features/public-space/queries/public-space-query.ts";
|
||||
import {
|
||||
buildPageUrl,
|
||||
buildPublicSpaceUrl,
|
||||
buildSharedPageUrl,
|
||||
} from "@/features/page/page.utils.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { sanitizeUrl, copyToClipboard, isEditorReady } from "@docmost/editor-ext";
|
||||
import { normalizeUrl } from "@/lib/utils";
|
||||
@@ -60,9 +65,10 @@ export default function LinkView(props: MarkViewProps) {
|
||||
const href = mark.attrs.href as string;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { shareId, pageSlug } = useParams();
|
||||
const { shareId, spaceSlug, pageSlug } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const isShareRoute = location.pathname.startsWith("/share");
|
||||
const isPublicSpaceRoute = location.pathname.startsWith("/docs/");
|
||||
|
||||
const [popoverState, setPopoverState] = useState<
|
||||
"closed" | "preview" | "edit"
|
||||
@@ -84,16 +90,33 @@ export default function LinkView(props: MarkViewProps) {
|
||||
const activeView = isPopoverVisible ? popoverState : lastOpenState.current;
|
||||
|
||||
const { data: linkedPage } = usePageQuery({
|
||||
pageId: isPopoverVisible && slugId && !isShareRoute ? slugId : null,
|
||||
pageId:
|
||||
isPopoverVisible && slugId && !isShareRoute && !isPublicSpaceRoute
|
||||
? slugId
|
||||
: null,
|
||||
});
|
||||
|
||||
const { data: sharedPageData } = useSharePageQuery({
|
||||
pageId: isPopoverVisible && slugId && isShareRoute ? slugId : null,
|
||||
});
|
||||
|
||||
const pageTitle = isShareRoute
|
||||
? sharedPageData?.page?.title
|
||||
: linkedPage?.title;
|
||||
// Resolved eagerly (not gated on the popover): an unresolvable target must
|
||||
// render as inert text rather than a link that dead-ends at /login.
|
||||
const { data: publicSpacePageData } = usePublicSpacePageQuery({
|
||||
spaceSlug: isPublicSpaceRoute && slugId ? spaceSlug : undefined,
|
||||
pageSlugId: slugId,
|
||||
contentless: true,
|
||||
});
|
||||
|
||||
const isUnresolvedPublicLink =
|
||||
isPublicSpaceRoute && isInternal && !publicSpacePageData?.page && !slugId;
|
||||
|
||||
let pageTitle = linkedPage?.title;
|
||||
if (isShareRoute) {
|
||||
pageTitle = sharedPageData?.page?.title;
|
||||
} else if (isPublicSpaceRoute) {
|
||||
pageTitle = publicSpacePageData?.page?.title;
|
||||
}
|
||||
|
||||
const pendingTitleRef = useRef<string | null>(null);
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -260,6 +283,27 @@ export default function LinkView(props: MarkViewProps) {
|
||||
anchorId: anchor || undefined,
|
||||
});
|
||||
navigate(sharedUrl);
|
||||
} else if (isPublicSpaceRoute) {
|
||||
if (slugId && publicSpacePageData?.page) {
|
||||
navigate(
|
||||
buildPublicSpaceUrl({
|
||||
// cross-space targets resolve to their own space's public URL
|
||||
spaceSlug: publicSpacePageData.space?.slug ?? spaceSlug,
|
||||
pageSlugId: slugId,
|
||||
pageTitle: pageTitle,
|
||||
anchorId: anchor || undefined,
|
||||
}),
|
||||
);
|
||||
} else if (slugId) {
|
||||
// no public URL: the /p/ resolver redirects members straight to the
|
||||
// page and funnels anonymous visitors through login first; a new tab
|
||||
// keeps the docs tab's history intact through that redirect chain
|
||||
window.open(
|
||||
buildPageUrl(undefined, slugId, pageTitle, anchor || undefined),
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
navigate(anchor ? `${targetPath}#${anchor}` : targetPath);
|
||||
}
|
||||
@@ -276,8 +320,11 @@ export default function LinkView(props: MarkViewProps) {
|
||||
location.pathname,
|
||||
isInternal,
|
||||
isShareRoute,
|
||||
isPublicSpaceRoute,
|
||||
slugId,
|
||||
shareId,
|
||||
spaceSlug,
|
||||
publicSpacePageData,
|
||||
pageTitle,
|
||||
pageSlug,
|
||||
]);
|
||||
@@ -319,12 +366,18 @@ export default function LinkView(props: MarkViewProps) {
|
||||
setPopoverState("closed");
|
||||
}, [editor]);
|
||||
|
||||
const internalHref = () => {
|
||||
if (isShareRoute && slugId) {
|
||||
return buildSharedPageUrl({ shareId, pageSlugId: slugId, pageTitle });
|
||||
}
|
||||
if (isPublicSpaceRoute && slugId && publicSpacePageData?.page) {
|
||||
return buildPublicSpaceUrl({ spaceSlug, pageSlugId: slugId, pageTitle });
|
||||
}
|
||||
return href;
|
||||
};
|
||||
|
||||
const displayHref = sanitizeUrl(
|
||||
isInternal
|
||||
? isShareRoute && slugId
|
||||
? buildSharedPageUrl({ shareId, pageSlugId: slugId, pageTitle })
|
||||
: href
|
||||
: normalizeUrl(href),
|
||||
isInternal ? internalHref() : normalizeUrl(href),
|
||||
);
|
||||
|
||||
const linkTitleInput = (
|
||||
@@ -374,6 +427,16 @@ export default function LinkView(props: MarkViewProps) {
|
||||
</>
|
||||
);
|
||||
|
||||
// Targets outside the published space have no public URL, so the label is
|
||||
// rendered as inert text instead of a link that dead-ends at /login.
|
||||
if (isUnresolvedPublicLink) {
|
||||
return (
|
||||
<span ref={wrapperRef} className={classes.linkWrapper}>
|
||||
<MarkViewContent />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
opened={isPopoverVisible}
|
||||
|
||||
@@ -4,8 +4,10 @@ import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { useSharePageQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { usePublicSpacePageQuery } from "@/features/public-space/queries/public-space-query.ts";
|
||||
import {
|
||||
buildPageUrl,
|
||||
buildPublicSpaceUrl,
|
||||
buildSharedPageUrl,
|
||||
} from "@/features/page/page.utils.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
@@ -21,17 +23,30 @@ export default function MentionView(props: NodeViewProps) {
|
||||
|
||||
const location = useLocation();
|
||||
const isShareRoute = location.pathname.startsWith("/share");
|
||||
const isPublicSpaceRoute = location.pathname.startsWith("/docs/");
|
||||
|
||||
const {
|
||||
data: page,
|
||||
isLoading,
|
||||
isError,
|
||||
} = usePageQuery({ pageId: isPageMention && !isShareRoute ? slugId : null });
|
||||
} = usePageQuery({
|
||||
pageId:
|
||||
isPageMention && !isShareRoute && !isPublicSpaceRoute ? slugId : null,
|
||||
});
|
||||
|
||||
const { data: sharedPage } = useSharePageQuery({
|
||||
pageId: isPageMention && isShareRoute ? slugId : undefined,
|
||||
});
|
||||
|
||||
// Without the slugId guard the request resolves to the space home, which
|
||||
// would render a mention pointing at the wrong page.
|
||||
const { data: publicPageData } = usePublicSpacePageQuery({
|
||||
spaceSlug:
|
||||
isPageMention && isPublicSpaceRoute && slugId ? spaceSlug : undefined,
|
||||
pageSlugId: slugId,
|
||||
contentless: true,
|
||||
});
|
||||
|
||||
const currentPageSlugId = extractPageSlugId(pageSlug);
|
||||
const isSamePage = currentPageSlugId === slugId;
|
||||
|
||||
@@ -87,7 +102,61 @@ export default function MentionView(props: NodeViewProps) {
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{isPageMention && !isShareRoute && isError && (
|
||||
{isPageMention && isPublicSpaceRoute && publicPageData?.page && (
|
||||
<Anchor
|
||||
component={Link}
|
||||
fw={500}
|
||||
to={buildPublicSpaceUrl({
|
||||
spaceSlug: publicPageData.space?.slug ?? spaceSlug,
|
||||
pageSlugId: slugId,
|
||||
pageTitle: publicPageData.page.title || label,
|
||||
anchorId,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
underline="never"
|
||||
className={classes.pageMentionLink}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
component="span"
|
||||
size={18}
|
||||
style={{ verticalAlign: "text-bottom" }}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
<span className={classes.pageMentionText}>
|
||||
{publicPageData.page.title || label}
|
||||
</span>
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{/* No public URL: the /p/ resolver redirects members to the page and
|
||||
funnels anonymous visitors through login first. New tab, so the
|
||||
redirect chain never rewrites the docs tab's history. */}
|
||||
{isPageMention && isPublicSpaceRoute && !publicPageData?.page && (
|
||||
<Anchor
|
||||
fw={500}
|
||||
href={buildPageUrl(undefined, slugId, label, anchorId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="never"
|
||||
className={classes.pageMentionLink}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
component="span"
|
||||
size={18}
|
||||
style={{ verticalAlign: "text-bottom" }}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
<span className={classes.pageMentionText}>{label}</span>
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{isPageMention && !isShareRoute && !isPublicSpaceRoute && isError && (
|
||||
<Anchor
|
||||
component={Link}
|
||||
fw={500}
|
||||
@@ -111,7 +180,7 @@ export default function MentionView(props: NodeViewProps) {
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{isPageMention && !isShareRoute && !isError && (
|
||||
{isPageMention && !isShareRoute && !isPublicSpaceRoute && !isError && (
|
||||
<Anchor
|
||||
component={Link}
|
||||
fw={500}
|
||||
|
||||
@@ -106,6 +106,9 @@ export function PdfMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`pdf-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
|
||||
@@ -61,6 +61,9 @@ export const SubpagesMenu = React.memo(
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`subpages-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
|
||||
@@ -3,22 +3,30 @@ import { Stack, Text, Anchor, ActionIcon } from "@mantine/core";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { useGetSidebarPagesQuery } from "@/features/page/queries/page-query";
|
||||
import { useMemo } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import classes from "./subpages.module.css";
|
||||
import styles from "../mention/mention.module.css";
|
||||
import {
|
||||
buildPageUrl,
|
||||
buildPublicSpaceUrl,
|
||||
buildSharedPageUrl,
|
||||
} from "@/features/page/page.utils.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { sortPositionKeys } from "@/features/page/tree/utils/utils";
|
||||
import { useSharedPageSubpages } from "@/features/share/hooks/use-shared-page-subpages";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { publicSpaceTreeDataAtom } from "@/features/public-space/atoms/public-space-atoms.ts";
|
||||
import { findSubpagesInTree } from "@/features/share/utils";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
|
||||
export default function SubpagesView(props: NodeViewProps) {
|
||||
const { editor } = props;
|
||||
const { spaceSlug, shareId, pageSlug } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const isPublicSpaceRoute = location.pathname.startsWith("/docs/");
|
||||
|
||||
const publicSpaceTreeData = useAtomValue(publicSpaceTreeDataAtom);
|
||||
|
||||
// @ts-ignore
|
||||
const storagePageId = editor.storage.pageId;
|
||||
@@ -29,11 +37,25 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
currentPageId = routePageId;
|
||||
}
|
||||
|
||||
// Public docs must resolve the page from the route, not editor storage:
|
||||
// storage.pageId is set after this view's first render and is not reactive,
|
||||
// which froze the list at "No subpages" until something re-rendered it. The
|
||||
// space home renders at the bare URL, so it falls back to the first root.
|
||||
if (isPublicSpaceRoute) {
|
||||
currentPageId = routePageId ?? publicSpaceTreeData?.[0]?.slugId;
|
||||
}
|
||||
|
||||
// Get subpages from shared tree if we're in a shared context
|
||||
const sharedSubpages = useSharedPageSubpages(currentPageId);
|
||||
const publicSpaceSubpages = useMemo(
|
||||
() => findSubpagesInTree(publicSpaceTreeData, currentPageId),
|
||||
[publicSpaceTreeData, currentPageId],
|
||||
);
|
||||
|
||||
const isPublicView = Boolean(shareId) || isPublicSpaceRoute;
|
||||
|
||||
const { data, isLoading, error } = useGetSidebarPagesQuery(
|
||||
shareId ? null : { pageId: currentPageId },
|
||||
isPublicView ? null : { pageId: currentPageId },
|
||||
);
|
||||
|
||||
const subpages = useMemo(() => {
|
||||
@@ -48,17 +70,33 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
}));
|
||||
}
|
||||
|
||||
if (isPublicSpaceRoute) {
|
||||
return publicSpaceSubpages.map((node) => ({
|
||||
id: node.value,
|
||||
slugId: node.slugId,
|
||||
title: node.name,
|
||||
icon: node.icon,
|
||||
position: node.position,
|
||||
}));
|
||||
}
|
||||
|
||||
// Otherwise use the API data
|
||||
if (!data?.pages) return [];
|
||||
const allPages = data.pages.flatMap((page) => page.items);
|
||||
return sortPositionKeys(allPages);
|
||||
}, [data, shareId, sharedSubpages]);
|
||||
}, [
|
||||
data,
|
||||
shareId,
|
||||
sharedSubpages,
|
||||
isPublicSpaceRoute,
|
||||
publicSpaceSubpages,
|
||||
]);
|
||||
|
||||
if (isLoading && !shareId) {
|
||||
if (isLoading && !isPublicView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error && !shareId) {
|
||||
if (error && !isPublicView) {
|
||||
return (
|
||||
<NodeViewWrapper data-drag-handle>
|
||||
<Text c="dimmed" size="md" py="md">
|
||||
@@ -96,7 +134,13 @@ export default function SubpagesView(props: NodeViewProps) {
|
||||
pageSlugId: page.slugId,
|
||||
pageTitle: page.title,
|
||||
})
|
||||
: buildPageUrl(spaceSlug, page.slugId, page.title)
|
||||
: isPublicSpaceRoute
|
||||
? buildPublicSpaceUrl({
|
||||
spaceSlug,
|
||||
pageSlugId: page.slugId,
|
||||
pageTitle: page.title,
|
||||
})
|
||||
: buildPageUrl(spaceSlug, page.slugId, page.title)
|
||||
}
|
||||
underline="never"
|
||||
className={styles.pageMentionLink}
|
||||
|
||||
@@ -18,7 +18,7 @@ export type HeadingLink = {
|
||||
position: number;
|
||||
};
|
||||
|
||||
const recalculateLinks = (nodePos: NodePos[]) => {
|
||||
export const recalculateLinks = (nodePos: NodePos[]) => {
|
||||
const nodes: HTMLElement[] = [];
|
||||
|
||||
const links: HeadingLink[] = Array.from(nodePos).reduce<HeadingLink[]>(
|
||||
|
||||
+16
-1
@@ -9,6 +9,7 @@ import React, {
|
||||
} from "react";
|
||||
import {
|
||||
lookupTransclusion,
|
||||
lookupTransclusionForPublicSpace,
|
||||
lookupTransclusionForShare,
|
||||
} from "@/features/transclusion/services/transclusion-api";
|
||||
import type { TransclusionLookup } from "@/features/transclusion/types/transclusion.types";
|
||||
@@ -38,6 +39,7 @@ const TransclusionLookupContext = createContext<ContextValue | null>(null);
|
||||
export function TransclusionLookupProvider({
|
||||
children,
|
||||
shareId,
|
||||
spaceSlug,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
@@ -47,6 +49,11 @@ export function TransclusionLookupProvider({
|
||||
* app, where personal permissions gate access.
|
||||
*/
|
||||
shareId?: string;
|
||||
/**
|
||||
* When set, lookups go through the public-space endpoint and are gated by
|
||||
* the published space. Used by the public docs viewer.
|
||||
*/
|
||||
spaceSlug?: string;
|
||||
}) {
|
||||
const subscribersRef = useRef(new Map<LookupKey, Subscriber[]>());
|
||||
const queueRef = useRef(new Set<LookupKey>());
|
||||
@@ -55,6 +62,8 @@ export function TransclusionLookupProvider({
|
||||
// memoized callbacks (and thus doesn't re-render every consumer).
|
||||
const shareIdRef = useRef<string | undefined>(shareId);
|
||||
shareIdRef.current = shareId;
|
||||
const spaceSlugRef = useRef<string | undefined>(spaceSlug);
|
||||
spaceSlugRef.current = spaceSlug;
|
||||
// Last looked-up value for each key. Re-subscribers (e.g. when the editor
|
||||
// remounts after switching from static to live) get this immediately
|
||||
// instead of triggering a duplicate fetch.
|
||||
@@ -91,12 +100,18 @@ export function TransclusionLookupProvider({
|
||||
|
||||
try {
|
||||
const activeShareId = shareIdRef.current;
|
||||
const activeSpaceSlug = spaceSlugRef.current;
|
||||
const { items } = activeShareId
|
||||
? await lookupTransclusionForShare({
|
||||
shareId: activeShareId,
|
||||
references,
|
||||
})
|
||||
: await lookupTransclusion({ references });
|
||||
: activeSpaceSlug
|
||||
? await lookupTransclusionForPublicSpace({
|
||||
spaceSlug: activeSpaceSlug,
|
||||
references,
|
||||
})
|
||||
: await lookupTransclusion({ references });
|
||||
for (const r of items) {
|
||||
const key = `${r.sourcePageId}::${r.transclusionId}`;
|
||||
resultCacheRef.current.set(key, r);
|
||||
|
||||
@@ -132,6 +132,9 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`video-menu`}
|
||||
ref={(element) => {
|
||||
if (element) element.style.zIndex = "99";
|
||||
}}
|
||||
updateDelay={0}
|
||||
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||
options={{
|
||||
|
||||
@@ -35,6 +35,16 @@ interface PageEditorProps {
|
||||
* that isn't itself shared.
|
||||
*/
|
||||
shareId?: string;
|
||||
/**
|
||||
* When rendering inside a public space, pass the space slug. Transclusion
|
||||
* lookups then resolve against the published space instead of the viewer's
|
||||
* personal permissions.
|
||||
*/
|
||||
spaceSlug?: string;
|
||||
/** Rendered between the title and the content. */
|
||||
byline?: React.ReactNode;
|
||||
/** Set false when the consumer renders its own end matter (e.g. prev/next). */
|
||||
trailingSpace?: boolean;
|
||||
}
|
||||
|
||||
export default function ReadonlyPageEditor({
|
||||
@@ -43,12 +53,16 @@ export default function ReadonlyPageEditor({
|
||||
pageId,
|
||||
printMode = false,
|
||||
shareId,
|
||||
spaceSlug,
|
||||
byline,
|
||||
trailingSpace = true,
|
||||
}: PageEditorProps) {
|
||||
const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom);
|
||||
const [lightboxRequest, setLightboxRequest] = useAtom(lightboxRequestAtom);
|
||||
const [contentEditor, setContentEditor] = useState<Editor | null>(null);
|
||||
const isComponentMounted = useRef(false);
|
||||
const editorCreated = useRef(false);
|
||||
const isPublicView = Boolean(shareId || spaceSlug);
|
||||
|
||||
const canScroll = useCallback(
|
||||
() => isComponentMounted.current && editorCreated.current,
|
||||
@@ -64,9 +78,9 @@ export default function ReadonlyPageEditor({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shareId) return;
|
||||
if (!isPublicView) return;
|
||||
setLightboxRequest(null);
|
||||
}, [pageId, shareId]);
|
||||
}, [pageId, isPublicView]);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const excludedExtensions = new Set([
|
||||
@@ -99,7 +113,7 @@ export default function ReadonlyPageEditor({
|
||||
];
|
||||
|
||||
return (
|
||||
<TransclusionLookupProvider shareId={shareId}>
|
||||
<TransclusionLookupProvider shareId={shareId} spaceSlug={spaceSlug}>
|
||||
<div className="page-title">
|
||||
<EditorProvider
|
||||
editable={false}
|
||||
@@ -110,6 +124,8 @@ export default function ReadonlyPageEditor({
|
||||
></EditorProvider>
|
||||
</div>
|
||||
|
||||
{byline}
|
||||
|
||||
<EditorProvider
|
||||
editable={false}
|
||||
immediatelyRender={true}
|
||||
@@ -117,7 +133,7 @@ export default function ReadonlyPageEditor({
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
editorProps={
|
||||
shareId
|
||||
isPublicView
|
||||
? {
|
||||
handleClickOn: (_view, _pos, node) => {
|
||||
const request = getLightboxClickRequest(node);
|
||||
@@ -144,7 +160,7 @@ export default function ReadonlyPageEditor({
|
||||
}
|
||||
}}
|
||||
></EditorProvider>
|
||||
{shareId && contentEditor && (
|
||||
{isPublicView && contentEditor && (
|
||||
<LightboxView
|
||||
editor={contentEditor}
|
||||
open={!!lightboxRequest}
|
||||
@@ -153,7 +169,7 @@ export default function ReadonlyPageEditor({
|
||||
onClose={() => setLightboxRequest(null)}
|
||||
/>
|
||||
)}
|
||||
<div style={{ paddingBottom: "20vh" }}></div>
|
||||
{trailingSpace && <div style={{ paddingBottom: "20vh" }}></div>}
|
||||
</TransclusionLookupProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user