* fix comments
* fix page history
* fix aside width on smaller screens
This commit is contained in:
Philipinho
2024-04-23 22:07:00 +01:00
parent 2af1fe3c40
commit b91c3ede1e
15 changed files with 611 additions and 464 deletions

View File

@ -24,6 +24,12 @@
} }
} }
@media (max-width: 48em) {
.aside {
width: 350px;
}
}
@media (max-width: 48em) { @media (max-width: 48em) {
.navbar { .navbar {
width: 300px; width: 300px;

View File

@ -33,8 +33,8 @@ export default function Shell({ children }: { children: React.ReactNode }) {
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened }, collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}} }}
aside={{ aside={{
width: 300, width: 350,
breakpoint: "md", breakpoint: "sm",
collapsed: { mobile: !isAsideOpen, desktop: !isAsideOpen }, collapsed: { mobile: !isAsideOpen, desktop: !isAsideOpen },
}} }}
padding="md" padding="md"

View File

@ -57,8 +57,8 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
await createCommentMutation.mutateAsync(commentData); await createCommentMutation.mutateAsync(commentData);
editor editor
.chain() .chain()
.setContent(createdComment.id)
// @ts-ignore // @ts-ignore
.setComment(createdComment.id)
.unsetCommentDecoration() .unsetCommentDecoration()
.run(); .run();
setActiveCommentId(createdComment.id); setActiveCommentId(createdComment.id);
@ -75,7 +75,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
} }
}; };
const handleCommentEditorChange = (newContent) => { const handleCommentEditorChange = (newContent: any) => {
setComment(newContent); setComment(newContent);
}; };
@ -93,7 +93,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
> >
<Stack gap={2}> <Stack gap={2}>
<Group> <Group>
<Avatar size="sm" c="blue"> <Avatar size="sm" color="blue">
{currentUser.user.name.charAt(0)} {currentUser.user.name.charAt(0)}
</Avatar> </Avatar>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>

View File

