mirror of
https://github.com/docmost/docmost.git
synced 2025-11-19 03:21:08 +10: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:
@ -5,4 +5,6 @@ export const pageEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const titleEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const readOnlyEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const yjsConnectionStatusAtom = atom<string>("");
|
||||
|
||||
@ -139,7 +139,7 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
{selected && editor.isEditable && (
|
||||
<ActionIcon
|
||||
onClick={handleOpen}
|
||||
variant="default"
|
||||
|
||||
@ -170,7 +170,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
{selected && editor.isEditable && (
|
||||
<ActionIcon
|
||||
onClick={handleOpen}
|
||||
variant="default"
|
||||
|
||||
@ -1,21 +1,34 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ActionIcon, Anchor, Text } from "@mantine/core";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import {
|
||||
buildPageUrl,
|
||||
buildSharedPageUrl,
|
||||
} from "@/features/page/page.utils.ts";
|
||||
import classes from "./mention.module.css";
|
||||
|
||||
export default function MentionView(props: NodeViewProps) {
|
||||
const { node } = props;
|
||||
const { label, entityType, entityId, slugId } = node.attrs;
|
||||
const { spaceSlug } = useParams();
|
||||
const { shareId } = useParams();
|
||||
const {
|
||||
data: page,
|
||||
isLoading,
|
||||
isError,
|
||||
} = usePageQuery({ pageId: entityType === "page" ? slugId : null });
|
||||
|
||||
const location = useLocation();
|
||||
const isShareRoute = location.pathname.startsWith("/share");
|
||||
|
||||
const shareSlugUrl = buildSharedPageUrl({
|
||||
shareId,
|
||||
pageSlugId: slugId,
|
||||
pageTitle: label,
|
||||
});
|
||||
|
||||
return (
|
||||
<NodeViewWrapper style={{ display: "inline" }}>
|
||||
{entityType === "user" && (
|
||||
@ -28,7 +41,9 @@ export default function MentionView(props: NodeViewProps) {
|
||||
<Anchor
|
||||
component={Link}
|
||||
fw={500}
|
||||
to={buildPageUrl(spaceSlug, slugId, label)}
|
||||
to={
|
||||
isShareRoute ? shareSlugUrl : buildPageUrl(spaceSlug, slugId, label)
|
||||
}
|
||||
underline="never"
|
||||
className={classes.pageMentionLink}
|
||||
>
|
||||
|
||||
@ -52,3 +52,8 @@
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.leftBorder {
|
||||
border-left: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
type TableOfContentsProps = {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
isShare?: boolean;
|
||||
};
|
||||
|
||||
export type HeadingLink = {
|
||||
@ -73,6 +74,7 @@ export const TableOfContents: FC<TableOfContentsProps> = (props) => {
|
||||
|
||||
const handleUpdate = () => {
|
||||
const result = recalculateLinks(props.editor?.$nodes("heading"));
|
||||
|
||||
setLinks(result.links);
|
||||
setHeadingDOMNodes(result.nodes);
|
||||
};
|
||||
@ -85,9 +87,12 @@ export const TableOfContents: FC<TableOfContentsProps> = (props) => {
|
||||
};
|
||||
}, [props.editor]);
|
||||
|
||||
useEffect(() => {
|
||||
handleUpdate();
|
||||
}, []);
|
||||
useEffect(
|
||||
() => {
|
||||
handleUpdate();
|
||||
},
|
||||
props.isShare ? [props.editor] : [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@ -133,16 +138,23 @@ export const TableOfContents: FC<TableOfContentsProps> = (props) => {
|
||||
if (!links.length) {
|
||||
return (
|
||||
<>
|
||||
<Text size="sm">
|
||||
{t("Add headings (H1, H2, H3) to generate a table of contents.")}
|
||||
</Text>
|
||||
{!props.isShare && (
|
||||
<Text size="sm">
|
||||
{t("Add headings (H1, H2, H3) to generate a table of contents.")}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{props.isShare && (
|
||||
<Text mb="md" fw={500}>
|
||||
{t("Table of contents")}
|
||||
</Text>
|
||||
)}
|
||||
<div className={props.isShare ? classes.leftBorder : ""}>
|
||||
{links.map((item, idx) => (
|
||||
<Box<"button">
|
||||
component="button"
|
||||
|
||||
67
apps/client/src/features/editor/readonly-page-editor.tsx
Normal file
67
apps/client/src/features/editor/readonly-page-editor.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import "@/features/editor/styles/index.css";
|
||||
import React, { useMemo } from "react";
|
||||
import { EditorProvider } from "@tiptap/react";
|
||||
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||
import { Document } from "@tiptap/extension-document";
|
||||
import { Heading } from "@tiptap/extension-heading";
|
||||
import { Text } from "@tiptap/extension-text";
|
||||
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||
import { useAtom } from "jotai/index";
|
||||
import {
|
||||
pageEditorAtom,
|
||||
readOnlyEditorAtom,
|
||||
} from "@/features/editor/atoms/editor-atoms.ts";
|
||||
import { Editor } from "@tiptap/core";
|
||||
|
||||
interface PageEditorProps {
|
||||
title: string;
|
||||
content: any;
|
||||
}
|
||||
|
||||
export default function ReadonlyPageEditor({
|
||||
title,
|
||||
content,
|
||||
}: PageEditorProps) {
|
||||
const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
return [...mainExtensions];
|
||||
}, []);
|
||||
|
||||
const titleExtensions = [
|
||||
Document.extend({
|
||||
content: "heading",
|
||||
}),
|
||||
Heading,
|
||||
Text,
|
||||
Placeholder.configure({
|
||||
placeholder: "Untitled",
|
||||
showOnlyWhenEditable: false,
|
||||
}),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorProvider
|
||||
editable={false}
|
||||
immediatelyRender={true}
|
||||
extensions={titleExtensions}
|
||||
content={title}
|
||||
></EditorProvider>
|
||||
|
||||
<EditorProvider
|
||||
editable={false}
|
||||
immediatelyRender={true}
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
onCreate={({ editor }) => {
|
||||
if (editor) {
|
||||
// @ts-ignore
|
||||
setReadOnlyEditor(editor);
|
||||
}
|
||||
}}
|
||||
></EditorProvider>
|
||||
<div style={{ paddingBottom: "20vh" }}></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user