mirror of
https://github.com/docmost/docmost.git
synced 2025-11-18 05:11:09 +10:00
Share - WIP
This commit is contained in:
@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Security from "@/ee/security/pages/security.tsx";
|
import Security from "@/ee/security/pages/security.tsx";
|
||||||
import License from "@/ee/licence/pages/license.tsx";
|
import License from "@/ee/licence/pages/license.tsx";
|
||||||
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
||||||
|
import SharedPage from '@/pages/share/shared-page.tsx';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -51,6 +52,8 @@ export default function App() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Route path={"/share/:shareId/:pageId"} element={<SharedPage />} />
|
||||||
|
|
||||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||||
|
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export default function DrawioView(props: NodeViewProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selected && (
|
{selected && editor.isEditable && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@ -170,7 +170,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selected && (
|
{selected && editor.isEditable && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
52
apps/client/src/features/editor/readonly-page-editor.tsx
Normal file
52
apps/client/src/features/editor/readonly-page-editor.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import "@/features/editor/styles/index.css";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { EditorProvider } from "@tiptap/react";
|
||||||
|
import { mainExtensions } from "@/features/editor/extensions/extensions";
|
||||||
|
import { Document } from "@tiptap/extension-document";
|
||||||
|
import { Heading } from "@tiptap/extension-heading";
|
||||||
|
import { Text } from "@tiptap/extension-text";
|
||||||
|
import { Placeholder } from "@tiptap/extension-placeholder";
|
||||||
|
|
||||||
|
interface PageEditorProps {
|
||||||
|
title: string;
|
||||||
|
content: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReadonlyPageEditor({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
}: PageEditorProps) {
|
||||||
|
const extensions = useMemo(() => {
|
||||||
|
return [...mainExtensions];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const titleExtensions = [
|
||||||
|
Document.extend({
|
||||||
|
content: "heading",
|
||||||
|
}),
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: "Untitled",
|
||||||
|
showOnlyWhenEditable: false,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EditorProvider
|
||||||
|
editable={false}
|
||||||
|
immediatelyRender={true}
|
||||||
|
extensions={titleExtensions}
|
||||||
|
content={title}
|
||||||
|
></EditorProvider>
|
||||||
|
|
||||||
|
<EditorProvider
|
||||||
|
editable={false}
|
||||||
|
immediatelyRender={true}
|
||||||
|
extensions={extensions}
|
||||||
|
content={content}
|
||||||
|
></EditorProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
|
export const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
|
||||||
const titleSlug = slugify(pageTitle?.substring(0, 70) || "untitled", {
|
const titleSlug = slugify(pageTitle?.substring(0, 70) || "untitled", {
|
||||||
customReplacements: [
|
customReplacements: [
|
||||||
["♥", ""],
|
["♥", ""],
|
||||||
@ -8,7 +8,7 @@ const buildPageSlug = (pageSlugId: string, pageTitle?: string): string => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
return `p/${titleSlug}-${pageSlugId}`;
|
return `${titleSlug}-${pageSlugId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildPageUrl = (
|
export const buildPageUrl = (
|
||||||
@ -17,7 +17,7 @@ export const buildPageUrl = (
|
|||||||
pageTitle?: string,
|
pageTitle?: string,
|
||||||
): string => {
|
): string => {
|
||||||
if (spaceName === undefined) {
|
if (spaceName === undefined) {
|
||||||
return `/${buildPageSlug(pageSlugId, pageTitle)}`;
|
return `/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||||
}
|
}
|
||||||
return `/s/${spaceName}/${buildPageSlug(pageSlugId, pageTitle)}`;
|
return `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
|
||||||
};
|
};
|
||||||
|
|||||||
65
apps/client/src/features/share/queries/share-query.ts
Normal file
65
apps/client/src/features/share/queries/share-query.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { validate as isValidUuid } from "uuid";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
ICreateShare,
|
||||||
|
IShareInput,
|
||||||
|
} from "@/features/share/types/share.types.ts";
|
||||||
|
import {
|
||||||
|
createShare,
|
||||||
|
deleteShare,
|
||||||
|
getShare,
|
||||||
|
updateShare,
|
||||||
|
} from "@/features/share/services/share-service.ts";
|
||||||
|
import { IPage } from "@/features/page/types/page.types.ts";
|
||||||
|
|
||||||
|
export function useShareQuery(
|
||||||
|
shareInput: Partial<IShareInput>,
|
||||||
|
): UseQueryResult<IPage, Error> {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["shares", shareInput],
|
||||||
|
queryFn: () => getShare(shareInput),
|
||||||
|
enabled: !!shareInput.shareId,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateShareMutation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return useMutation<any, Error, ICreateShare>({
|
||||||
|
mutationFn: (data) => createShare(data),
|
||||||
|
onSuccess: (data) => {},
|
||||||
|
onError: (error) => {
|
||||||
|
notifications.show({ message: t("Failed to share page"), color: "red" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateShareMutation() {
|
||||||
|
return useMutation<any, Error, Partial<IShareInput>>({
|
||||||
|
mutationFn: (data) => updateShare(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteShareMutation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (shareId: string) => deleteShare(shareId),
|
||||||
|
onSuccess: () => {
|
||||||
|
notifications.show({ message: t("Share deleted successfully") });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
notifications.show({
|
||||||
|
message: t("Failed to delete share"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
36
apps/client/src/features/share/services/share-service.ts
Normal file
36
apps/client/src/features/share/services/share-service.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import api from "@/lib/api-client";
|
||||||
|
import {
|
||||||
|
IExportPageParams,
|
||||||
|
IMovePage,
|
||||||
|
IMovePageToSpace,
|
||||||
|
IPage,
|
||||||
|
IPageInput,
|
||||||
|
SidebarPagesParams,
|
||||||
|
} from "@/features/page/types/page.types";
|
||||||
|
import { IAttachment, IPagination } from "@/lib/types.ts";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import {
|
||||||
|
ICreateShare,
|
||||||
|
IShareInput,
|
||||||
|
} from "@/features/share/types/share.types.ts";
|
||||||
|
|
||||||
|
export async function createShare(data: ICreateShare): Promise<any> {
|
||||||
|
const req = await api.post<any>("/shares/create", data);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getShare(
|
||||||
|
shareInput: Partial<IShareInput>,
|
||||||
|
): Promise<IPage> {
|
||||||
|
const req = await api.post<IPage>("/shares/info", shareInput);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateShare(data: Partial<IShareInput>): Promise<any> {
|
||||||
|
const req = await api.post<IPage>("/shares/update", data);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteShare(shareId: string): Promise<void> {
|
||||||
|
await api.post("/shares/delete", { shareId });
|
||||||
|
}
|
||||||
12
apps/client/src/features/share/types/share.types.ts
Normal file
12
apps/client/src/features/share/types/share.types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
export interface ICreateShare {
|
||||||
|
slugId: string;
|
||||||
|
pageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface IShareInput {
|
||||||
|
shareId: string;
|
||||||
|
pageId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
56
apps/client/src/pages/share/shared-page.tsx
Normal file
56
apps/client/src/pages/share/shared-page.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useShareQuery } from "@/features/share/queries/share-query.ts";
|
||||||
|
import { Container } from "@mantine/core";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
|
||||||
|
import { buildPageSlug } from "@/features/page/page.utils.ts";
|
||||||
|
|
||||||
|
export default function SharedPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { shareId } = useParams();
|
||||||
|
const {
|
||||||
|
data: page,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useShareQuery({ shareId: shareId });
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!page) return;
|
||||||
|
const pageSlug = buildPageSlug(page.slugId, page.title);
|
||||||
|
const shareSlug = `/share/${shareId}/${pageSlug}`;
|
||||||
|
navigate(shareSlug, { replace: true });
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !page) {
|
||||||
|
if ([401, 403, 404].includes(error?.["status"])) {
|
||||||
|
return <div>{t("Page not found")}</div>;
|
||||||
|
}
|
||||||
|
return <div>{t("Error fetching page data.")}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
page && (
|
||||||
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Container size={900} pt={50}>
|
||||||
|
<ReadonlyPageEditor
|
||||||
|
key={page.id}
|
||||||
|
title={page.title}
|
||||||
|
content={page.content}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import { SpaceModule } from './space/space.module';
|
|||||||
import { GroupModule } from './group/group.module';
|
import { GroupModule } from './group/group.module';
|
||||||
import { CaslModule } from './casl/casl.module';
|
import { CaslModule } from './casl/casl.module';
|
||||||
import { DomainMiddleware } from '../common/middlewares/domain.middleware';
|
import { DomainMiddleware } from '../common/middlewares/domain.middleware';
|
||||||
|
import { ShareModule } from './share/share.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -28,6 +29,7 @@ import { DomainMiddleware } from '../common/middlewares/domain.middleware';
|
|||||||
SpaceModule,
|
SpaceModule,
|
||||||
GroupModule,
|
GroupModule,
|
||||||
CaslModule,
|
CaslModule,
|
||||||
|
ShareModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule implements NestModule {
|
export class CoreModule implements NestModule {
|
||||||
|
|||||||
6
apps/server/src/core/share/dto/create-share.dto.ts
Normal file
6
apps/server/src/core/share/dto/create-share.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateShareDto {
|
||||||
|
@IsString()
|
||||||
|
pageId: string;
|
||||||
|
}
|
||||||
28
apps/server/src/core/share/dto/share.dto.ts
Normal file
28
apps/server/src/core/share/dto/share.dto.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class ShareIdDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
shareId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceIdDto {
|
||||||
|
@IsUUID()
|
||||||
|
spaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShareInfoDto extends ShareIdDto {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
pageId: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsBoolean()
|
||||||
|
// includeContent: boolean;
|
||||||
|
}
|
||||||
8
apps/server/src/core/share/dto/update-page.dto.ts
Normal file
8
apps/server/src/core/share/dto/update-page.dto.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateShareDto } from './create-share.dto';
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateShareDto extends PartialType(CreateShareDto) {
|
||||||
|
//@IsString()
|
||||||
|
//pageId: string;
|
||||||
|
}
|
||||||
105
apps/server/src/core/share/share.controller.ts
Normal file
105
apps/server/src/core/share/share.controller.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
ForbiddenException,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
NotFoundException,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
import {
|
||||||
|
SpaceCaslAction,
|
||||||
|
SpaceCaslSubject,
|
||||||
|
} from '../casl/interfaces/space-ability.type';
|
||||||
|
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 } 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';
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Controller('shares')
|
||||||
|
export class ShareController {
|
||||||
|
constructor(
|
||||||
|
private readonly shareService: ShareService,
|
||||||
|
private readonly spaceAbility: SpaceAbilityFactory,
|
||||||
|
private readonly pageRepo: PageRepo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('/info')
|
||||||
|
async getPage(@Body() dto: ShareInfoDto) {
|
||||||
|
return this.shareService.getShare(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('create')
|
||||||
|
async create(
|
||||||
|
@Body() createShareDto: CreateShareDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
|
||||||
|
const page = await this.pageRepo.findById(createShareDto.pageId);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new NotFoundException('Page not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
||||||
|
if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Page)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.shareService.createShare({
|
||||||
|
pageId: page.id,
|
||||||
|
authUserId: user.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('update')
|
||||||
|
async update(@Body() updatePageDto: UpdateShareDto, @AuthUser() user: User) {
|
||||||
|
/* const page = await this.pageRepo.findById(updatePageDto.pageId);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new NotFoundException('Page not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
||||||
|
if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
//return this.shareService.update(page, updatePageDto, user.id);
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('delete')
|
||||||
|
async delete(@Body() shareIdDto: ShareIdDto, @AuthUser() user: User) {
|
||||||
|
/* const page = await this.pageRepo.findById(pageIdDto.pageId);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new NotFoundException('Page not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
||||||
|
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
// await this.shareService.forceDelete(pageIdDto.pageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/server/src/core/share/share.module.ts
Normal file
10
apps/server/src/core/share/share.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ShareController } from './share.controller';
|
||||||
|
import { ShareService } from './share.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ShareController],
|
||||||
|
providers: [ShareService],
|
||||||
|
exports: [ShareService],
|
||||||
|
})
|
||||||
|
export class ShareModule {}
|
||||||
65
apps/server/src/core/share/share.service.ts
Normal file
65
apps/server/src/core/share/share.service.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ShareInfoDto } from './dto/share.dto';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
|
import { generateSlugId } from '../../common/helpers';
|
||||||
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShareService {
|
||||||
|
constructor(
|
||||||
|
private readonly pageRepo: PageRepo,
|
||||||
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createShare(opts: {
|
||||||
|
authUserId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
pageId: string;
|
||||||
|
}) {
|
||||||
|
const { authUserId, workspaceId, pageId } = opts;
|
||||||
|
|
||||||
|
const slugId = generateSlugId(); // or custom slug
|
||||||
|
const share = this.db
|
||||||
|
.insertInto('shares')
|
||||||
|
.values({ slugId: slugId, pageId, creatorId: authUserId, workspaceId })
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return share;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShare(dto: ShareInfoDto) {
|
||||||
|
// for now only single page share
|
||||||
|
|
||||||
|
// if only share Id is provided, return
|
||||||
|
|
||||||
|
// if share id is pass with page id, what to do?
|
||||||
|
// if uuid is used, use Id
|
||||||
|
const share = await this.db
|
||||||
|
.selectFrom('shares')
|
||||||
|
.selectAll()
|
||||||
|
.where('slugId', '=', dto.shareId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!share) {
|
||||||
|
throw new NotFoundException('Share not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await this.pageRepo.findById(share.pageId, {
|
||||||
|
includeContent: true,
|
||||||
|
includeCreator: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// cleanup json content
|
||||||
|
// remove comments mark
|
||||||
|
// make sure attachments work (videos, images, excalidraw, drawio)
|
||||||
|
// figure out internal links?
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new NotFoundException('Page not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable('shares')
|
||||||
|
.addColumn('id', 'uuid', (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
||||||
|
)
|
||||||
|
.addColumn('slug_id', 'varchar', (col) => col.notNull())
|
||||||
|
.addColumn('page_id', 'varchar', (col) => col.notNull())
|
||||||
|
.addColumn('include_sub_pages', 'varchar', (col) => col)
|
||||||
|
.addColumn('creator_id', 'uuid', (col) => col.references('users.id'))
|
||||||
|
|
||||||
|
// pageSlug
|
||||||
|
|
||||||
|
//.addColumn('space_id', 'uuid', (col) =>
|
||||||
|
// col.references('spaces.id').onDelete('cascade').notNull(),
|
||||||
|
// )
|
||||||
|
.addColumn('workspace_id', 'uuid', (col) =>
|
||||||
|
col.references('workspaces.id').onDelete('cascade').notNull(),
|
||||||
|
)
|
||||||
|
.addColumn('created_at', 'timestamptz', (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn('updated_at', 'timestamptz', (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn('deleted_at', 'timestamptz', (col) => col)
|
||||||
|
.addUniqueConstraint('shares_slug_id_unique', ['slug_id'])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('shares_slug_id_idx')
|
||||||
|
.on('shares')
|
||||||
|
.column('slug_id')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable('shares').execute();
|
||||||
|
}
|
||||||
13
apps/server/src/database/types/db.d.ts
vendored
13
apps/server/src/database/types/db.d.ts
vendored
@ -183,6 +183,18 @@ export interface Pages {
|
|||||||
ydoc: Buffer | null;
|
ydoc: Buffer | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Shares {
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
creatorId: string | null;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
id: Generated<string>;
|
||||||
|
includeSubPages: string | null;
|
||||||
|
pageId: string;
|
||||||
|
slugId: string;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpaceMembers {
|
export interface SpaceMembers {
|
||||||
addedById: string | null;
|
addedById: string | null;
|
||||||
createdAt: Generated<Timestamp>;
|
createdAt: Generated<Timestamp>;
|
||||||
@ -288,6 +300,7 @@ export interface DB {
|
|||||||
groupUsers: GroupUsers;
|
groupUsers: GroupUsers;
|
||||||
pageHistory: PageHistory;
|
pageHistory: PageHistory;
|
||||||
pages: Pages;
|
pages: Pages;
|
||||||
|
shares: Shares;
|
||||||
spaceMembers: SpaceMembers;
|
spaceMembers: SpaceMembers;
|
||||||
spaces: Spaces;
|
spaces: Spaces;
|
||||||
users: Users;
|
users: Users;
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
Billing as BillingSubscription,
|
Billing as BillingSubscription,
|
||||||
AuthProviders,
|
AuthProviders,
|
||||||
AuthAccounts,
|
AuthAccounts,
|
||||||
|
Shares,
|
||||||
} from './db';
|
} from './db';
|
||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
@ -101,3 +102,8 @@ export type UpdatableAuthProvider = Updateable<Omit<AuthProviders, 'id'>>;
|
|||||||
export type AuthAccount = Selectable<AuthAccounts>;
|
export type AuthAccount = Selectable<AuthAccounts>;
|
||||||
export type InsertableAuthAccount = Insertable<AuthAccounts>;
|
export type InsertableAuthAccount = Insertable<AuthAccounts>;
|
||||||
export type UpdatableAuthAccount = Updateable<Omit<AuthAccounts, 'id'>>;
|
export type UpdatableAuthAccount = Updateable<Omit<AuthAccounts, 'id'>>;
|
||||||
|
|
||||||
|
// Share
|
||||||
|
export type Share = Selectable<Shares>;
|
||||||
|
export type InsertableShare = Insertable<Shares>;
|
||||||
|
export type UpdatableShare = Updateable<Omit<Shares, 'id'>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user