From 5bdefda9c79a24aad2433bd590795bddb515a3ee Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 16 Apr 2025 20:19:16 +0100 Subject: [PATCH] WIP --- apps/client/src/components/theme-toggle.tsx | 10 +- .../features/page/tree/styles/tree.module.css | 4 + .../src/features/share/atoms/sidebar-atom.ts | 13 ++ .../features/share/components/share-modal.tsx | 175 ++++++++++++------ .../features/share/components/share-shell.tsx | 67 ++++++- .../features/share/components/shared-tree.tsx | 13 +- .../src/features/share/queries/share-query.ts | 29 ++- .../features/share/services/share-service.ts | 26 +-- .../src/features/share/types/share.types.ts | 32 +++- apps/client/src/pages/share/shared-page.tsx | 34 ++-- .../src/core/share/dto/create-share.dto.ts | 10 - apps/server/src/core/share/dto/share.dto.ts | 24 +++ .../src/core/share/dto/update-page.dto.ts | 7 - .../server/src/core/share/share.controller.ts | 48 +++-- apps/server/src/core/share/share.service.ts | 70 +++++-- .../src/database/repos/share/share.repo.ts | 1 + 16 files changed, 412 insertions(+), 151 deletions(-) create mode 100644 apps/client/src/features/share/atoms/sidebar-atom.ts delete mode 100644 apps/server/src/core/share/dto/create-share.dto.ts delete mode 100644 apps/server/src/core/share/dto/update-page.dto.ts diff --git a/apps/client/src/components/theme-toggle.tsx b/apps/client/src/components/theme-toggle.tsx index 220155b5..bf9245a2 100644 --- a/apps/client/src/components/theme-toggle.tsx +++ b/apps/client/src/components/theme-toggle.tsx @@ -9,17 +9,15 @@ import classes from "./theme-toggle.module.css"; export function ThemeToggle() { const { setColorScheme } = useMantineColorScheme(); - const computedColorScheme = useComputedColorScheme("light", { - getInitialValueInEffect: true, - }); + const computedColorScheme = useComputedColorScheme(); return ( - setColorScheme(computedColorScheme === "light" ? "dark" : "light") - } + onClick={() => { + setColorScheme(computedColorScheme === "light" ? "dark" : "light"); + }} aria-label="Toggle color scheme" > diff --git a/apps/client/src/features/page/tree/styles/tree.module.css b/apps/client/src/features/page/tree/styles/tree.module.css index 0a258fb5..a769dea6 100644 --- a/apps/client/src/features/page/tree/styles/tree.module.css +++ b/apps/client/src/features/page/tree/styles/tree.module.css @@ -70,6 +70,10 @@ background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); } +.row:focus .node:global(.isFocused) { + background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); +} + .row { white-space: nowrap; cursor: pointer; diff --git a/apps/client/src/features/share/atoms/sidebar-atom.ts b/apps/client/src/features/share/atoms/sidebar-atom.ts new file mode 100644 index 00000000..cfb7e39d --- /dev/null +++ b/apps/client/src/features/share/atoms/sidebar-atom.ts @@ -0,0 +1,13 @@ +import { atomWithWebStorage } from "@/lib/jotai-helper.ts"; +import { atom } from 'jotai/index'; + +export const tableOfContentAsideAtom = atomWithWebStorage( + "showTOC", + true, +); + +export const mobileTableOfContentAsideAtom = atom(false); + + + +const sidebarWidthAtom = atomWithWebStorage('sidebarWidth', 300); \ No newline at end of file diff --git a/apps/client/src/features/share/components/share-modal.tsx b/apps/client/src/features/share/components/share-modal.tsx index 9b91804f..ece6864d 100644 --- a/apps/client/src/features/share/components/share-modal.tsx +++ b/apps/client/src/features/share/components/share-modal.tsx @@ -1,31 +1,83 @@ import { Button, Group, - MantineSize, Popover, Switch, Text, TextInput, } from "@mantine/core"; import { IconWorld } from "@tabler/icons-react"; -import React, { useState } from "react"; -import { useShareStatusQuery } from "@/features/share/queries/share-query.ts"; +import React, { useEffect, useState } from "react"; +import { + useCreateShareMutation, + useShareForPageQuery, + useUpdateShareMutation, +} from "@/features/share/queries/share-query.ts"; import { useParams } from "react-router-dom"; import { extractPageSlugId } from "@/lib"; import { useTranslation } from "react-i18next"; import CopyTextButton from "@/components/common/copy.tsx"; +import { getAppUrl } from "@/lib/config.ts"; export default function ShareModal() { const { t } = useTranslation(); const { pageSlug } = useParams(); - const { data } = useShareStatusQuery(extractPageSlugId(pageSlug)); + const pageId = extractPageSlugId(pageSlug); + const { data: share } = useShareForPageQuery(pageId); + const createShareMutation = useCreateShareMutation(); + const updateShareMutation = useUpdateShareMutation(); + // pageIsShared means that the share exists and its level equals zero. + const pageIsShared = share && share.level === 0; + // if level is greater than zero, then it is a descendant page from a shared page + const isDescendantShared = share && share.level > 0; - const publicLink = - window.location.protocol +'//' + window.location.host + - "/share/" + - data?.["share"]?.["key"] + - "/" + - pageSlug; + const publicLink = `${getAppUrl()}/share/${share?.key}/${pageSlug}`; + + + // TODO: think of permissions + // controls should be read only for non space editors. + + + // we could use the same shared content but have it have a share status + // when you unshare, we hide the rest menu + + // todo, is public only if this is the shared page + // if this is not the shared page and include chdilren == false, then set it to false + const [isPagePublic, setIsPagePublic] = useState(false); + useEffect(() => { + if (share) { + setIsPagePublic(true); + } else { + setIsPagePublic(false); + } + }, [share, pageId]); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + createShareMutation.mutateAsync({ pageId: pageId }); + setIsPagePublic(value); + // on create refetch share + }; + + const handleSubPagesChange = async ( + event: React.ChangeEvent, + ) => { + const value = event.currentTarget.checked; + updateShareMutation.mutateAsync({ + shareId: share.id, + includeSubPages: value, + }); + }; + + const handleIndexSearchChange = async ( + event: React.ChangeEvent, + ) => { + const value = event.currentTarget.checked; + updateShareMutation.mutateAsync({ + shareId: share.id, + searchIndexing: value, + }); + }; return ( @@ -39,50 +91,69 @@ export default function ShareModal() { - -
- {t("Make page public")} -
- -
+ {isDescendantShared ? ( + + {t("This page was shared via")} {share.sharedPage.title} + + ) : ( + <> + +
+ Share page + + Make it public to the internet + +
+ +
- - } - /> - + {pageIsShared && ( + <> + + } + /> + + + +
+ {t("Include sub pages")} + + Include children of this page + +
+ +
+ + +
+ {t("Enable search indexing")} + + Allow search engine indexing + +
+ +
+ + )} + + )}
); } - -interface PageWidthToggleProps { - isChecked: boolean; - size?: MantineSize; - label?: string; -} - -export function ToggleShare({ isChecked, size, label }: PageWidthToggleProps) { - const { t } = useTranslation(); - const [checked, setChecked] = useState(isChecked); - - const handleChange = async (event: React.ChangeEvent) => { - const value = event.currentTarget.checked; - setChecked(value); - }; - - return ( - - ); -} diff --git a/apps/client/src/features/share/components/share-shell.tsx b/apps/client/src/features/share/components/share-shell.tsx index 11e4934a..0501b8ed 100644 --- a/apps/client/src/features/share/components/share-shell.tsx +++ b/apps/client/src/features/share/components/share-shell.tsx @@ -2,13 +2,12 @@ import React from "react"; import { Affix, AppShell, - Burger, Button, Group, ScrollArea, Text, + Tooltip, } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts"; import { useParams } from "react-router-dom"; import SharedTree from "@/features/share/components/shared-tree.tsx"; @@ -16,6 +15,18 @@ import { TableOfContents } from "@/features/editor/components/table-of-contents/ import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import { ThemeToggle } from "@/components/theme-toggle.tsx"; import { useAtomValue } from "jotai"; +import { useAtom } from "jotai"; +import { + desktopSidebarAtom, + mobileSidebarAtom, +} 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"; const MemoizedSharedTree = React.memo(SharedTree); @@ -24,7 +35,15 @@ export default function ShareShell({ }: { children: React.ReactNode; }) { - const [opened, { toggle }] = useDisclosure(); + 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 { shareId } = useParams(); const { data } = useGetSharedPageTreeQuery(shareId); const readOnlyEditor = useAtomValue(readOnlyEditorAtom); @@ -35,19 +54,51 @@ export default function ShareShell({ navbar={{ width: 300, breakpoint: "sm", - collapsed: { mobile: !opened, desktop: false }, + collapsed: { + mobile: !mobileOpened, + desktop: !desktopOpened, + }, }} aside={{ width: 300, - breakpoint: "sm", - collapsed: { mobile: true, desktop: false }, + breakpoint: "md", + collapsed: { + mobile: mobileTocOpened, + desktop: tocOpened, + }, }} padding="md" > - - + + {data?.pageTree?.length > 0 && ( + <> + + + + + + + + + )} + + + + diff --git a/apps/client/src/features/share/components/shared-tree.tsx b/apps/client/src/features/share/components/shared-tree.tsx index 37a359fb..b7550e02 100644 --- a/apps/client/src/features/share/components/shared-tree.tsx +++ b/apps/client/src/features/share/components/shared-tree.tsx @@ -22,6 +22,8 @@ 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 { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; +import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; interface SharedTree { sharedPageTree: ISharedPageTree; @@ -40,6 +42,7 @@ export default function SharedTree({ sharedPageTree }: SharedTree) { const [openTreeNodes, setOpenTreeNodes] = useAtom( openSharedTreeNodesAtom, ); + const currentNodeId = extractPageSlugId(pageSlug); const treeData: SharedPageTreeNode[] = useMemo(() => { @@ -99,6 +102,11 @@ export default function SharedTree({ sharedPageTree }: SharedTree) { setOpenTreeNodes(tree?.openState); }} initialOpenState={openTreeNodes} + onClick={(e) => { + if (tree && tree.focusedNode) { + tree.select(tree.focusedNode); + } + }} > {Node} @@ -108,9 +116,9 @@ export default function SharedTree({ sharedPageTree }: SharedTree) { } function Node({ node, style, tree }: NodeRendererProps) { - const navigate = useNavigate(); const { shareId } = useParams(); const { t } = useTranslation(); + const [, setMobileSidebarState] = useAtom(mobileSidebarAtom); const pageUrl = buildSharedPageUrl({ shareId: shareId, @@ -125,6 +133,9 @@ function Node({ node, style, tree }: NodeRendererProps) { className={clsx(classes.node, node.state, styles.treeNode)} component={Link} to={pageUrl} + onClick={() => { + setMobileSidebarState(false); + }} > {node.data.name || t("untitled")} diff --git a/apps/client/src/features/share/queries/share-query.ts b/apps/client/src/features/share/queries/share-query.ts index a8c5d152..db43d8e3 100644 --- a/apps/client/src/features/share/queries/share-query.ts +++ b/apps/client/src/features/share/queries/share-query.ts @@ -2,23 +2,26 @@ import { keepPreviousData, useMutation, useQuery, + useQueryClient, UseQueryResult, } from "@tanstack/react-query"; import { notifications } from "@mantine/notifications"; import { useTranslation } from "react-i18next"; import { ICreateShare, - ISharedItem, + ISharedItem, ISharedPage, ISharedPageTree, + IShareForPage, IShareInfoInput, -} from "@/features/share/types/share.types.ts"; + IUpdateShare, +} from '@/features/share/types/share.types.ts'; import { createShare, deleteShare, getSharedPageTree, + getShareForPage, getShareInfo, getShares, - getShareStatus, updateShare, } from "@/features/share/services/share-service.ts"; import { IPage } from "@/features/page/types/page.types.ts"; @@ -36,7 +39,7 @@ export function useGetSharesQuery( export function useShareQuery( shareInput: Partial, -): UseQueryResult { +): UseQueryResult { const query = useQuery({ queryKey: ["shares", shareInput], queryFn: () => getShareInfo(shareInput), @@ -46,12 +49,12 @@ export function useShareQuery( return query; } -export function useShareStatusQuery( +export function useShareForPageQuery( pageId: string, -): UseQueryResult { +): UseQueryResult { const query = useQuery({ - queryKey: ["share-status", pageId], - queryFn: () => getShareStatus(pageId), + queryKey: ["share-for-page", pageId], + queryFn: () => getShareForPage(pageId), enabled: !!pageId, staleTime: 5 * 60 * 1000, }); @@ -63,7 +66,6 @@ export function useCreateShareMutation() { const { t } = useTranslation(); return useMutation({ mutationFn: (data) => createShare(data), - onSuccess: (data) => {}, onError: (error) => { notifications.show({ message: t("Failed to share page"), color: "red" }); }, @@ -71,8 +73,15 @@ export function useCreateShareMutation() { } export function useUpdateShareMutation() { - return useMutation>({ + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (data) => updateShare(data), + onSuccess: (data) => { + queryClient.refetchQueries({ + predicate: (item) => + ["share-for-page"].includes(item.queryKey[0] as string), + }); + }, }); } diff --git a/apps/client/src/features/share/services/share-service.ts b/apps/client/src/features/share/services/share-service.ts index 747ac64d..e810f198 100644 --- a/apps/client/src/features/share/services/share-service.ts +++ b/apps/client/src/features/share/services/share-service.ts @@ -3,10 +3,12 @@ import { IPage } from "@/features/page/types/page.types"; import { ICreateShare, - ISharedItem, + ISharedItem, ISharedPage, ISharedPageTree, + IShareForPage, IShareInfoInput, -} from "@/features/share/types/share.types.ts"; + IUpdateShare, +} from '@/features/share/types/share.types.ts'; import { IPagination, QueryParams } from "@/lib/types.ts"; export async function getShares( @@ -21,22 +23,20 @@ export async function createShare(data: ICreateShare): Promise { return req.data; } -export async function getShareStatus(pageId: string): Promise { - const req = await api.post("/shares/status", { pageId }); +export async function updateShare(data: IUpdateShare): Promise { + const req = await api.post("/shares/update", data); + return req.data; +} + +export async function getShareForPage(pageId: string): Promise { + const req = await api.post("/shares/for-page", { pageId }); return req.data; } export async function getShareInfo( shareInput: Partial, -): Promise { - const req = await api.post("/shares/info", shareInput); - return req.data; -} - -export async function updateShare( - data: Partial, -): Promise { - const req = await api.post("/shares/update", data); +): Promise { + const req = await api.post("/shares/page-info", shareInput); return req.data; } diff --git a/apps/client/src/features/share/types/share.types.ts b/apps/client/src/features/share/types/share.types.ts index 633fcb6a..2228bf9d 100644 --- a/apps/client/src/features/share/types/share.types.ts +++ b/apps/client/src/features/share/types/share.types.ts @@ -5,6 +5,7 @@ export interface IShare { key: string; pageId: string; includeSubPages: boolean; + searchIndexing: boolean; creatorId: string; spaceId: string; workspaceId: string; @@ -32,11 +33,36 @@ export interface ISharedItem extends IShare { }; } -export interface ICreateShare { - pageId: string; - includeSubPages?: boolean; +export interface ISharedPage extends IShare { + page: IPage; + share: IShare & { + level: number; + sharedPage: { id: string; slugId: string; title: string }; + }; } +export interface IShareForPage extends IShare { + level: number; + page: { + id: string; + title: string; + slugId: string; + }; + sharedPage: { + id: string; + slugId: string; + title: string; + }; +} + +export interface ICreateShare { + pageId?: string; + includeSubPages?: boolean; + searchIndexing?: boolean; +} + +export type IUpdateShare = ICreateShare & { shareId: string; pageId?: string }; + export interface IShareInfoInput { pageId: string; } diff --git a/apps/client/src/pages/share/shared-page.tsx b/apps/client/src/pages/share/shared-page.tsx index 5e897e59..0741b221 100644 --- a/apps/client/src/pages/share/shared-page.tsx +++ b/apps/client/src/pages/share/shared-page.tsx @@ -1,9 +1,9 @@ -import { useParams } from "react-router-dom"; +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 from "react"; +import React, { useEffect } from "react"; import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx"; import { extractPageSlugId } from "@/lib"; import { Error404 } from "@/components/ui/error-404.tsx"; @@ -11,19 +11,27 @@ import { Error404 } from "@/components/ui/error-404.tsx"; export default function SingleSharedPage() { const { t } = useTranslation(); const { pageSlug } = useParams(); + const { shareId } = useParams(); + const navigate = useNavigate(); - const { - data: page, - isLoading, - isError, - error, - } = useShareQuery({ pageId: extractPageSlugId(pageSlug) }); + const { data, isLoading, isError, error } = useShareQuery({ + pageId: extractPageSlugId(pageSlug), + }); + + useEffect(() => { + if (shareId && data) { + if (data.share.key !== shareId) { + // affects parent share, what to do? + //navigate(`/share/${data.share.key}/${pageSlug}`); + } + } + }, [shareId, data]); if (isLoading) { return <>; } - if (isError || !page) { + if (isError || !data) { if ([401, 403, 404].includes(error?.["status"])) { return ; } @@ -33,14 +41,14 @@ export default function SingleSharedPage() { return (
- {`${page?.icon || ""} ${page?.title || t("untitled")}`} + {`${data?.page?.icon || ""} ${data?.page?.title || t("untitled")}`}
diff --git a/apps/server/src/core/share/dto/create-share.dto.ts b/apps/server/src/core/share/dto/create-share.dto.ts deleted file mode 100644 index aad5bb5e..00000000 --- a/apps/server/src/core/share/dto/create-share.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IsBoolean, IsOptional, IsString } from 'class-validator'; - -export class CreateShareDto { - @IsString() - pageId: string; - - @IsBoolean() - @IsOptional() - includeSubPages: boolean; -} diff --git a/apps/server/src/core/share/dto/share.dto.ts b/apps/server/src/core/share/dto/share.dto.ts index 46c609cd..5c7becc0 100644 --- a/apps/server/src/core/share/dto/share.dto.ts +++ b/apps/server/src/core/share/dto/share.dto.ts @@ -6,6 +6,30 @@ import { IsUUID, } from 'class-validator'; +export class CreateShareDto { + @IsString() + @IsNotEmpty() + pageId: string; + + @IsBoolean() + @IsOptional() + includeSubPages: boolean; + + @IsOptional() + @IsBoolean() + searchIndexing: boolean; +} + +export class UpdateShareDto extends CreateShareDto { + @IsString() + @IsNotEmpty() + shareId: string; + + @IsString() + @IsOptional() + pageId: string; +} + export class ShareIdDto { @IsString() @IsNotEmpty() diff --git a/apps/server/src/core/share/dto/update-page.dto.ts b/apps/server/src/core/share/dto/update-page.dto.ts deleted file mode 100644 index 30a0c38e..00000000 --- a/apps/server/src/core/share/dto/update-page.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class UpdateShareDto { - @IsString() - @IsNotEmpty() - shareId: string; -} diff --git a/apps/server/src/core/share/share.controller.ts b/apps/server/src/core/share/share.controller.ts index cba63bdf..11fef67e 100644 --- a/apps/server/src/core/share/share.controller.ts +++ b/apps/server/src/core/share/share.controller.ts @@ -18,9 +18,13 @@ import { import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator'; import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; import { ShareService } from './share.service'; -import { UpdateShareDto } from './dto/update-page.dto'; -import { CreateShareDto } from './dto/create-share.dto'; -import { ShareIdDto, ShareInfoDto, SharePageIdDto } from './dto/share.dto'; +import { + CreateShareDto, + ShareIdDto, + ShareInfoDto, + SharePageIdDto, + UpdateShareDto, +} from './dto/share.dto'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { Public } from '../../common/decorators/public.decorator'; @@ -48,8 +52,8 @@ export class ShareController { @Public() @HttpCode(HttpStatus.OK) - @Post('/info') - async getShare( + @Post('/page-info') + async getSharedPageInfo( @Body() dto: ShareInfoDto, @AuthWorkspace() workspace: Workspace, ) { @@ -61,24 +65,40 @@ export class ShareController { } @HttpCode(HttpStatus.OK) - @Post('/status') - async getShareStatus( + @Post('/info') + async getShare(@Body() dto: ShareIdDto, @AuthUser() user: User) { + const share = await this.shareRepo.findById(dto.shareId); + + if (!share) { + throw new NotFoundException('Share not found'); + } + + const ability = await this.spaceAbility.createForUser(user, share.spaceId); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Share)) { + throw new ForbiddenException(); + } + + return share; + } + + @HttpCode(HttpStatus.OK) + @Post('/for-page') + async getShareForPage( @Body() dto: SharePageIdDto, @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { const page = await this.pageRepo.findById(dto.pageId); - - if (!page || workspace.id !== page.workspaceId) { - throw new NotFoundException('Page not found'); + if (!page) { + throw new NotFoundException('Shared page not found'); } const ability = await this.spaceAbility.createForUser(user, page.spaceId); - if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Share)) { + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Share)) { throw new ForbiddenException(); } - return this.shareService.getShareStatus(page.id, workspace.id); + return this.shareService.getShareForPage(page.id, workspace.id); } @HttpCode(HttpStatus.OK) @@ -103,7 +123,7 @@ export class ShareController { page, authUserId: user.id, workspaceId: workspace.id, - includeSubPages: createShareDto.includeSubPages, + createShareDto, }); } @@ -121,7 +141,7 @@ export class ShareController { throw new ForbiddenException(); } - //return this.shareService.update(page, updatePageDto, user.id); + return this.shareService.updateShare(share.id, updateShareDto); } @HttpCode(HttpStatus.OK) diff --git a/apps/server/src/core/share/share.service.ts b/apps/server/src/core/share/share.service.ts index 4994a3e5..854c5e59 100644 --- a/apps/server/src/core/share/share.service.ts +++ b/apps/server/src/core/share/share.service.ts @@ -4,7 +4,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; -import { ShareInfoDto } from './dto/share.dto'; +import { CreateShareDto, ShareInfoDto, UpdateShareDto } from './dto/share.dto'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB } from '@docmost/db/types/kysely.types'; import { generateSlugId } from '../../common/helpers'; @@ -55,9 +55,9 @@ export class ShareService { authUserId: string; workspaceId: string; page: Page; - includeSubPages: boolean; + createShareDto: CreateShareDto; }) { - const { authUserId, workspaceId, page, includeSubPages } = opts; + const { authUserId, workspaceId, page, createShareDto } = opts; try { const shares = await this.shareRepo.findByPageId(page.id); @@ -68,19 +68,35 @@ export class ShareService { return await this.shareRepo.insertShare({ key: generateSlugId(), pageId: page.id, - includeSubPages: includeSubPages, + includeSubPages: createShareDto.includeSubPages, + searchIndexing: true, creatorId: authUserId, spaceId: page.spaceId, workspaceId, }); } catch (err) { this.logger.error(err); - throw new BadRequestException('Failed to create page'); + throw new BadRequestException('Failed to share page'); + } + } + + async updateShare(shareId: string, updateShareDto: UpdateShareDto) { + try { + return this.shareRepo.updateShare( + { + includeSubPages: updateShareDto.includeSubPages, + searchIndexing: updateShareDto.searchIndexing, + }, + shareId, + ); + } catch (err) { + this.logger.error(err); + throw new BadRequestException('Failed to update share'); } } async getSharedPage(dto: ShareInfoDto, workspaceId: string) { - const share = await this.getShareStatus(dto.pageId, workspaceId); + const share = await this.getShareForPage(dto.pageId, workspaceId); if (!share) { throw new NotFoundException('Shared page not found'); @@ -94,25 +110,33 @@ export class ShareService { page.content = await this.updatePublicAttachments(page); if (!page) { - throw new NotFoundException('Page not found'); + throw new NotFoundException('Shared page not found'); } - return page; + return { page, share }; } - async getShareStatus(pageId: string, workspaceId: string) { + async getShareForPage(pageId: string, workspaceId: string) { // here we try to check if a page was shared directly or if it inherits the share from its closest shared ancestor const share = await this.db .withRecursive('page_hierarchy', (cte) => cte .selectFrom('pages') - .select(['id', 'parentPageId', sql`0`.as('level')]) + .select([ + 'id', + 'slugId', + 'pages.title', + 'parentPageId', + sql`0`.as('level'), + ]) .where(isValidUUID(pageId) ? 'id' : 'slugId', '=', pageId) .unionAll((union) => union .selectFrom('pages as p') .select([ 'p.id', + 'p.slugId', + 'p.title', 'p.parentPageId', // Increase the level by 1 for each ancestor. sql`ph.level + 1`.as('level'), @@ -124,10 +148,13 @@ export class ShareService { .leftJoin('shares', 'shares.pageId', 'page_hierarchy.id') .select([ 'page_hierarchy.id as sharedPageId', + 'page_hierarchy.slugId as sharedPageSlugId', + 'page_hierarchy.title as sharedPageTitle', 'page_hierarchy.level as level', - 'shares.id as shareId', - 'shares.key as shareKey', - 'shares.includeSubPages as includeSubPages', + 'shares.id', + 'shares.key', + 'shares.pageId', + 'shares.includeSubPages', 'shares.creatorId', 'shares.spaceId', 'shares.workspaceId', @@ -147,7 +174,22 @@ export class ShareService { throw new NotFoundException('Shared page not found'); } - return share; + return { + id: share.id, + key: share.key, + includeSubPages: share.includeSubPages, + pageId: share.pageId, + creatorId: share.creatorId, + spaceId: share.spaceId, + workspaceId: share.workspaceId, + createdAt: share.createdAt, + level: share.level, + sharedPage: { + id: share.sharedPageId, + slugId: share.sharedPageSlugId, + title: share.sharedPageTitle, + }, + }; } async getShareAncestorPage( diff --git a/apps/server/src/database/repos/share/share.repo.ts b/apps/server/src/database/repos/share/share.repo.ts index db8fb9e1..e4cae8ef 100644 --- a/apps/server/src/database/repos/share/share.repo.ts +++ b/apps/server/src/database/repos/share/share.repo.ts @@ -98,6 +98,7 @@ export class ShareRepo { .updateTable('shares') .set({ ...updatableShare, updatedAt: new Date() }) .where(!isValidUUID(shareId) ? 'key' : 'id', '=', shareId) + .returning(this.baseFields) .executeTakeFirst(); }