This commit is contained in:
Philipinho
2025-04-18 20:46:55 +01:00
parent c20931bd95
commit 5ccea524c7
14 changed files with 220 additions and 85 deletions

View File

@ -362,5 +362,22 @@
"Move page to a different space.": "Move page to a different space.", "Move page to a different space.": "Move page to a different space.",
"Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...", "Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...",
"Table of contents": "Table of contents", "Table of contents": "Table of contents",
"Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents." "Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents.",
"Share": "Share",
"Public sharing": "Public sharing",
"Shared by": "Shared by",
"Shared at": "Shared at",
"Inherits public sharing from": "Inherits public sharing from",
"Publicly shared": "Publicly shared",
"Share to web": "Share to web",
"Anyone with the link can view this page": "Anyone with the link can view this page",
"Make this page publicly accessible": "Make this page publicly accessible",
"Include sub-pages": "Include sub-pages",
"Make sub-pages public too": "Make sub-pages public too",
"Allow search engines to index page": "Allow search engines to index page",
"Open page": "Open page",
"Page": "Page",
"Revoke public link": "Revoke public link",
"Are you sure you want to revoke this public link?": "Are you sure you want to revoke this public link?",
"Publicly shared pages from spaces you are a member of will appear here": "Publicly shared pages from spaces you are a member of will appear here"
} }

View File

@ -86,7 +86,7 @@ export default function App() {
<Route path={"groups"} element={<Groups />} /> <Route path={"groups"} element={<Groups />} />
<Route path={"groups/:groupId"} element={<GroupInfo />} /> <Route path={"groups/:groupId"} element={<GroupInfo />} />
<Route path={"spaces"} element={<Spaces />} /> <Route path={"spaces"} element={<Spaces />} />
<Route path={"shares"} element={<Shares />} /> <Route path={"sharing"} element={<Shares />} />
<Route path={"security"} element={<Security />} /> <Route path={"security"} element={<Security />} />
{!isCloud() && <Route path={"license"} element={<License />} />} {!isCloud() && <Route path={"license"} element={<License />} />}
{isCloud() && <Route path={"billing"} element={<Billing />} />} {isCloud() && <Route path={"billing"} element={<Billing />} />}

View File

@ -82,7 +82,7 @@ const groupedData: DataGroup[] = [
}, },
{ label: "Groups", icon: IconUsersGroup, path: "/settings/groups" }, { label: "Groups", icon: IconUsersGroup, path: "/settings/groups" },
{ label: "Spaces", icon: IconSpaces, path: "/settings/spaces" }, { label: "Spaces", icon: IconSpaces, path: "/settings/spaces" },
{ label: "Sharing", icon: IconWorld, path: "/settings/shares" }, { label: "Public sharing", icon: IconWorld, path: "/settings/sharing" },
], ],
}, },
@ -172,7 +172,7 @@ export default function SettingsSidebar() {
case "Security & SSO": case "Security & SSO":
prefetchHandler = prefetchSsoProviders; prefetchHandler = prefetchSsoProviders;
break; break;
case "Sharing": case "Public sharing":
prefetchHandler = prefetchShares; prefetchHandler = prefetchShares;
break; break;
default: default:

View File

@ -59,7 +59,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
</Tooltip> </Tooltip>
)} )}
<ShareModal/> <ShareModal readOnly={readOnly}/>
<Tooltip label={t("Comments")} openDelay={250} withArrow> <Tooltip label={t("Comments")} openDelay={250} withArrow>
<ActionIcon <ActionIcon

View File

