mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 21:51:10 +10:00
WIP
This commit is contained in:
@ -9,17 +9,15 @@ import classes from "./theme-toggle.module.css";
|
|||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const { setColorScheme } = useMantineColorScheme();
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
const computedColorScheme = useComputedColorScheme("light", {
|
const computedColorScheme = useComputedColorScheme();
|
||||||
getInitialValueInEffect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label="Toggle Color Scheme">
|
<Tooltip label="Toggle Color Scheme">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
setColorScheme(computedColorScheme === "light" ? "dark" : "light")
|
setColorScheme(computedColorScheme === "light" ? "dark" : "light");
|
||||||
}
|
}}
|
||||||
aria-label="Toggle color scheme"
|
aria-label="Toggle color scheme"
|
||||||
>
|
>
|
||||||
<IconSun className={classes.light} size={18} stroke={1.5} />
|
<IconSun className={classes.light} size={18} stroke={1.5} />
|
||||||
|
|||||||
@ -70,6 +70,10 @@
|
|||||||
background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
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 {
|
.row {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
13
apps/client/src/features/share/atoms/sidebar-atom.ts
Normal file
13
apps/client/src/features/share/atoms/sidebar-atom.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { atomWithWebStorage } from "@/lib/jotai-helper.ts";
|
||||||
|
import { atom } from 'jotai/index';
|
||||||
|
|
||||||
|
export const tableOfContentAsideAtom = atomWithWebStorage<boolean>(
|
||||||
|
"showTOC",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mobileTableOfContentAsideAtom = atom<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);
|
||||||
@ -1,31 +1,83 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
MantineSize,
|
|
||||||
Popover,
|
Popover,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconWorld } from "@tabler/icons-react";
|
import { IconWorld } from "@tabler/icons-react";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useShareStatusQuery } from "@/features/share/queries/share-query.ts";
|
import {
|
||||||
|
useCreateShareMutation,
|
||||||
|
useShareForPageQuery,
|
||||||
|
useUpdateShareMutation,
|
||||||
|
} from "@/features/share/queries/share-query.ts";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CopyTextButton from "@/components/common/copy.tsx";
|
import CopyTextButton from "@/components/common/copy.tsx";
|
||||||
|
import { getAppUrl } from "@/lib/config.ts";
|
||||||
|
|
||||||
export default function ShareModal() {
|
export default function ShareModal() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { pageSlug } = useParams();
|
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 =
|
const publicLink = `${getAppUrl()}/share/${share?.key}/${pageSlug}`;
|
||||||
window.location.protocol +'//' + window.location.host +
|
|
||||||
"/share/" +
|
|
||||||
data?.["share"]?.["key"] +
|
// TODO: think of permissions
|
||||||
"/" +
|
// controls should be read only for non space editors.
|
||||||
pageSlug;
|
|
||||||
|
|
||||||
|
// 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<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (share) {
|
||||||
|
setIsPagePublic(true);
|
||||||
|
} else {
|
||||||
|
setIsPagePublic(false);
|
||||||
|
}
|
||||||
|
}, [share, pageId]);
|
||||||
|
|
||||||
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
createShareMutation.mutateAsync({ pageId: pageId });
|
||||||
|
setIsPagePublic(value);
|
||||||
|
// on create refetch share
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubPagesChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
updateShareMutation.mutateAsync({
|
||||||
|
shareId: share.id,
|
||||||
|
includeSubPages: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIndexSearchChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
updateShareMutation.mutateAsync({
|
||||||
|
shareId: share.id,
|
||||||
|
searchIndexing: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover width={350} position="bottom" withArrow shadow="md">
|
<Popover width={350} position="bottom" withArrow shadow="md">
|
||||||
@ -39,50 +91,69 @@ export default function ShareModal() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
{isDescendantShared ? (
|
||||||
<div>
|
<Text>
|
||||||
<Text size="md">{t("Make page public")}</Text>
|
{t("This page was shared via")} {share.sharedPage.title}
|
||||||
</div>
|
</Text>
|
||||||
<ToggleShare isChecked={true}></ToggleShare>
|
) : (
|
||||||
</Group>
|
<>
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text>Share page</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Make it public to the internet
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={handleChange}
|
||||||
|
defaultChecked={isPagePublic}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Group my="sm" grow>
|
{pageIsShared && (
|
||||||
<TextInput
|
<>
|
||||||
variant="filled"
|
<Group my="sm" grow>
|
||||||
value={publicLink}
|
<TextInput
|
||||||
pointer
|
variant="filled"
|
||||||
readOnly
|
value={publicLink}
|
||||||
rightSection={<CopyTextButton text={publicLink} />}
|
readOnly
|
||||||
/>
|
rightSection={<CopyTextButton text={publicLink} />}
|
||||||
</Group>
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text>{t("Include sub pages")}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Include children of this page
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={handleSubPagesChange}
|
||||||
|
checked={share.includeSubPages}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl" mt="sm">
|
||||||
|
<div>
|
||||||
|
<Text>{t("Enable search indexing")}</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Allow search engine indexing
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={handleIndexSearchChange}
|
||||||
|
checked={share.searchIndexing}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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<HTMLInputElement>) => {
|
|
||||||
const value = event.currentTarget.checked;
|
|
||||||
setChecked(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
size={size}
|
|
||||||
label={label}
|
|
||||||
labelPosition="left"
|
|
||||||
defaultChecked={checked}
|
|
||||||
onChange={handleChange}
|
|
||||||
aria-label={t("Toggle share")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,13 +2,12 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
Affix,
|
Affix,
|
||||||
AppShell,
|
AppShell,
|
||||||
Burger,
|
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Text,
|
Text,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts";
|
import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import SharedTree from "@/features/share/components/shared-tree.tsx";
|
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 { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle.tsx";
|
import { ThemeToggle } from "@/components/theme-toggle.tsx";
|
||||||
import { useAtomValue } from "jotai";
|
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);
|
const MemoizedSharedTree = React.memo(SharedTree);
|
||||||
|
|
||||||
@ -24,7 +35,15 @@ export default function ShareShell({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
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 { shareId } = useParams();
|
||||||
const { data } = useGetSharedPageTreeQuery(shareId);
|
const { data } = useGetSharedPageTreeQuery(shareId);
|
||||||
const readOnlyEditor = useAtomValue(readOnlyEditorAtom);
|
const readOnlyEditor = useAtomValue(readOnlyEditorAtom);
|
||||||
@ -35,19 +54,51 @@ export default function ShareShell({
|
|||||||
navbar={{
|
navbar={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened, desktop: false },
|
collapsed: {
|
||||||
|
mobile: !mobileOpened,
|
||||||
|
desktop: !desktopOpened,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
aside={{
|
aside={{
|
||||||
width: 300,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "md",
|
||||||
collapsed: { mobile: true, desktop: false },
|
collapsed: {
|
||||||
|
mobile: mobileTocOpened,
|
||||||
|
desktop: tocOpened,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
<AppShell.Header>
|
<AppShell.Header>
|
||||||
<Group wrap="nowrap" justify="space-between" p="sm">
|
<Group wrap="nowrap" justify="space-between" p="sm">
|
||||||
<Burger opened={opened} onClick={toggle} size="sm" />
|
<Group>
|
||||||
<ThemeToggle />
|
{data?.pageTree?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
<Group>
|
||||||
|
<ThemeToggle />
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import { extractPageSlugId } from "@/lib";
|
|||||||
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
||||||
import classes from "@/features/page/tree/styles/tree.module.css";
|
import classes from "@/features/page/tree/styles/tree.module.css";
|
||||||
import styles from "./share.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 {
|
interface SharedTree {
|
||||||
sharedPageTree: ISharedPageTree;
|
sharedPageTree: ISharedPageTree;
|
||||||
@ -40,6 +42,7 @@ export default function SharedTree({ sharedPageTree }: SharedTree) {
|
|||||||
const [openTreeNodes, setOpenTreeNodes] = useAtom<OpenMap>(
|
const [openTreeNodes, setOpenTreeNodes] = useAtom<OpenMap>(
|
||||||
openSharedTreeNodesAtom,
|
openSharedTreeNodesAtom,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentNodeId = extractPageSlugId(pageSlug);
|
const currentNodeId = extractPageSlugId(pageSlug);
|
||||||
|
|
||||||
const treeData: SharedPageTreeNode[] = useMemo(() => {
|
const treeData: SharedPageTreeNode[] = useMemo(() => {
|
||||||
@ -99,6 +102,11 @@ export default function SharedTree({ sharedPageTree }: SharedTree) {
|
|||||||
setOpenTreeNodes(tree?.openState);
|
setOpenTreeNodes(tree?.openState);
|
||||||
}}
|
}}
|
||||||
initialOpenState={openTreeNodes}
|
initialOpenState={openTreeNodes}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (tree && tree.focusedNode) {
|
||||||
|
tree.select(tree.focusedNode);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{Node}
|
{Node}
|
||||||
</Tree>
|
</Tree>
|
||||||
@ -108,9 +116,9 @@ export default function SharedTree({ sharedPageTree }: SharedTree) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Node({ node, style, tree }: NodeRendererProps<any>) {
|
function Node({ node, style, tree }: NodeRendererProps<any>) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { shareId } = useParams();
|
const { shareId } = useParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [, setMobileSidebarState] = useAtom(mobileSidebarAtom);
|
||||||
|
|
||||||
const pageUrl = buildSharedPageUrl({
|
const pageUrl = buildSharedPageUrl({
|
||||||
shareId: shareId,
|
shareId: shareId,
|
||||||
@ -125,6 +133,9 @@ function Node({ node, style, tree }: NodeRendererProps<any>) {
|
|||||||
className={clsx(classes.node, node.state, styles.treeNode)}
|
className={clsx(classes.node, node.state, styles.treeNode)}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={pageUrl}
|
to={pageUrl}
|
||||||
|
onClick={() => {
|
||||||
|
setMobileSidebarState(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PageArrow node={node} />
|
<PageArrow node={node} />
|
||||||
<span className={classes.text}>{node.data.name || t("untitled")}</span>
|
<span className={classes.text}>{node.data.name || t("untitled")}</span>
|
||||||
|
|||||||
@ -2,23 +2,26 @@ import {
|
|||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ICreateShare,
|
ICreateShare,
|
||||||
ISharedItem,
|
ISharedItem, ISharedPage,
|
||||||
ISharedPageTree,
|
ISharedPageTree,
|
||||||
|
IShareForPage,
|
||||||
IShareInfoInput,
|
IShareInfoInput,
|
||||||
} from "@/features/share/types/share.types.ts";
|
IUpdateShare,
|
||||||
|
} from '@/features/share/types/share.types.ts';
|
||||||
import {
|
import {
|
||||||
createShare,
|
createShare,
|
||||||
deleteShare,
|
deleteShare,
|
||||||
getSharedPageTree,
|
getSharedPageTree,
|
||||||
|
getShareForPage,
|
||||||
getShareInfo,
|
getShareInfo,
|
||||||
getShares,
|
getShares,
|
||||||
getShareStatus,
|
|
||||||
updateShare,
|
updateShare,
|
||||||
} from "@/features/share/services/share-service.ts";
|
} from "@/features/share/services/share-service.ts";
|
||||||
import { IPage } from "@/features/page/types/page.types.ts";
|
import { IPage } from "@/features/page/types/page.types.ts";
|
||||||
@ -36,7 +39,7 @@ export function useGetSharesQuery(
|
|||||||
|
|
||||||
export function useShareQuery(
|
export function useShareQuery(
|
||||||
shareInput: Partial<IShareInfoInput>,
|
shareInput: Partial<IShareInfoInput>,
|
||||||
): UseQueryResult<IPage, Error> {
|
): UseQueryResult<ISharedPage, Error> {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["shares", shareInput],
|
queryKey: ["shares", shareInput],
|
||||||
queryFn: () => getShareInfo(shareInput),
|
queryFn: () => getShareInfo(shareInput),
|
||||||
@ -46,12 +49,12 @@ export function useShareQuery(
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShareStatusQuery(
|
export function useShareForPageQuery(
|
||||||
pageId: string,
|
pageId: string,
|
||||||
): UseQueryResult<IPage, Error> {
|
): UseQueryResult<IShareForPage, Error> {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["share-status", pageId],
|
queryKey: ["share-for-page", pageId],
|
||||||
queryFn: () => getShareStatus(pageId),
|
queryFn: () => getShareForPage(pageId),
|
||||||
enabled: !!pageId,
|
enabled: !!pageId,
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
@ -63,7 +66,6 @@ export function useCreateShareMutation() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return useMutation<any, Error, ICreateShare>({
|
return useMutation<any, Error, ICreateShare>({
|
||||||
mutationFn: (data) => createShare(data),
|
mutationFn: (data) => createShare(data),
|
||||||
onSuccess: (data) => {},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
notifications.show({ message: t("Failed to share page"), color: "red" });
|
notifications.show({ message: t("Failed to share page"), color: "red" });
|
||||||
},
|
},
|
||||||
@ -71,8 +73,15 @@ export function useCreateShareMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateShareMutation() {
|
export function useUpdateShareMutation() {
|
||||||
return useMutation<any, Error, Partial<IShareInfoInput>>({
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<any, Error, IUpdateShare>({
|
||||||
mutationFn: (data) => updateShare(data),
|
mutationFn: (data) => updateShare(data),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.refetchQueries({
|
||||||
|
predicate: (item) =>
|
||||||
|
["share-for-page"].includes(item.queryKey[0] as string),
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { IPage } from "@/features/page/types/page.types";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ICreateShare,
|
ICreateShare,
|
||||||
ISharedItem,
|
ISharedItem, ISharedPage,
|
||||||
ISharedPageTree,
|
ISharedPageTree,
|
||||||
|
IShareForPage,
|
||||||
IShareInfoInput,
|
IShareInfoInput,
|
||||||
} from "@/features/share/types/share.types.ts";
|
IUpdateShare,
|
||||||
|
} from '@/features/share/types/share.types.ts';
|
||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
|
|
||||||
export async function getShares(
|
export async function getShares(
|
||||||
@ -21,22 +23,20 @@ export async function createShare(data: ICreateShare): Promise<any> {
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getShareStatus(pageId: string): Promise<any> {
|
export async function updateShare(data: IUpdateShare): Promise<any> {
|
||||||
const req = await api.post<any>("/shares/status", { pageId });
|
const req = await api.post<any>("/shares/update", data);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getShareForPage(pageId: string): Promise<IShareForPage> {
|
||||||
|
const req = await api.post<any>("/shares/for-page", { pageId });
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getShareInfo(
|
export async function getShareInfo(
|
||||||
shareInput: Partial<IShareInfoInput>,
|
shareInput: Partial<IShareInfoInput>,
|
||||||
): Promise<IPage> {
|
): Promise<ISharedPage> {
|
||||||
const req = await api.post<IPage>("/shares/info", shareInput);
|
const req = await api.post<ISharedPage>("/shares/page-info", shareInput);
|
||||||
return req.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateShare(
|
|
||||||
data: Partial<IShareInfoInput>,
|
|
||||||
): Promise<any> {
|
|
||||||
const req = await api.post<any>("/shares/update", data);
|
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface IShare {
|
|||||||
key: string;
|
key: string;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
includeSubPages: boolean;
|
includeSubPages: boolean;
|
||||||
|
searchIndexing: boolean;
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -32,11 +33,36 @@ export interface ISharedItem extends IShare {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateShare {
|
export interface ISharedPage extends IShare {
|
||||||
pageId: string;
|
page: IPage;
|
||||||
includeSubPages?: boolean;
|
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 {
|
export interface IShareInfoInput {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useShareQuery } from "@/features/share/queries/share-query.ts";
|
import { useShareQuery } from "@/features/share/queries/share-query.ts";
|
||||||
import { Container } from "@mantine/core";
|
import { Container } from "@mantine/core";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
|
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||||
@ -11,19 +11,27 @@ import { Error404 } from "@/components/ui/error-404.tsx";
|
|||||||
export default function SingleSharedPage() {
|
export default function SingleSharedPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
|
const { shareId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {
|
const { data, isLoading, isError, error } = useShareQuery({
|
||||||
data: page,
|
pageId: extractPageSlugId(pageSlug),
|
||||||
isLoading,
|
});
|
||||||
isError,
|
|
||||||
error,
|
useEffect(() => {
|
||||||
} = useShareQuery({ pageId: extractPageSlugId(pageSlug) });
|
if (shareId && data) {
|
||||||
|
if (data.share.key !== shareId) {
|
||||||
|
// affects parent share, what to do?
|
||||||
|
//navigate(`/share/${data.share.key}/${pageSlug}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [shareId, data]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !page) {
|
if (isError || !data) {
|
||||||
if ([401, 403, 404].includes(error?.["status"])) {
|
if ([401, 403, 404].includes(error?.["status"])) {
|
||||||
return <Error404 />;
|
return <Error404 />;
|
||||||
}
|
}
|
||||||
@ -33,14 +41,14 @@ export default function SingleSharedPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
|
<title>{`${data?.page?.icon || ""} ${data?.page?.title || t("untitled")}`}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<Container size={900}>
|
<Container size={900}>
|
||||||
<ReadonlyPageEditor
|
<ReadonlyPageEditor
|
||||||
key={page.id}
|
key={data.page.id}
|
||||||
title={page.title}
|
title={data.page.title}
|
||||||
content={page.content}
|
content={data.page.content}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class CreateShareDto {
|
|
||||||
@IsString()
|
|
||||||
pageId: string;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
includeSubPages: boolean;
|
|
||||||
}
|
|
||||||
@ -6,6 +6,30 @@ import {
|
|||||||
IsUUID,
|
IsUUID,
|
||||||
} from 'class-validator';
|
} 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 {
|
export class ShareIdDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class UpdateShareDto {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
shareId: string;
|
|
||||||
}
|
|
||||||
@ -18,9 +18,13 @@ import {
|
|||||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||||
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
|
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
|
||||||
import { ShareService } from './share.service';
|
import { ShareService } from './share.service';
|
||||||
import { UpdateShareDto } from './dto/update-page.dto';
|
import {
|
||||||
import { CreateShareDto } from './dto/create-share.dto';
|
CreateShareDto,
|
||||||
import { ShareIdDto, ShareInfoDto, SharePageIdDto } from './dto/share.dto';
|
ShareIdDto,
|
||||||
|
ShareInfoDto,
|
||||||
|
SharePageIdDto,
|
||||||
|
UpdateShareDto,
|
||||||
|
} from './dto/share.dto';
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { Public } from '../../common/decorators/public.decorator';
|
import { Public } from '../../common/decorators/public.decorator';
|
||||||
@ -48,8 +52,8 @@ export class ShareController {
|
|||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/info')
|
@Post('/page-info')
|
||||||
async getShare(
|
async getSharedPageInfo(
|
||||||
@Body() dto: ShareInfoDto,
|
@Body() dto: ShareInfoDto,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
@ -61,24 +65,40 @@ export class ShareController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('/status')
|
@Post('/info')
|
||||||
async getShareStatus(
|
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,
|
@Body() dto: SharePageIdDto,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const page = await this.pageRepo.findById(dto.pageId);
|
const page = await this.pageRepo.findById(dto.pageId);
|
||||||
|
if (!page) {
|
||||||
if (!page || workspace.id !== page.workspaceId) {
|
throw new NotFoundException('Shared page not found');
|
||||||
throw new NotFoundException('Page not found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
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();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.shareService.getShareStatus(page.id, workspace.id);
|
return this.shareService.getShareForPage(page.id, workspace.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ -103,7 +123,7 @@ export class ShareController {
|
|||||||
page,
|
page,
|
||||||
authUserId: user.id,
|
authUserId: user.id,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
includeSubPages: createShareDto.includeSubPages,
|
createShareDto,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +141,7 @@ export class ShareController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
//return this.shareService.update(page, updatePageDto, user.id);
|
return this.shareService.updateShare(share.id, updateShareDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ShareInfoDto } from './dto/share.dto';
|
import { CreateShareDto, ShareInfoDto, UpdateShareDto } from './dto/share.dto';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
import { generateSlugId } from '../../common/helpers';
|
import { generateSlugId } from '../../common/helpers';
|
||||||
@ -55,9 +55,9 @@ export class ShareService {
|
|||||||
authUserId: string;
|
authUserId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
page: Page;
|
page: Page;
|
||||||
includeSubPages: boolean;
|
createShareDto: CreateShareDto;
|
||||||
}) {
|
}) {
|
||||||
const { authUserId, workspaceId, page, includeSubPages } = opts;
|
const { authUserId, workspaceId, page, createShareDto } = opts;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shares = await this.shareRepo.findByPageId(page.id);
|
const shares = await this.shareRepo.findByPageId(page.id);
|
||||||
@ -68,19 +68,35 @@ export class ShareService {
|
|||||||
return await this.shareRepo.insertShare({
|
return await this.shareRepo.insertShare({
|
||||||
key: generateSlugId(),
|
key: generateSlugId(),
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
includeSubPages: includeSubPages,
|
includeSubPages: createShareDto.includeSubPages,
|
||||||
|
searchIndexing: true,
|
||||||
creatorId: authUserId,
|
creatorId: authUserId,
|
||||||
spaceId: page.spaceId,
|
spaceId: page.spaceId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(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) {
|
async getSharedPage(dto: ShareInfoDto, workspaceId: string) {
|
||||||
const share = await this.getShareStatus(dto.pageId, workspaceId);
|
const share = await this.getShareForPage(dto.pageId, workspaceId);
|
||||||
|
|
||||||
if (!share) {
|
if (!share) {
|
||||||
throw new NotFoundException('Shared page not found');
|
throw new NotFoundException('Shared page not found');
|
||||||
@ -94,25 +110,33 @@ export class ShareService {
|
|||||||
page.content = await this.updatePublicAttachments(page);
|
page.content = await this.updatePublicAttachments(page);
|
||||||
|
|
||||||
if (!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
|
// 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
|
const share = await this.db
|
||||||
.withRecursive('page_hierarchy', (cte) =>
|
.withRecursive('page_hierarchy', (cte) =>
|
||||||
cte
|
cte
|
||||||
.selectFrom('pages')
|
.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)
|
.where(isValidUUID(pageId) ? 'id' : 'slugId', '=', pageId)
|
||||||
.unionAll((union) =>
|
.unionAll((union) =>
|
||||||
union
|
union
|
||||||
.selectFrom('pages as p')
|
.selectFrom('pages as p')
|
||||||
.select([
|
.select([
|
||||||
'p.id',
|
'p.id',
|
||||||
|
'p.slugId',
|
||||||
|
'p.title',
|
||||||
'p.parentPageId',
|
'p.parentPageId',
|
||||||
// Increase the level by 1 for each ancestor.
|
// Increase the level by 1 for each ancestor.
|
||||||
sql`ph.level + 1`.as('level'),
|
sql`ph.level + 1`.as('level'),
|
||||||
@ -124,10 +148,13 @@ export class ShareService {
|
|||||||
.leftJoin('shares', 'shares.pageId', 'page_hierarchy.id')
|
.leftJoin('shares', 'shares.pageId', 'page_hierarchy.id')
|
||||||
.select([
|
.select([
|
||||||
'page_hierarchy.id as sharedPageId',
|
'page_hierarchy.id as sharedPageId',
|
||||||
|
'page_hierarchy.slugId as sharedPageSlugId',
|
||||||
|
'page_hierarchy.title as sharedPageTitle',
|
||||||
'page_hierarchy.level as level',
|
'page_hierarchy.level as level',
|
||||||
'shares.id as shareId',
|
'shares.id',
|
||||||
'shares.key as shareKey',
|
'shares.key',
|
||||||
'shares.includeSubPages as includeSubPages',
|
'shares.pageId',
|
||||||
|
'shares.includeSubPages',
|
||||||
'shares.creatorId',
|
'shares.creatorId',
|
||||||
'shares.spaceId',
|
'shares.spaceId',
|
||||||
'shares.workspaceId',
|
'shares.workspaceId',
|
||||||
@ -147,7 +174,22 @@ export class ShareService {
|
|||||||
throw new NotFoundException('Shared page not found');
|
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(
|
async getShareAncestorPage(
|
||||||
|
|||||||
@ -98,6 +98,7 @@ export class ShareRepo {
|
|||||||
.updateTable('shares')
|
.updateTable('shares')
|
||||||
.set({ ...updatableShare, updatedAt: new Date() })
|
.set({ ...updatableShare, updatedAt: new Date() })
|
||||||
.where(!isValidUUID(shareId) ? 'key' : 'id', '=', shareId)
|
.where(!isValidUUID(shareId) ? 'key' : 'id', '=', shareId)
|
||||||
|
.returning(this.baseFields)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user