mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 04:11:14 +10:00
Share - WIP
This commit is contained in:
@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
|
||||
import Security from "@/ee/security/pages/security.tsx";
|
||||
import License from "@/ee/licence/pages/license.tsx";
|
||||
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
||||
import SharedPage from '@/pages/share/shared-page.tsx';
|
||||
|
||||
export default function App() {
|
||||
const { t } = useTranslation();
|
||||
@ -51,6 +52,8 @@ export default function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Route path={"/share/:shareId/:pageId"} element={<SharedPage />} />
|
||||
|
||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||
|
||||
<Route element={<Layout />}>
|
||||
|
||||
@ -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"
|
||||
|
||||
52
apps/client/src/features/editor/readonly-page-editor.tsx
Normal file
52
apps/client/src/features/editor/readonly-page-editor.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
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";
|
||||
|
||||
interface PageEditorProps {
|
||||
title: string;
|
||||
content: any;
|
||||
}
|
||||
|
||||
export default function ReadonlyPageEditor({
|
||||
title,
|
||||
content,
|
||||
}: PageEditorProps) {
|
||||
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}
|
||||
></EditorProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
|
||||
export const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
|
||||
const titleSlug = slugify(pageTitle?.substring(0, 70) || "untitled", {
|
||||
customReplacements: [
|
||||
["♥", ""],
|
||||
@ -8,7 +8,7 @@ const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
|
||||
],
|
||||
});
|
||||
|
||||
return `p/${titleSlug}-${pageSlugId}`;
|
||||
return `${titleSlug}-${pageSlugId}`;
|
||||
};
|
||||
|
||||
export const buildPageUrl = (
|
||||
@ -17,7 +17,7 @@ export const buildPageUrl = (
|
||||
pageTitle?: string,
|
||||
): string => {
|
||||
if (spaceName === undefined) {
|
||||
return `/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
return `/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
}
|
||||
return `/s/${spaceName}/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
return `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||
};
|
||||
|
||||
65
apps/client/src/features/share/queries/share-query.ts
Normal file
65
apps/client/src/features/share/queries/share-query.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { validate as isValidUuid } from "uuid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ICreateShare,
|
||||
IShareInput,
|
||||
} from "@/features/share/types/share.types.ts";
|
||||
import {
|
||||
createShare,
|
||||
deleteShare,
|
||||
getShare,
|
||||
updateShare,
|
||||
} from "@/features/share/services/share-service.ts";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
|
||||
export function useShareQuery(
|
||||
shareInput: Partial<IShareInput>,
|
||||
): UseQueryResult<IPage, Error> {
|
||||
const query = useQuery({
|
||||
queryKey: ["shares", shareInput],
|
||||
queryFn: () => getShare(shareInput),
|
||||
enabled: !!shareInput.shareId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export function useCreateShareMutation() {
|
||||
const { t } = useTranslation();
|
||||
return useMutation<any, Error, ICreateShare>({
|
||||
mutationFn: (data) => createShare(data),
|
||||
onSuccess: (data) => {},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: t("Failed to share page"), color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateShareMutation() {
|
||||
return useMutation<any, Error, Partial<IShareInput>>({
|
||||
mutationFn: (data) => updateShare(data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteShareMutation() {
|
||||
const { t } = useTranslation();
|
||||
return useMutation({
|
||||
mutationFn: (shareId: string) => deleteShare(shareId),
|
||||
onSuccess: () => {
|
||||
notifications.show({ message: t("Share deleted successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
message: t("Failed to delete share"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
36
apps/client/src/features/share/services/share-service.ts
Normal file
36
apps/client/src/features/share/services/share-service.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import api from "@/lib/api-client";
|
||||
import {
|
||||
IExportPageParams,
|
||||
IMovePage,
|
||||
IMovePageToSpace,
|
||||
IPage,
|
||||
IPageInput,
|
||||
SidebarPagesParams,
|
||||
} from "@/features/page/types/page.types";
|
||||
import { IAttachment, IPagination } from "@/lib/types.ts";
|
||||
import { saveAs } from "file-saver";
|
||||
import {
|
||||
ICreateShare,
|
||||
IShareInput,
|
||||
} from "@/features/share/types/share.types.ts";
|
||||
|
||||
export async function createShare(data: ICreateShare): Promise<any> {
|
||||
const req = await api.post<any>("/shares/create", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getShare(
|
||||
shareInput: Partial<IShareInput>,
|
||||
): Promise<IPage> {
|
||||
const req = await api.post<IPage>("/shares/info", shareInput);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function updateShare(data: Partial<IShareInput>): Promise<any> {
|
||||
const req = await api.post<IPage>("/shares/update", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function deleteShare(shareId: string): Promise<void> {
|
||||
await api.post("/shares/delete", { shareId });
|
||||
}
|
||||
12
apps/client/src/features/share/types/share.types.ts
Normal file
12
apps/client/src/features/share/types/share.types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
export interface ICreateShare {
|
||||
slugId: string;
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IShareInput {
|
||||
shareId: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
56
apps/client/src/pages/share/shared-page.tsx
Normal file
56
apps/client/src/pages/share/shared-page.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useShareQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { Container } from "@mantine/core";
|
||||
import React, { useEffect } from "react";
|
||||
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
|
||||
import { buildPageSlug } from "@/features/page/page.utils.ts";
|
||||
|
||||
export default function SharedPage() {
|
||||
const { t } = useTranslation();
|
||||
const { shareId } = useParams();
|
||||
const {
|
||||
data: page,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useShareQuery({ shareId: shareId });
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!page) return;
|
||||
const pageSlug = buildPageSlug(page.slugId, page.title);
|
||||
const shareSlug = `/share/${shareId}/${pageSlug}`;
|
||||
navigate(shareSlug, { replace: true });
|
||||
}, [page]);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (isError || !page) {
|
||||
if ([401, 403, 404].includes(error?.["status"])) {
|
||||
return <div>{t("Page not found")}</div>;
|
||||
}
|
||||
return <div>{t("Error fetching page data.")}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
page && (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
|
||||
</Helmet>
|
||||
|
||||
<Container size={900} pt={50}>
|
||||
<ReadonlyPageEditor
|
||||
key={page.id}
|
||||
title={page.title}
|
||||
content={page.content}
|
||||
/>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user