@ -8,7 +8,6 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useUserRole from "@/hooks/use-user-role.tsx";
import { ISharedItem } from "@/features/share/types/share.types.ts"; import { ISharedItem } from "@/features/share/types/share.types.ts";
import { import {
buildPageUrl, buildPageUrl,
@ -17,15 +16,16 @@ import {
import { useClipboard } from "@mantine/hooks"; import { useClipboard } from "@mantine/hooks";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useDeleteShareMutation } from "@/features/share/queries/share-query.ts";
interface Props { interface Props {
share: ISharedItem; share: ISharedItem;
} }
export default function ShareActionMenu({ share }: Props) { export default function ShareActionMenu({ share }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { isAdmin } = useUserRole();
const navigate = useNavigate(); const navigate = useNavigate();
const clipboard = useClipboard(); const clipboard = useClipboard();
const deleteShareMutation = useDeleteShareMutation();
const openPage = () => { const openPage = () => {
const pageLink = buildPageUrl( const pageLink = buildPageUrl(
@ -38,7 +38,7 @@ export default function ShareActionMenu({ share }: Props) {
const copyLink = () => { const copyLink = () => {
const shareLink = buildSharedPageUrl({ const shareLink = buildSharedPageUrl({
shareId: share.includeSubPages ? share.key : undefined, shareId: share.key,
pageTitle: share.page.title, pageTitle: share.page.title,
pageSlugId: share.page.slugId, pageSlugId: share.page.slugId,
}); });
@ -47,19 +47,19 @@ export default function ShareActionMenu({ share }: Props) {
notifications.show({ message: t("Link copied") }); notifications.show({ message: t("Link copied") });
}; };
const onRevoke = async () => { const onRevoke = async () => {
// deleteShareMutation.mutateAsync(share.key);
}; };
const openRevokeModal = () => const openRevokeModal = () =>
modals.openConfirmModal({ modals.openConfirmModal({
title: t("Unshare page"), title: t("Revoke public link"),
children: ( children: (
<Text size="sm"> <Text size="sm">
{t("Are you sure you want to unshare this page?")} {t("Are you sure you want to revoke this public link?")}
</Text> </Text>
), ),
centered: true, centered: true,
labels: { confirm: t("Unshare"), cancel: t("Don't") }, labels: { confirm: t("Revoke"), cancel: t("Don't") },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: onRevoke, onConfirm: onRevoke,
}); });
@ -95,9 +95,9 @@ export default function ShareActionMenu({ share }: Props) {
c="red" c="red"
onClick={openRevokeModal} onClick={openRevokeModal}
leftSection={<IconTrash size={16} />} leftSection={<IconTrash size={16} />}
disabled={!isAdmin} disabled={share.space?.userRole === "reader"}
> >
{t("Unshare")} {t("Revoke")}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>

View File

@ -43,7 +43,7 @@ export default function ShareList() {
component={Link} component={Link}
target="_blank" target="_blank"
to={buildSharedPageUrl({ to={buildSharedPageUrl({
shareId: share.includeSubPages ? share.key : undefined, shareId: share.key,
pageTitle: share.page.title, pageTitle: share.page.title,
pageSlugId: share.page.slugId, pageSlugId: share.page.slugId,
})} })}
@ -52,7 +52,7 @@ export default function ShareList() {
{getPageIcon(share.page.icon)} {getPageIcon(share.page.icon)}
<div className={classes.shareLinkText}> <div className={classes.shareLinkText}>
<Text fz="sm" fw={500} lineClamp={1}> <Text fz="sm" fw={500} lineClamp={1}>
{share.page.title} {share.page.title || t("untitled")}
</Text> </Text>
</div> </div>
</Group> </Group>

View File

@ -1,31 +1,42 @@
import { import {
Button, ActionIcon,
Anchor,
Group, Group,
Indicator,
Popover, Popover,
Switch, Switch,
Text, Text,
TextInput, TextInput,
Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconWorld } from "@tabler/icons-react"; import { IconWorld } from "@tabler/icons-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
useCreateShareMutation, useCreateShareMutation,
useDeleteShareMutation,
useShareForPageQuery, useShareForPageQuery,
useUpdateShareMutation, useUpdateShareMutation,
} from "@/features/share/queries/share-query.ts"; } from "@/features/share/queries/share-query.ts";
import { useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { extractPageSlugId } from "@/lib"; import { extractPageSlugId, getPageIcon } 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"; import { getAppUrl } from "@/lib/config.ts";
import { buildPageUrl } from "@/features/page/page.utils.ts";
import classes from "@/features/share/components/share.module.css";
export default function ShareModal() { interface ShareModalProps {
readOnly: boolean;
}
export default function ShareModal({ readOnly }: ShareModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { pageSlug } = useParams(); const { pageSlug } = useParams();
const pageId = extractPageSlugId(pageSlug); const pageId = extractPageSlugId(pageSlug);
const { data: share } = useShareForPageQuery(pageId); const { data: share } = useShareForPageQuery(pageId);
const { spaceSlug } = useParams();
const createShareMutation = useCreateShareMutation(); const createShareMutation = useCreateShareMutation();
const updateShareMutation = useUpdateShareMutation(); const updateShareMutation = useUpdateShareMutation();
const deleteShareMutation = useDeleteShareMutation();
// pageIsShared means that the share exists and its level equals zero. // pageIsShared means that the share exists and its level equals zero.
const pageIsShared = share && share.level === 0; const pageIsShared = share && share.level === 0;
// if level is greater than zero, then it is a descendant page from a shared page // if level is greater than zero, then it is a descendant page from a shared page
@ -33,16 +44,6 @@ export default function ShareModal() {
const publicLink = `${getAppUrl()}/share/${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<boolean>(false); const [isPagePublic, setIsPagePublic] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (share) { if (share) {
@ -54,9 +55,20 @@ export default function ShareModal() {
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked; const value = event.currentTarget.checked;
createShareMutation.mutateAsync({ pageId: pageId });
setIsPagePublic(value); if (value) {
// on create refetch share createShareMutation.mutateAsync({
pageId: pageId,
includeSubPages: true,
searchIndexing: true,
});
setIsPagePublic(value);
} else {
if (share && share.id) {
deleteShareMutation.mutateAsync(share.id);
setIsPagePublic(value);
}
}
}; };
const handleSubPagesChange = async ( const handleSubPagesChange = async (
@ -82,32 +94,74 @@ export default function ShareModal() {
return ( return (
<Popover width={350} position="bottom" withArrow shadow="md"> <Popover width={350} position="bottom" withArrow shadow="md">
<Popover.Target> <Popover.Target>
<Button <Tooltip label={t("Share")} openDelay={250} withArrow>
variant="default" <Indicator
style={{ border: "none" }} color="green"
leftSection={<IconWorld size={20} stroke={1.5} />} offset={7}
> disabled={!isPagePublic}
Share withBorder
</Button> >
<ActionIcon variant="default" style={{ border: "none" }}>
<IconWorld size={20} stroke={1.5} />
</ActionIcon>
</Indicator>
</Tooltip>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
{isDescendantShared ? ( {isDescendantShared ? (
<Text> <>
{t("This page was shared via")} {share.sharedPage.title} <Text size="sm">{t("Inherits public sharing from")}</Text>
</Text> <Anchor
size="sm"
underline="never"
style={{
cursor: "pointer",
color: "var(--mantine-color-text)",
}}
component={Link}
to={buildPageUrl(
spaceSlug,
share.sharedPage.slugId,
share.sharedPage.title,
)}
>
<Group gap="4" wrap="nowrap" my="sm">
{getPageIcon(share.sharedPage.icon)}
<div className={classes.shareLinkText}>
<Text fz="sm" fw={500} lineClamp={1}>
{share.sharedPage.title || t("untitled")}
</Text>
</div>
</Group>
</Anchor>
<Group my="sm" grow>
<TextInput
variant="filled"
value={publicLink}
readOnly
rightSection={<CopyTextButton text={publicLink} />}
/>
</Group>
</>
) : ( ) : (
<> <>
<Group justify="space-between" wrap="nowrap" gap="xl"> <Group justify="space-between" wrap="nowrap" gap="xl">
<div> <div>
<Text>Share page</Text> <Text size="sm">
{isPagePublic ? t("Publicly shared") : t("Share to web")}
</Text>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Make it public to the internet {isPagePublic
? t("Anyone with the link can view this page")
: t("Make this page publicly accessible")}
</Text> </Text>
</div> </div>
<Switch <Switch
onChange={handleChange} onChange={handleChange}
defaultChecked={isPagePublic} defaultChecked={isPagePublic}
size="sm" disabled={readOnly}
size="xs"
/> />
</Group> </Group>
@ -124,29 +178,32 @@ export default function ShareModal() {
<Group justify="space-between" wrap="nowrap" gap="xl"> <Group justify="space-between" wrap="nowrap" gap="xl">
<div> <div>
<Text>{t("Include sub pages")}</Text> <Text size="sm">{t("Include sub-pages")}</Text>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Include children of this page {t("Make sub-pages public too")}
</Text> </Text>
</div> </div>
<Switch <Switch
onChange={handleSubPagesChange} onChange={handleSubPagesChange}
checked={share.includeSubPages} checked={share.includeSubPages}
size="xs" size="xs"
disabled={readOnly}
/> />
</Group> </Group>
<Group justify="space-between" wrap="nowrap" gap="xl" mt="sm"> <Group justify="space-between" wrap="nowrap" gap="xl" mt="sm">
<div> <div>
<Text>{t("Enable search indexing")}</Text> <Text size="sm">{t("Search engine indexing")}</Text>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Allow search engine indexing {t("Allow search engines to index page")}
</Text> </Text>
</div> </div>
<Switch <Switch
onChange={handleIndexSearchChange} onChange={handleIndexSearchChange}
checked={share.searchIndexing} checked={share.searchIndexing}
size="xs" size="xs"
disabled={readOnly}
/> />
</Group> </Group>
</> </>

View File

@ -6,7 +6,6 @@ import {
Button, Button,
Group, Group,
ScrollArea, ScrollArea,
Text,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts"; import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts";
@ -56,14 +55,16 @@ export default function ShareShell({
return ( return (
<AppShell <AppShell
header={{ height: 48 }} header={{ height: 48 }}
navbar={{ {...(data?.pageTree?.length > 1 && {
width: 300, navbar: {
breakpoint: "sm", width: 300,
collapsed: { breakpoint: "sm",
mobile: !mobileOpened, collapsed: {
desktop: !desktopOpened, mobile: !mobileOpened,
desktop: !desktopOpened,
},
}, },
}} })}
aside={{ aside={{
width: 300, width: 300,
breakpoint: "sm", breakpoint: "sm",
@ -77,7 +78,7 @@ export default function ShareShell({
<AppShell.Header> <AppShell.Header>
<Group wrap="nowrap" justify="space-between" py="sm" px="xl"> <Group wrap="nowrap" justify="space-between" py="sm" px="xl">
<Group> <Group>
{data?.pageTree?.length > 0 && ( {data?.pageTree?.length > 1 && (
<> <>
<Tooltip label={t("Sidebar toggle")}> <Tooltip label={t("Sidebar toggle")}>
<SidebarToggle <SidebarToggle
@ -133,7 +134,7 @@ export default function ShareShell({
</Group> </Group>
</AppShell.Header> </AppShell.Header>
{data?.pageTree?.length > 0 && ( {data?.pageTree?.length > 1 && (
<AppShell.Navbar p="md"> <AppShell.Navbar p="md">
<MemoizedSharedTree sharedPageTree={data} /> <MemoizedSharedTree sharedPageTree={data} />
</AppShell.Navbar> </AppShell.Navbar>

View File

@ -7,7 +7,7 @@ import {
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useElementSize, useMergedRef } from "@mantine/hooks"; import { useElementSize, useMergedRef } from "@mantine/hooks";
import { SpaceTreeNode } from "@/features/page/tree/types.ts"; import { SpaceTreeNode } from "@/features/page/tree/types.ts";
import { Link, useNavigate, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { atom, useAtom } from "jotai/index"; import { atom, useAtom } from "jotai/index";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { buildSharedPageUrl } from "@/features/page/page.utils.ts"; import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
@ -22,7 +22,6 @@ 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"; import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
interface SharedTree { interface SharedTree {
@ -54,12 +53,16 @@ export default function SharedTree({ sharedPageTree }: SharedTree) {
const parentNodeId = treeData?.[0]?.slugId; const parentNodeId = treeData?.[0]?.slugId;
if (parentNodeId && tree) { if (parentNodeId && tree) {
const parentNode = tree.get(parentNodeId);
setTimeout(() => { setTimeout(() => {
tree.openSiblings(tree.get(parentNodeId)); if (parentNode) {
tree.openSiblings(parentNode);
}
}); });
// open direct children of parent node // open direct children of parent node
tree.get(parentNodeId).children.forEach((node) => { parentNode?.children.forEach((node) => {
tree.openSiblings(node); tree.openSiblings(node);
}); });
} }
@ -84,7 +87,7 @@ export default function SharedTree({ sharedPageTree }: SharedTree) {
<div ref={mergedRef} className={classes.treeContainer}> <div ref={mergedRef} className={classes.treeContainer}>
{rootElement.current && ( {rootElement.current && (
<Tree <Tree
initialData={treeData} data={treeData}
disableDrag={true} disableDrag={true}
disableDrop={true} disableDrop={true}
disableEdit={true} disableEdit={true}

View File

@ -9,12 +9,13 @@ import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ICreateShare, ICreateShare,
ISharedItem, ISharedPage, ISharedItem,
ISharedPage,
ISharedPageTree, ISharedPageTree,
IShareForPage, IShareForPage,
IShareInfoInput, IShareInfoInput,
IUpdateShare, IUpdateShare,
} from '@/features/share/types/share.types.ts'; } from "@/features/share/types/share.types.ts";
import { import {
createShare, createShare,
deleteShare, deleteShare,
@ -26,6 +27,7 @@ import {
} 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";
import { IPagination, QueryParams } from "@/lib/types.ts"; import { IPagination, QueryParams } from "@/lib/types.ts";
import { useEffect } from "react";
export function useGetSharesQuery( export function useGetSharesQuery(
params?: QueryParams, params?: QueryParams,
@ -56,7 +58,8 @@ export function useShareForPageQuery(
queryKey: ["share-for-page", pageId], queryKey: ["share-for-page", pageId],
queryFn: () => getShareForPage(pageId), queryFn: () => getShareForPage(pageId),
enabled: !!pageId, enabled: !!pageId,
staleTime: 5 * 60 * 1000, staleTime: 0,
retry: false,
}); });
return query; return query;
@ -64,8 +67,16 @@ export function useShareForPageQuery(
export function useCreateShareMutation() { export function useCreateShareMutation() {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<any, Error, ICreateShare>({ return useMutation<any, Error, ICreateShare>({
mutationFn: (data) => createShare(data), mutationFn: (data) => createShare(data),
onSuccess: (data) => {
queryClient.invalidateQueries({
predicate: (item) =>
["share-for-page", "share-list"].includes(item.queryKey[0] as string),
});
},
onError: (error) => { onError: (error) => {
notifications.show({ message: t("Failed to share page"), color: "red" }); notifications.show({ message: t("Failed to share page"), color: "red" });
}, },
@ -77,9 +88,22 @@ export function useUpdateShareMutation() {
return useMutation<any, Error, IUpdateShare>({ return useMutation<any, Error, IUpdateShare>({
mutationFn: (data) => updateShare(data), mutationFn: (data) => updateShare(data),
onSuccess: (data) => { onSuccess: (data) => {
queryClient.refetchQueries({ queryClient.invalidateQueries({
predicate: (item) => predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string), ["share-for-page", "share-list"].includes(item.queryKey[0] as string),
});
},
onError: (error, params) => {
if (error?.["status"] === 404) {
queryClient.removeQueries({
predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string),
});
}
notifications.show({
message: error?.["response"]?.data?.message || "Share share not found",
color: "red",
}); });
}, },
}); });
@ -87,14 +111,33 @@ export function useUpdateShareMutation() {
export function useDeleteShareMutation() { export function useDeleteShareMutation() {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (shareId: string) => deleteShare(shareId), mutationFn: (shareId: string) => deleteShare(shareId),
onSuccess: () => { onSuccess: (data) => {
queryClient.removeQueries({
predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string),
});
queryClient.invalidateQueries({
predicate: (item) =>
["share-list"].includes(item.queryKey[0] as string),
});
notifications.show({ message: t("Share deleted successfully") }); notifications.show({ message: t("Share deleted successfully") });
}, },
onError: (error) => { onError: (error) => {
if (error?.["status"] === 404) {
queryClient.removeQueries({
predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string),
});
}
notifications.show({ notifications.show({
message: t("Failed to delete share"), message: error?.["response"]?.data?.message || "Failed to delete share",
color: "red", color: "red",
}); });
}, },

View File

@ -25,6 +25,7 @@ export interface ISharedItem extends IShare {
id: string; id: string;
name: string; name: string;
slug: string; slug: string;
userRole: string;
}; };
creator: { creator: {
id: string; id: string;
@ -37,21 +38,17 @@ export interface ISharedPage extends IShare {
page: IPage; page: IPage;
share: IShare & { share: IShare & {
level: number; level: number;
sharedPage: { id: string; slugId: string; title: string }; sharedPage: { id: string; slugId: string; title: string; icon: string };
}; };
} }
export interface IShareForPage extends IShare { export interface IShareForPage extends IShare {
level: number; level: number;
page: {
id: string;
title: string;
slugId: string;
};
sharedPage: { sharedPage: {
id: string; id: string;
slugId: string; slugId: string;
title: string; title: string;
icon: string;
}; };
} }

View File

@ -3,6 +3,9 @@ import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts"; import { getAppName } from "@/lib/config.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ShareList from "@/features/share/components/share-list.tsx"; import ShareList from "@/features/share/components/share-list.tsx";
import { Alert, Text } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import React from "react";
export default function Shares() { export default function Shares() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -11,10 +14,17 @@ export default function Shares() {
<> <>
<Helmet> <Helmet>
<title> <title>
{t("Shares")} - {getAppName()} {t("Public sharing")} - {getAppName()}
</title> </title>
</Helmet> </Helmet>
<SettingsTitle title={t("Shares")} /> <SettingsTitle title={t("Public sharing")} />
<Alert variant="light" color="blue" icon={<IconInfoCircle />}>
{t(
"Publicly shared pages from spaces you are a member of will appear here",
)}
</Alert>
<ShareList /> <ShareList />
</> </>
); );

View File

@ -22,7 +22,7 @@ export default function SingleSharedPage() {
if (shareId && data) { if (shareId && data) {
if (data.share.key !== shareId) { if (data.share.key !== shareId) {
// affects parent share, what to do? // affects parent share, what to do?
//navigate(`/share/${data.share.key}/${pageSlug}`); navigate(`/share/${data.share.key}/${pageSlug}`);
} }
} }
}, [shareId, data]); }, [shareId, data]);
@ -41,7 +41,8 @@ export default function SingleSharedPage() {
return ( return (
<div> <div>
<Helmet> <Helmet>
<title>{`${data?.page?.icon || ""} ${data?.page?.title || t("untitled")}`}</title> <title>{`${data?.page?.title || t("untitled")}`}</title>
{!data?.share.searchIndexing && <meta name="robots" content="noindex" />}
</Helmet> </Helmet>
<Container size={900}> <Container size={900}>

View File

@ -69,7 +69,7 @@ export class ShareService {
key: generateSlugId(), key: generateSlugId(),
pageId: page.id, pageId: page.id,
includeSubPages: createShareDto.includeSubPages, includeSubPages: createShareDto.includeSubPages,
searchIndexing: true, searchIndexing: createShareDto.searchIndexing,
creatorId: authUserId, creatorId: authUserId,
spaceId: page.spaceId, spaceId: page.spaceId,
workspaceId, workspaceId,
@ -126,6 +126,7 @@ export class ShareService {
'id', 'id',
'slugId', 'slugId',
'pages.title', 'pages.title',
'pages.icon',
'parentPageId', 'parentPageId',
sql`0`.as('level'), sql`0`.as('level'),
]) ])
@ -137,6 +138,7 @@ export class ShareService {
'p.id', 'p.id',
'p.slugId', 'p.slugId',
'p.title', 'p.title',
'p.icon',
'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'),
@ -150,11 +152,13 @@ export class ShareService {
'page_hierarchy.id as sharedPageId', 'page_hierarchy.id as sharedPageId',
'page_hierarchy.slugId as sharedPageSlugId', 'page_hierarchy.slugId as sharedPageSlugId',
'page_hierarchy.title as sharedPageTitle', 'page_hierarchy.title as sharedPageTitle',
'page_hierarchy.icon as sharedPageIcon',
'page_hierarchy.level as level', 'page_hierarchy.level as level',
'shares.id', 'shares.id',
'shares.key', 'shares.key',
'shares.pageId', 'shares.pageId',
'shares.includeSubPages', 'shares.includeSubPages',
'shares.searchIndexing',
'shares.creatorId', 'shares.creatorId',
'shares.spaceId', 'shares.spaceId',
'shares.workspaceId', 'shares.workspaceId',
@ -166,18 +170,19 @@ export class ShareService {
.executeTakeFirst(); .executeTakeFirst();
if (!share || share.workspaceId != workspaceId) { if (!share || share.workspaceId != workspaceId) {
throw new NotFoundException('Shared page not found'); return undefined;
} }
if (share.level === 1 && !share.includeSubPages) { if (share.level === 1 && !share.includeSubPages) {
// we can only show a page if its shared ancestor permits it // we can only show a page if its shared ancestor permits it
throw new NotFoundException('Shared page not found'); return undefined;
} }
return { return {
id: share.id, id: share.id,
key: share.key, key: share.key,
includeSubPages: share.includeSubPages, includeSubPages: share.includeSubPages,
searchIndexing: share.searchIndexing,
pageId: share.pageId, pageId: share.pageId,
creatorId: share.creatorId, creatorId: share.creatorId,
spaceId: share.spaceId, spaceId: share.spaceId,
@ -188,6 +193,7 @@ export class ShareService {
id: share.sharedPageId, id: share.sharedPageId,
slugId: share.sharedPageSlugId, slugId: share.sharedPageSlugId,
title: share.sharedPageTitle, title: share.sharedPageTitle,
icon: share.sharedPageIcon,
}, },
}; };
} }