diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index fde67ecf..0746ed15 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -380,5 +380,8 @@ "Delete public share link": "Delete public share link", "Delete share": "Delete share", "Are you sure you want to delete this shared link?": "Are you sure you want to delete this shared 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" + "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", + "Share deleted successfully": "Share deleted successfully", + "Share not found": "Share not found", + "Failed to share page": "Failed to share page" } diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 6bc5a778..cff59009 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -29,6 +29,7 @@ import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-selec import SharedPage from "@/pages/share/shared-page.tsx"; import Shares from "@/pages/settings/shares/shares.tsx"; import ShareLayout from "@/features/share/components/share-layout.tsx"; +import ShareRedirect from '@/pages/share/share-redirect.tsx'; export default function App() { const { t } = useTranslation(); @@ -58,7 +59,8 @@ export default function App() { } /> } /> - + + } /> } /> }> diff --git a/apps/client/src/features/share/queries/share-query.ts b/apps/client/src/features/share/queries/share-query.ts index 194d50ea..dea047bf 100644 --- a/apps/client/src/features/share/queries/share-query.ts +++ b/apps/client/src/features/share/queries/share-query.ts @@ -9,6 +9,7 @@ import { notifications } from "@mantine/notifications"; import { useTranslation } from "react-i18next"; import { ICreateShare, + IShare, ISharedItem, ISharedPage, ISharedPageTree, @@ -22,6 +23,7 @@ import { getSharedPageTree, getShareForPage, getShareInfo, + getSharePageInfo, getShares, updateShare, } from "@/features/share/services/share-service.ts"; @@ -39,12 +41,24 @@ export function useGetSharesQuery( }); } -export function useShareQuery( +export function useGetShareByIdQuery( + shareId: string, +): UseQueryResult { + const query = useQuery({ + queryKey: ["share-by-id", shareId], + queryFn: () => getShareInfo(shareId), + enabled: !!shareId, + }); + + return query; +} + +export function useSharePageQuery( shareInput: Partial, ): UseQueryResult { const query = useQuery({ queryKey: ["shares", shareInput], - queryFn: () => getShareInfo(shareInput), + queryFn: () => getSharePageInfo(shareInput), enabled: !!shareInput.pageId, }); @@ -84,7 +98,9 @@ export function useCreateShareMutation() { } export function useUpdateShareMutation() { + const { t } = useTranslation(); const queryClient = useQueryClient(); + return useMutation({ mutationFn: (data) => updateShare(data), onSuccess: (data) => { @@ -99,10 +115,16 @@ export function useUpdateShareMutation() { predicate: (item) => ["share-for-page"].includes(item.queryKey[0] as string), }); + + notifications.show({ + message: t("Share not found"), + color: "red", + }); + return; } notifications.show({ - message: error?.["response"]?.data?.message || "Share share not found", + message: error?.["response"]?.data?.message || "Share not found", color: "red", }); }, diff --git a/apps/client/src/features/share/services/share-service.ts b/apps/client/src/features/share/services/share-service.ts index e810f198..2f43ba20 100644 --- a/apps/client/src/features/share/services/share-service.ts +++ b/apps/client/src/features/share/services/share-service.ts @@ -3,12 +3,14 @@ import { IPage } from "@/features/page/types/page.types"; import { ICreateShare, - ISharedItem, ISharedPage, + IShare, + ISharedItem, + ISharedPage, ISharedPageTree, IShareForPage, IShareInfoInput, IUpdateShare, -} from '@/features/share/types/share.types.ts'; +} from "@/features/share/types/share.types.ts"; import { IPagination, QueryParams } from "@/lib/types.ts"; export async function getShares( @@ -23,6 +25,11 @@ export async function createShare(data: ICreateShare): Promise { return req.data; } +export async function getShareInfo(shareId: string): Promise { + const req = await api.post("/shares/info", { shareId }); + return req.data; +} + export async function updateShare(data: IUpdateShare): Promise { const req = await api.post("/shares/update", data); return req.data; @@ -33,7 +40,7 @@ export async function getShareForPage(pageId: string): Promise { return req.data; } -export async function getShareInfo( +export async function getSharePageInfo( shareInput: Partial, ): Promise { const req = await api.post("/shares/page-info", shareInput); diff --git a/apps/client/src/features/share/types/share.types.ts b/apps/client/src/features/share/types/share.types.ts index 8163fa06..c40801e8 100644 --- a/apps/client/src/features/share/types/share.types.ts +++ b/apps/client/src/features/share/types/share.types.ts @@ -12,6 +12,7 @@ export interface IShare { createdAt: string; updatedAt: string; deletedAt: string | null; + sharedPage?: ISharePage; } export interface ISharedItem extends IShare { @@ -44,12 +45,14 @@ export interface ISharedPage extends IShare { export interface IShareForPage extends IShare { level: number; - sharedPage: { - id: string; - slugId: string; - title: string; - icon: string; - }; + sharedPage: ISharePage; +} + +interface ISharePage { + id: string; + slugId: string; + title: string; + icon: string; } export interface ICreateShare { diff --git a/apps/client/src/pages/share/share-redirect.tsx b/apps/client/src/pages/share/share-redirect.tsx new file mode 100644 index 00000000..5653e83f --- /dev/null +++ b/apps/client/src/pages/share/share-redirect.tsx @@ -0,0 +1,35 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect } from "react"; +import { buildSharedPageUrl } from "@/features/page/page.utils.ts"; +import { Error404 } from "@/components/ui/error-404.tsx"; +import { useGetShareByIdQuery } from "@/features/share/queries/share-query.ts"; + +export default function ShareRedirect() { + const { shareId } = useParams(); + const navigate = useNavigate(); + + const { data: share, isLoading, isError } = useGetShareByIdQuery(shareId); + + useEffect(() => { + if (share) { + navigate( + buildSharedPageUrl({ + shareId: share.key, + pageSlugId: share?.sharedPage.slugId, + pageTitle: share?.sharedPage.title, + }), + { replace: true }, + ); + } + }, [isLoading, share]); + + if (isError) { + return ; + } + + if (isLoading) { + return <>; + } + + return null; +} diff --git a/apps/client/src/pages/share/shared-page.tsx b/apps/client/src/pages/share/shared-page.tsx index 93493475..8cd69836 100644 --- a/apps/client/src/pages/share/shared-page.tsx +++ b/apps/client/src/pages/share/shared-page.tsx @@ -1,7 +1,7 @@ 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 { useSharePageQuery } from "@/features/share/queries/share-query.ts"; import { Container } from "@mantine/core"; import React, { useEffect } from "react"; import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx"; @@ -14,7 +14,7 @@ export default function SingleSharedPage() { const { shareId } = useParams(); const navigate = useNavigate(); - const { data, isLoading, isError, error } = useShareQuery({ + const { data, isLoading, isError, error } = useSharePageQuery({ pageId: extractPageSlugId(pageSlug), }); diff --git a/apps/server/src/core/share/share.controller.ts b/apps/server/src/core/share/share.controller.ts index 11fef67e..6ea13799 100644 --- a/apps/server/src/core/share/share.controller.ts +++ b/apps/server/src/core/share/share.controller.ts @@ -67,7 +67,9 @@ export class ShareController { @HttpCode(HttpStatus.OK) @Post('/info') async getShare(@Body() dto: ShareIdDto, @AuthUser() user: User) { - const share = await this.shareRepo.findById(dto.shareId); + const share = await this.shareRepo.findById(dto.shareId, { + includeSharedPage: true, + }); if (!share) { throw new NotFoundException('Share not found'); diff --git a/apps/server/src/database/repos/share/share.repo.ts b/apps/server/src/database/repos/share/share.repo.ts index 44e8cd33..bcdd7474 100644 --- a/apps/server/src/database/repos/share/share.repo.ts +++ b/apps/server/src/database/repos/share/share.repo.ts @@ -39,6 +39,7 @@ export class ShareRepo { async findById( shareId: string, opts?: { + includeSharedPage?: boolean; includeCreator?: boolean; withLock?: boolean; trx?: KyselyTransaction; @@ -48,6 +49,10 @@ export class ShareRepo { let query = db.selectFrom('shares').select(this.baseFields); + if (opts?.includeSharedPage) { + query = query.select((eb) => this.withSharedPage(eb)); + } + if (opts?.includeCreator) { query = query.select((eb) => this.withCreator(eb)); } @@ -98,7 +103,11 @@ export class ShareRepo { return dbOrTx(this.db, trx) .updateTable('shares') .set({ ...updatableShare, updatedAt: new Date() }) - .where(isValidUUID(shareId) ? 'id' : sql`LOWER(key)`, '=', shareId.toLowerCase()) + .where( + isValidUUID(shareId) ? 'id' : sql`LOWER(key)`, + '=', + shareId.toLowerCase(), + ) .returning(this.baseFields) .executeTakeFirst(); } @@ -215,4 +224,19 @@ export class ShareRepo { .whereRef('users.id', '=', 'shares.creatorId'), ).as('creator'); } + + withSharedPage(eb: ExpressionBuilder) { + return jsonObjectFrom( + eb + .selectFrom('pages') + .select([ + 'pages.id', + 'pages.slugId', + 'pages.title', + 'pages.icon', + 'pages.parentPageId', + ]) + .whereRef('pages.id', '=', 'shares.pageId'), + ).as('sharedPage'); + } }