@ -1,16 +1,24 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from "react";
import { useParams } from 'react-router-dom'; import { useParams } from "react-router-dom";
import { Divider, Paper } from '@mantine/core'; import { Divider, Paper } from "@mantine/core";
import CommentListItem from '@/features/comment/components/comment-list-item'; import CommentListItem from "@/features/comment/components/comment-list-item";
import { useCommentsQuery, useCreateCommentMutation } from '@/features/comment/queries/comment-query'; import {
useCommentsQuery,
useCreateCommentMutation,
} from "@/features/comment/queries/comment-query";
import CommentEditor from '@/features/comment/components/comment-editor'; import CommentEditor from "@/features/comment/components/comment-editor";
import CommentActions from '@/features/comment/components/comment-actions'; import CommentActions from "@/features/comment/components/comment-actions";
import { useFocusWithin } from '@mantine/hooks'; import { useFocusWithin } from "@mantine/hooks";
import { IComment } from "@/features/comment/types/comment.types.ts";
function CommentList() { function CommentList() {
const { pageId } = useParams(); const { pageId } = useParams();
const { data: comments, isLoading: isCommentsLoading, isError } = useCommentsQuery(pageId); const {
data: comments,
isLoading: isCommentsLoading,
isError,
} = useCommentsQuery(pageId);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const createCommentMutation = useCreateCommentMutation(); const createCommentMutation = useCreateCommentMutation();
@ -22,12 +30,12 @@ function CommentList() {
return <div>Error loading comments.</div>; return <div>Error loading comments.</div>;
} }
if (!comments || comments.length === 0) { if (!comments || comments.items.length === 0) {
return <>No comments yet.</>; return <>No comments yet.</>;
} }
const renderComments = (comment) => { const renderComments = (comment: IComment) => {
const handleAddReply = async (commentId, content) => { const handleAddReply = async (commentId: string, content: string) => {
try { try {
setIsLoading(true); setIsLoading(true);
const commentData = { const commentData = {
@ -38,14 +46,22 @@ function CommentList() {
await createCommentMutation.mutateAsync(commentData); await createCommentMutation.mutateAsync(commentData);
} catch (error) { } catch (error) {
console.error('Failed to post comment:', error); console.error("Failed to post comment:", error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<Paper shadow="sm" radius="md" p="sm" mb="sm" withBorder key={comment.id} data-comment-id={comment.id}> <Paper
shadow="sm"
radius="md"
p="sm"
mb="sm"
withBorder
key={comment.id}
data-comment-id={comment.id}
>
<div> <div>
<CommentListItem comment={comment} /> <CommentListItem comment={comment} />
<ChildComments comments={comments} parentId={comment.id} /> <ChildComments comments={comments} parentId={comment.id} />
@ -53,26 +69,34 @@ function CommentList() {
<Divider my={4} /> <Divider my={4} />
<CommentEditorWithActions commentId={comment.id} onSave={handleAddReply} isLoading={isLoading} /> <CommentEditorWithActions
commentId={comment.id}
onSave={handleAddReply}
isLoading={isLoading}
/>
</Paper> </Paper>
); );
}; };
return ( return (
<> <>
{comments.filter(comment => comment.parentCommentId === null).map(renderComments)} {comments.items
.filter((comment) => comment.parentCommentId === null)
.map(renderComments)}
</> </>
); );
} }
const ChildComments = ({ comments, parentId }) => { const ChildComments = ({ comments, parentId }) => {
const getChildComments = (parentId) => { const getChildComments = (parentId: string) => {
return comments.filter(comment => comment.parentCommentId === parentId); return comments.items.filter(
(comment: IComment) => comment.parentCommentId === parentId,
);
}; };
return ( return (
<div> <div>
{getChildComments(parentId).map(childComment => ( {getChildComments(parentId).map((childComment) => (
<div key={childComment.id}> <div key={childComment.id}>
<CommentListItem comment={childComment} /> <CommentListItem comment={childComment} />
<ChildComments comments={comments} parentId={childComment.id} /> <ChildComments comments={comments} parentId={childComment.id} />
@ -83,23 +107,26 @@ const ChildComments = ({ comments, parentId }) => {
}; };
const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => {
const [content, setContent] = useState(''); const [content, setContent] = useState("");
const { ref, focused } = useFocusWithin(); const { ref, focused } = useFocusWithin();
const commentEditorRef = useRef(null); const commentEditorRef = useRef(null);
const handleSave = () => { const handleSave = () => {
onSave(commentId, content); onSave(commentId, content);
setContent(''); setContent("");
commentEditorRef.current?.clearContent(); commentEditorRef.current?.clearContent();
}; };
return ( return (
<div ref={ref}> <div ref={ref}>
<CommentEditor ref={commentEditorRef} onUpdate={setContent} editable={true} /> <CommentEditor
ref={commentEditorRef}
onUpdate={setContent}
editable={true}
/>
{focused && <CommentActions onSave={handleSave} isLoading={isLoading} />} {focused && <CommentActions onSave={handleSave} isLoading={isLoading} />}
</div> </div>
); );
}; };
export default CommentList; export default CommentList;

View File

@ -1,16 +1,28 @@
import { useMutation, useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query'; import {
useMutation,
useQuery,
useQueryClient,
UseQueryResult,
} from "@tanstack/react-query";
import { import {
createComment, createComment,
deleteComment, getPageComments, deleteComment,
getPageComments,
resolveComment, resolveComment,
updateComment, updateComment,
} from '@/features/comment/services/comment-service'; } from "@/features/comment/services/comment-service";
import { IComment, IResolveComment } from '@/features/comment/types/comment.types'; import {
import { notifications } from '@mantine/notifications'; IComment,
IResolveComment,
} from "@/features/comment/types/comment.types";
import { notifications } from "@mantine/notifications";
import { IPagination } from "@/lib/types.ts";
export const RQ_KEY = (pageId: string) => ['comments', pageId]; export const RQ_KEY = (pageId: string) => ["comments", pageId];
export function useCommentsQuery(pageId: string): UseQueryResult<IComment[], Error> { export function useCommentsQuery(
pageId: string,
): UseQueryResult<IPagination<IComment>, Error> {
return useQuery({ return useQuery({
queryKey: RQ_KEY(pageId), queryKey: RQ_KEY(pageId),
queryFn: () => getPageComments(pageId), queryFn: () => getPageComments(pageId),
@ -27,14 +39,14 @@ export function useCreateCommentMutation() {
const newComment = data; const newComment = data;
let comments = queryClient.getQueryData(RQ_KEY(data.pageId)); let comments = queryClient.getQueryData(RQ_KEY(data.pageId));
if (comments) { if (comments) {
comments = prevComments => [...prevComments, newComment]; // comments = prevComments => [...prevComments, newComment];
queryClient.setQueryData(RQ_KEY(data.pageId), comments); //queryClient.setQueryData(RQ_KEY(data.pageId), comments);
} }
notifications.show({ message: 'Comment created successfully' }); notifications.show({ message: "Comment created successfully" });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ message: 'Error creating comment', color: 'red' }); notifications.show({ message: "Error creating comment", color: "red" });
}, },
}); });
} }
@ -43,10 +55,10 @@ export function useUpdateCommentMutation() {
return useMutation<IComment, Error, Partial<IComment>>({ return useMutation<IComment, Error, Partial<IComment>>({
mutationFn: (data) => updateComment(data), mutationFn: (data) => updateComment(data),
onSuccess: (data) => { onSuccess: (data) => {
notifications.show({ message: 'Comment updated successfully' }); notifications.show({ message: "Comment updated successfully" });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ message: 'Failed to update comment', color: 'red' }); notifications.show({ message: "Failed to update comment", color: "red" });
}, },
}); });
} }
@ -59,13 +71,13 @@ export function useDeleteCommentMutation(pageId?: string) {
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
let comments = queryClient.getQueryData(RQ_KEY(pageId)) as IComment[]; let comments = queryClient.getQueryData(RQ_KEY(pageId)) as IComment[];
if (comments) { if (comments) {
comments = comments.filter(comment => comment.id !== variables); // comments = comments.filter(comment => comment.id !== variables);
queryClient.setQueryData(RQ_KEY(pageId), comments); // queryClient.setQueryData(RQ_KEY(pageId), comments);
} }
notifications.show({ message: 'Comment deleted successfully' }); notifications.show({ message: "Comment deleted successfully" });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ message: 'Failed to delete comment', color: 'red' }); notifications.show({ message: "Failed to delete comment", color: "red" });
}, },
}); });
} }
@ -76,21 +88,26 @@ export function useResolveCommentMutation() {
return useMutation({ return useMutation({
mutationFn: (data: IResolveComment) => resolveComment(data), mutationFn: (data: IResolveComment) => resolveComment(data),
onSuccess: (data: IComment, variables) => { onSuccess: (data: IComment, variables) => {
const currentComments = queryClient.getQueryData(
const currentComments = queryClient.getQueryData(RQ_KEY(data.pageId)) as IComment[]; RQ_KEY(data.pageId),
) as IComment[];
if (currentComments) { if (currentComments) {
const updatedComments = currentComments.map((comment) => const updatedComments = currentComments.map((comment) =>
comment.id === variables.commentId ? { ...comment, ...data } : comment, comment.id === variables.commentId
? { ...comment, ...data }
: comment,
); );
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments); queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
} }
notifications.show({ message: 'Comment resolved successfully' }); notifications.show({ message: "Comment resolved successfully" });
}, },
onError: (error) => { onError: (error) => {
notifications.show({ message: 'Failed to resolve comment', color: 'red' }); notifications.show({
message: "Failed to resolve comment",
color: "red",
});
}, },
}); });
} }

View File

@ -1,8 +1,14 @@
import api from '@/lib/api-client'; import api from "@/lib/api-client";
import { IComment, IResolveComment } from '@/features/comment/types/comment.types'; import {
IComment,
IResolveComment,
} from "@/features/comment/types/comment.types";
import { IPagination } from "@/lib/types.ts";
export async function createComment(data: Partial<IComment>): Promise<IComment> { export async function createComment(
const req = await api.post<IComment>('/comments/create', data); data: Partial<IComment>,
): Promise<IComment> {
const req = await api.post<IComment>("/comments/create", data);
return req.data as IComment; return req.data as IComment;
} }
@ -11,21 +17,25 @@ export async function resolveComment(data: IResolveComment): Promise<IComment> {
return req.data as IComment; return req.data as IComment;
} }
export async function updateComment(data: Partial<IComment>): Promise<IComment> { export async function updateComment(
data: Partial<IComment>,
): Promise<IComment> {
const req = await api.post<IComment>(`/comments/update`, data); const req = await api.post<IComment>(`/comments/update`, data);
return req.data as IComment; return req.data as IComment;
} }
export async function getCommentById(id: string): Promise<IComment> { export async function getCommentById(commentId: string): Promise<IComment> {
const req = await api.post<IComment>('/comments/view', { id }); const req = await api.post<IComment>("/comments/info", { commentId });
return req.data as IComment; return req.data as IComment;
} }
export async function getPageComments(pageId: string): Promise<IComment[]> { export async function getPageComments(
const req = await api.post<IComment[]>('/comments', { pageId }); pageId: string,
return req.data as IComment[]; ): Promise<IPagination<IComment>> {
const req = await api.post("/comments", { pageId });
return req.data;
} }
export async function deleteComment(id: string): Promise<void> { export async function deleteComment(commentId: string): Promise<void> {
await api.post('/comments/delete', { id }); await api.post("/comments/delete", { commentId });
} }

View File

@ -1,50 +1,76 @@
import { usePageHistoryListQuery, usePageHistoryQuery } from '@/features/page-history/queries/page-history-query'; import {
import { useParams } from 'react-router-dom'; usePageHistoryListQuery,
import HistoryItem from '@/features/page-history/components/history-item'; usePageHistoryQuery,
import { activeHistoryIdAtom, historyAtoms } from '@/features/page-history/atoms/history-atoms'; } from "@/features/page-history/queries/page-history-query";
import { useAtom } from 'jotai'; import { useParams } from "react-router-dom";
import { useCallback, useEffect } from 'react'; import HistoryItem from "@/features/page-history/components/history-item";
import { Button, ScrollArea, Group, Divider, Text } from '@mantine/core'; import {
import { pageEditorAtom, titleEditorAtom } from '@/features/editor/atoms/editor-atoms'; activeHistoryIdAtom,
import { modals } from '@mantine/modals'; historyAtoms,
import { notifications } from '@mantine/notifications'; } from "@/features/page-history/atoms/history-atoms";
import { useAtom } from "jotai";
import { useCallback, useEffect } from "react";
import { Button, ScrollArea, Group, Divider, Text } from "@mantine/core";
import {
pageEditorAtom,
titleEditorAtom,
} from "@/features/editor/atoms/editor-atoms";
import { modals } from "@mantine/modals";
import { notifications } from "@mantine/notifications";
function HistoryList() { function HistoryList() {
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom); const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
const { pageId } = useParams(); const { pageId } = useParams();
const { data, isLoading, isError } = usePageHistoryListQuery(pageId); const {
data: pageHistoryList,
isLoading,
isError,
} = usePageHistoryListQuery(pageId);
const { data: activeHistoryData } = usePageHistoryQuery(activeHistoryId); const { data: activeHistoryData } = usePageHistoryQuery(activeHistoryId);
const [mainEditor] = useAtom(pageEditorAtom); const [mainEditor] = useAtom(pageEditorAtom);
const [mainEditorTitle] = useAtom(titleEditorAtom); const [mainEditorTitle] = useAtom(titleEditorAtom);
const [, setHistoryModalOpen] = useAtom(historyAtoms); const [, setHistoryModalOpen] = useAtom(historyAtoms);
const confirmModal = () => modals.openConfirmModal({ const confirmModal = () =>
title: 'Please confirm your action', modals.openConfirmModal({
title: "Please confirm your action",
children: ( children: (
<Text size="sm"> <Text size="sm">
Are you sure you want to restore this version? Any changes not versioned will be lost. Are you sure you want to restore this version? Any changes not
versioned will be lost.
</Text> </Text>
), ),
labels: { confirm: 'Confirm', cancel: 'Cancel' }, labels: { confirm: "Confirm", cancel: "Cancel" },
onConfirm: handleRestore, onConfirm: handleRestore,
}); });
const handleRestore = useCallback(() => { const handleRestore = useCallback(() => {
if (activeHistoryData) { if (activeHistoryData) {
mainEditorTitle.chain().clearContent().setContent(activeHistoryData.title, true).run(); mainEditorTitle
mainEditor.chain().clearContent().setContent(activeHistoryData.content).run(); .chain()
.clearContent()
.setContent(activeHistoryData.title, true)
.run();
mainEditor
.chain()
.clearContent()
.setContent(activeHistoryData.content)
.run();
setHistoryModalOpen(false); setHistoryModalOpen(false);
notifications.show({ message: 'Successfully restored' }); notifications.show({ message: "Successfully restored" });
} }
}, [activeHistoryData]); }, [activeHistoryData]);
useEffect(() => { useEffect(() => {
if (data && data.length > 0 && !activeHistoryId) { if (
setActiveHistoryId(data[0].id); pageHistoryList &&
pageHistoryList.items.length > 0 &&
!activeHistoryId
) {
setActiveHistoryId(pageHistoryList.items[0].id);
} }
}, [data]); }, [pageHistoryList]);
if (isLoading) { if (isLoading) {
return <></>; return <></>;
@ -54,14 +80,15 @@ function HistoryList() {
return <div>Error loading page history.</div>; return <div>Error loading page history.</div>;
} }
if (!data || data.length === 0) { if (!pageHistoryList || pageHistoryList.items.length === 0) {
return <>No page history saved yet.</>; return <>No page history saved yet.</>;
} }
return ( return (
<div> <div>
<ScrollArea h={620} w="100%" type="scroll" scrollbarSize={5}> <ScrollArea h={620} w="100%" type="scroll" scrollbarSize={5}>
{data && data.map((historyItem, index) => ( {pageHistoryList &&
pageHistoryList.items.map((historyItem, index) => (
<HistoryItem <HistoryItem
key={index} key={index}
historyItem={historyItem} historyItem={historyItem}
@ -74,11 +101,18 @@ function HistoryList() {
<Divider /> <Divider />
<Group p="xs" wrap="nowrap"> <Group p="xs" wrap="nowrap">
<Button size="compact-md" onClick={confirmModal}>Restore</Button> <Button size="compact-md" onClick={confirmModal}>
<Button variant="default" size="compact-md" onClick={() => setHistoryModalOpen(false)}>Cancel</Button> Restore
</Button>
<Button
variant="default"
size="compact-md"
onClick={() => setHistoryModalOpen(false)}
>
Cancel
</Button>
</Group> </Group>
</div> </div>
); );
} }

View File

@ -1,18 +1,26 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { getPageHistoryById, getPageHistoryList } from '@/features/page-history/services/page-history-service'; import {
import { IPageHistory } from '@/features/page-history/types/page.types'; getPageHistoryById,
getPageHistoryList,
} from "@/features/page-history/services/page-history-service";
import { IPageHistory } from "@/features/page-history/types/page.types";
import { IPagination } from "@/lib/types.ts";
export function usePageHistoryListQuery(pageId: string): UseQueryResult<IPageHistory[], Error> { export function usePageHistoryListQuery(
pageId: string,
): UseQueryResult<IPagination<IPageHistory>, Error> {
return useQuery({ return useQuery({
queryKey: ['page-history-list', pageId], queryKey: ["page-history-list", pageId],
queryFn: () => getPageHistoryList(pageId), queryFn: () => getPageHistoryList(pageId),
enabled: !!pageId, enabled: !!pageId,
}); });
} }
export function usePageHistoryQuery(historyId: string): UseQueryResult<IPageHistory, Error> { export function usePageHistoryQuery(
historyId: string,
): UseQueryResult<IPageHistory, Error> {
return useQuery({ return useQuery({
queryKey: ['page-history', historyId], queryKey: ["page-history", historyId],
queryFn: () => getPageHistoryById(historyId), queryFn: () => getPageHistoryById(historyId),
enabled: !!historyId, enabled: !!historyId,
staleTime: 10 * 60 * 1000, staleTime: 10 * 60 * 1000,

View File

@ -1,12 +1,20 @@
import api from '@/lib/api-client'; import api from "@/lib/api-client";
import { IPageHistory } from '@/features/page-history/types/page.types'; import { IPageHistory } from "@/features/page-history/types/page.types";
export async function getPageHistoryList(pageId: string): Promise<IPageHistory[]> { export async function getPageHistoryList(
const req = await api.post<IPageHistory[]>('/pages/history', { pageId }); pageId: string,
return req.data as IPageHistory[]; ): Promise<IPageHistory[]> {
const req = await api.post("/pages/history", {
pageId,
});
return req.data;
} }
export async function getPageHistoryById(id: string): Promise<IPageHistory> { export async function getPageHistoryById(
const req = await api.post<IPageHistory>('/pages/history/details', { id }); historyId: string,
return req.data as IPageHistory; ): Promise<IPageHistory> {
const req = await api.post<IPageHistory>("/pages/history/info", {
historyId,
});
return req.data;
} }

View File

@ -5,6 +5,8 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
UseGuards, UseGuards,
NotFoundException,
ForbiddenException,
} from '@nestjs/common'; } from '@nestjs/common';
import { CommentService } from './comment.service'; import { CommentService } from './comment.service';
import { CreateCommentDto } from './dto/create-comment.dto'; import { CreateCommentDto } from './dto/create-comment.dto';
@ -15,43 +17,90 @@ import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { User, Workspace } from '@docmost/db/types/entity.types'; import { User, Workspace } from '@docmost/db/types/entity.types';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import {
SpaceCaslAction,
SpaceCaslSubject,
} from '../casl/interfaces/space-ability.type';
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('comments') @Controller('comments')
export class CommentController { export class CommentController {
constructor(private readonly commentService: CommentService) {} constructor(
private readonly commentService: CommentService,
private readonly commentRepo: CommentRepo,
private readonly pageRepo: PageRepo,
private readonly spaceAbility: SpaceAbilityFactory,
) {}
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.OK)
@Post('create') @Post('create')
async create( async create(
@Body() createCommentDto: CreateCommentDto, @Body() createCommentDto: CreateCommentDto,
@AuthUser() user: User, @AuthUser() user: User,
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
) { ) {
const page = await this.pageRepo.findById(createCommentDto.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.commentService.create(user.id, workspace.id, createCommentDto); return this.commentService.create(user.id, workspace.id, createCommentDto);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post() @Post('/')
findPageComments( async findPageComments(
@Body() input: PageIdDto, @Body() input: PageIdDto,
@Body() @Body()
pagination: PaginationOptions, pagination: PaginationOptions,
//@AuthUser() user: User, @AuthUser() user: User,
// @AuthWorkspace() workspace: Workspace, // @AuthWorkspace() workspace: Workspace,
) { ) {
const page = await this.pageRepo.findById(input.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.commentService.findByPageId(input.pageId, pagination); return this.commentService.findByPageId(input.pageId, pagination);
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('info') @Post('info')
findOne(@Body() input: CommentIdDto) { async findOne(@Body() input: CommentIdDto, @AuthUser() user: User) {
return this.commentService.findById(input.commentId); const comment = await this.commentRepo.findById(input.commentId);
if (!comment) {
throw new NotFoundException('Comment not found');
}
// TODO: add spaceId to comment entity.
const page = await this.pageRepo.findById(comment.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return comment;
} }
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('update') @Post('update')
update(@Body() updateCommentDto: UpdateCommentDto) { update(@Body() updateCommentDto: UpdateCommentDto, @AuthUser() user: User) {
//TODO: only comment creators can update their comments
return this.commentService.update( return this.commentService.update(
updateCommentDto.commentId, updateCommentDto.commentId,
updateCommentDto, updateCommentDto,
@ -60,7 +109,8 @@ export class CommentController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post('delete') @Post('delete')
remove(@Body() input: CommentIdDto) { remove(@Body() input: CommentIdDto, @AuthUser() user: User) {
// TODO: only comment creators and admins can delete their comments
return this.commentService.remove(input.commentId); return this.commentService.remove(input.commentId);
} }
} }

View File

@ -1,10 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CommentService } from './comment.service'; import { CommentService } from './comment.service';
import { CommentController } from './comment.controller'; import { CommentController } from './comment.controller';
import { PageModule } from '../page/page.module';
@Module({ @Module({
imports: [PageModule], imports: [],
controllers: [CommentController], controllers: [CommentController],
providers: [CommentService], providers: [CommentService],
exports: [CommentService], exports: [CommentService],

View File

@ -5,17 +5,17 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { CreateCommentDto } from './dto/create-comment.dto'; import { CreateCommentDto } from './dto/create-comment.dto';
import { UpdateCommentDto } from './dto/update-comment.dto'; import { UpdateCommentDto } from './dto/update-comment.dto';
import { PageService } from '../page/services/page.service';
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo'; import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
import { Comment } from '@docmost/db/types/entity.types'; import { Comment } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { PaginationResult } from '@docmost/db/pagination/pagination'; import { PaginationResult } from '@docmost/db/pagination/pagination';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
@Injectable() @Injectable()
export class CommentService { export class CommentService {
constructor( constructor(
private commentRepo: CommentRepo, private commentRepo: CommentRepo,
private pageService: PageService, private pageRepo: PageRepo,
) {} ) {}
async findById(commentId: string) { async findById(commentId: string) {
@ -35,7 +35,7 @@ export class CommentService {
) { ) {
const commentContent = JSON.parse(createCommentDto.content); const commentContent = JSON.parse(createCommentDto.content);
const page = await this.pageService.findById(createCommentDto.pageId); const page = await this.pageRepo.findById(createCommentDto.pageId);
// const spaceId = null; // todo, get from page // const spaceId = null; // todo, get from page
if (!page) { if (!page) {
@ -59,7 +59,7 @@ export class CommentService {
const createdComment = await this.commentRepo.insertComment({ const createdComment = await this.commentRepo.insertComment({
pageId: createCommentDto.pageId, pageId: createCommentDto.pageId,
content: commentContent, content: commentContent,
selection: createCommentDto?.selection.substring(0, 250), selection: createCommentDto?.selection?.substring(0, 250),
type: 'inline', // for now type: 'inline', // for now
parentCommentId: createCommentDto?.parentCommentId, parentCommentId: createCommentDto?.parentCommentId,
creatorId: userId, creatorId: userId,
@ -74,7 +74,7 @@ export class CommentService {
pageId: string, pageId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
): Promise<PaginationResult<Comment>> { ): Promise<PaginationResult<Comment>> {
const page = await this.pageService.findById(pageId); const page = await this.pageRepo.findById(pageId);
if (!page) { if (!page) {
throw new BadRequestException('Page not found'); throw new BadRequestException('Page not found');

View File

@ -18,24 +18,11 @@ import { DB } from '@docmost/db/types/db';
export class PageHistoryRepo { export class PageHistoryRepo {
constructor(@InjectKysely() private readonly db: KyselyDB) {} constructor(@InjectKysely() private readonly db: KyselyDB) {}
private baseFields: Array<keyof PageHistory> = [
'id',
'pageId',
'title',
'slug',
'icon',
'coverPhoto',
'version',
'lastUpdatedById',
'workspaceId',
'createdAt',
'updatedAt',
];
async findById(pageHistoryId: string): Promise<PageHistory> { async findById(pageHistoryId: string): Promise<PageHistory> {
return await this.db return await this.db
.selectFrom('pageHistory') .selectFrom('pageHistory')
.select((eb) => [...this.baseFields, this.withLastUpdatedBy(eb)]) .selectAll()
.select((eb) => this.withLastUpdatedBy(eb))
.where('id', '=', pageHistoryId) .where('id', '=', pageHistoryId)
.executeTakeFirst(); .executeTakeFirst();
} }
@ -83,7 +70,8 @@ export class PageHistoryRepo {
async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) { async findPageHistoryByPageId(pageId: string, pagination: PaginationOptions) {
const query = this.db const query = this.db
.selectFrom('pageHistory') .selectFrom('pageHistory')
.select((eb) => [...this.baseFields, this.withLastUpdatedBy(eb)]) .selectAll()
.select((eb) => this.withLastUpdatedBy(eb))
.where('pageId', '=', pageId) .where('pageId', '=', pageId)
.orderBy('createdAt', 'desc'); .orderBy('createdAt', 'desc');
@ -101,6 +89,6 @@ export class PageHistoryRepo {
.selectFrom('users') .selectFrom('users')
.select(['users.id', 'users.name', 'users.avatarUrl']) .select(['users.id', 'users.name', 'users.avatarUrl'])
.whereRef('users.id', '=', 'pageHistory.lastUpdatedById'), .whereRef('users.id', '=', 'pageHistory.lastUpdatedById'),
).as('withLastUpdatedBy'); ).as('lastUpdatedBy');
} }
} }

View File

@ -10,40 +10,40 @@
"@hocuspocus/provider": "^2.11.3", "@hocuspocus/provider": "^2.11.3",
"@hocuspocus/server": "^2.11.3", "@hocuspocus/server": "^2.11.3",
"@hocuspocus/transformer": "^2.11.3", "@hocuspocus/transformer": "^2.11.3",
"@tiptap/extension-code-block": "^2.2.4", "@tiptap/extension-code-block": "^2.3.0",
"@tiptap/extension-collaboration": "^2.2.4", "@tiptap/extension-collaboration": "^2.3.0",
"@tiptap/extension-collaboration-cursor": "^2.2.4", "@tiptap/extension-collaboration-cursor": "^2.3.0",
"@tiptap/extension-color": "^2.2.4", "@tiptap/extension-color": "^2.3.0",
"@tiptap/extension-document": "^2.2.4", "@tiptap/extension-document": "^2.3.0",
"@tiptap/extension-heading": "^2.2.4", "@tiptap/extension-heading": "^2.3.0",
"@tiptap/extension-highlight": "^2.2.4", "@tiptap/extension-highlight": "^2.3.0",
"@tiptap/extension-link": "^2.2.4", "@tiptap/extension-link": "^2.3.0",
"@tiptap/extension-list-item": "^2.2.4", "@tiptap/extension-list-item": "^2.3.0",
"@tiptap/extension-list-keymap": "^2.2.4", "@tiptap/extension-list-keymap": "^2.3.0",
"@tiptap/extension-mention": "^2.2.4", "@tiptap/extension-mention": "^2.3.0",
"@tiptap/extension-placeholder": "^2.2.4", "@tiptap/extension-placeholder": "^2.3.0",
"@tiptap/extension-subscript": "^2.2.4", "@tiptap/extension-subscript": "^2.3.0",
"@tiptap/extension-superscript": "^2.2.4", "@tiptap/extension-superscript": "^2.3.0",
"@tiptap/extension-task-item": "^2.2.4", "@tiptap/extension-task-item": "^2.3.0",
"@tiptap/extension-task-list": "^2.2.4", "@tiptap/extension-task-list": "^2.3.0",
"@tiptap/extension-text": "^2.2.4", "@tiptap/extension-text": "^2.3.0",
"@tiptap/extension-text-align": "^2.2.4", "@tiptap/extension-text-align": "^2.3.0",
"@tiptap/extension-text-style": "^2.2.4", "@tiptap/extension-text-style": "^2.3.0",
"@tiptap/extension-typography": "^2.2.4", "@tiptap/extension-typography": "^2.3.0",
"@tiptap/extension-underline": "^2.2.4", "@tiptap/extension-underline": "^2.3.0",
"@tiptap/html": "^2.2.4", "@tiptap/html": "^2.3.0",
"@tiptap/pm": "^2.2.4", "@tiptap/pm": "^2.3.0",
"@tiptap/react": "^2.2.4", "@tiptap/react": "^2.3.0",
"@tiptap/starter-kit": "^2.2.4", "@tiptap/starter-kit": "^2.3.0",
"@tiptap/suggestion": "^2.2.4", "@tiptap/suggestion": "^2.3.0",
"fractional-indexing-jittered": "^0.9.1", "fractional-indexing-jittered": "^0.9.1",
"tiptap-extension-global-drag-handle": "^0.1.6", "tiptap-extension-global-drag-handle": "^0.1.6",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"yjs": "^13.6.14" "yjs": "^13.6.14"
}, },
"devDependencies": { "devDependencies": {
"@nx/js": "18.2.2", "@nx/js": "18.3.3",
"nx": "18.2.2" "nx": "18.3.3"
}, },
"workspaces": { "workspaces": {
"packages": [ "packages": [

570
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff