mirror of
https://github.com/docmost/docmost.git
synced 2025-11-19 10:11:08 +10:00
feat(EE): resolve comments (#1420)
* feat: resolve comment (EE) * Add resolve to comment mark in editor (EE) * comment ui permissions * sticky comment state tabs (EE) * cleanup * feat: add space_id to comments and allow space admins to delete any comment - Add space_id column to comments table with data migration from pages - Add last_edited_by_id, resolved_by_id, and updated_at columns to comments - Update comment deletion permissions to allow space admins to delete any comment - Backfill space_id on old comments * fix foreign keys
This commit is contained in:
@ -53,9 +53,11 @@ export class CommentController {
|
||||
}
|
||||
|
||||
return this.commentService.create(
|
||||
user.id,
|
||||
page.id,
|
||||
workspace.id,
|
||||
{
|
||||
userId: user.id,
|
||||
page,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
createCommentDto,
|
||||
);
|
||||
}
|
||||
@ -67,7 +69,6 @@ export class CommentController {
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
@AuthUser() user: User,
|
||||
// @AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const page = await this.pageRepo.findById(input.pageId);
|
||||
if (!page) {
|
||||
@ -89,12 +90,7 @@ export class CommentController {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
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);
|
||||
const ability = await this.spaceAbility.createForUser(user, comment.spaceId);
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
@ -103,19 +99,76 @@ export class CommentController {
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('update')
|
||||
update(@Body() updateCommentDto: UpdateCommentDto, @AuthUser() user: User) {
|
||||
//TODO: only comment creators can update their comments
|
||||
return this.commentService.update(
|
||||
updateCommentDto.commentId,
|
||||
updateCommentDto,
|
||||
async update(@Body() dto: UpdateCommentDto, @AuthUser() user: User) {
|
||||
const comment = await this.commentRepo.findById(dto.commentId);
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
const ability = await this.spaceAbility.createForUser(
|
||||
user,
|
||||
comment.spaceId,
|
||||
);
|
||||
|
||||
// must be a space member with edit permission
|
||||
if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException(
|
||||
'You must have space edit permission to edit comments',
|
||||
);
|
||||
}
|
||||
|
||||
return this.commentService.update(comment, dto, user);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('delete')
|
||||
remove(@Body() input: CommentIdDto, @AuthUser() user: User) {
|
||||
// TODO: only comment creators and admins can delete their comments
|
||||
return this.commentService.remove(input.commentId, user);
|
||||
async delete(@Body() input: CommentIdDto, @AuthUser() user: User) {
|
||||
const comment = await this.commentRepo.findById(input.commentId);
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
const ability = await this.spaceAbility.createForUser(
|
||||
user,
|
||||
comment.spaceId,
|
||||
);
|
||||
|
||||
// must be a space member with edit permission
|
||||
if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
// Check if user is the comment owner
|
||||
const isOwner = comment.creatorId === user.id;
|
||||
|
||||
if (isOwner) {
|
||||
/*
|
||||
// Check if comment has children from other users
|
||||
const hasChildrenFromOthers =
|
||||
await this.commentRepo.hasChildrenFromOtherUsers(comment.id, user.id);
|
||||
|
||||
// Owner can delete if no children from other users
|
||||
if (!hasChildrenFromOthers) {
|
||||
await this.commentRepo.deleteComment(comment.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// If has children from others, only space admin can delete
|
||||
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)) {
|
||||
throw new ForbiddenException(
|
||||
'Only space admins can delete comments with replies from other users',
|
||||
);
|
||||
}*/
|
||||
await this.commentRepo.deleteComment(comment.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Space admin can delete any comment
|
||||
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)) {
|
||||
throw new ForbiddenException(
|
||||
'You can only delete your own comments or must be a space admin',
|
||||
);
|
||||
}
|
||||
await this.commentRepo.deleteComment(comment.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,21 +7,24 @@ import {
|
||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||
import { UpdateCommentDto } from './dto/update-comment.dto';
|
||||
import { CommentRepo } from '@docmost/db/repos/comment/comment.repo';
|
||||
import { Comment, User } from '@docmost/db/types/entity.types';
|
||||
import { Comment, Page, User } from '@docmost/db/types/entity.types';
|
||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { PaginationResult } from '@docmost/db/pagination/pagination';
|
||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
|
||||
@Injectable()
|
||||
export class CommentService {
|
||||
constructor(
|
||||
private commentRepo: CommentRepo,
|
||||
private pageRepo: PageRepo,
|
||||
private spaceMemberRepo: SpaceMemberRepo,
|
||||
) {}
|
||||
|
||||
async findById(commentId: string) {
|
||||
const comment = await this.commentRepo.findById(commentId, {
|
||||
includeCreator: true,
|
||||
includeResolvedBy: true,
|
||||
});
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
@ -30,11 +33,10 @@ export class CommentService {
|
||||
}
|
||||
|
||||
async create(
|
||||
userId: string,
|
||||
pageId: string,
|
||||
workspaceId: string,
|
||||
opts: { userId: string; page: Page; workspaceId: string },
|
||||
createCommentDto: CreateCommentDto,
|
||||
) {
|
||||
const { userId, page, workspaceId } = opts;
|
||||
const commentContent = JSON.parse(createCommentDto.content);
|
||||
|
||||
if (createCommentDto.parentCommentId) {
|
||||
@ -42,7 +44,7 @@ export class CommentService {
|
||||
createCommentDto.parentCommentId,
|
||||
);
|
||||
|
||||
if (!parentComment || parentComment.pageId !== pageId) {
|
||||
if (!parentComment || parentComment.pageId !== page.id) {
|
||||
throw new BadRequestException('Parent comment not found');
|
||||
}
|
||||
|
||||
@ -51,17 +53,16 @@ export class CommentService {
|
||||
}
|
||||
}
|
||||
|
||||
const createdComment = await this.commentRepo.insertComment({
|
||||
pageId: pageId,
|
||||
return await this.commentRepo.insertComment({
|
||||
pageId: page.id,
|
||||
content: commentContent,
|
||||
selection: createCommentDto?.selection?.substring(0, 250),
|
||||
type: 'inline',
|
||||
parentCommentId: createCommentDto?.parentCommentId,
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
spaceId: page.spaceId,
|
||||
});
|
||||
|
||||
return createdComment;
|
||||
}
|
||||
|
||||
async findByPageId(
|
||||
@ -74,26 +75,16 @@ export class CommentService {
|
||||
throw new BadRequestException('Page not found');
|
||||
}
|
||||
|
||||
const pageComments = await this.commentRepo.findPageComments(
|
||||
pageId,
|
||||
pagination,
|
||||
);
|
||||
|
||||
return pageComments;
|
||||
return await this.commentRepo.findPageComments(pageId, pagination);
|
||||
}
|
||||
|
||||
async update(
|
||||
commentId: string,
|
||||
comment: Comment,
|
||||
updateCommentDto: UpdateCommentDto,
|
||||
authUser: User,
|
||||
): Promise<Comment> {
|
||||
const commentContent = JSON.parse(updateCommentDto.content);
|
||||
|
||||
const comment = await this.commentRepo.findById(commentId);
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
if (comment.creatorId !== authUser.id) {
|
||||
throw new ForbiddenException('You can only edit your own comments');
|
||||
}
|
||||
@ -104,26 +95,14 @@ export class CommentService {
|
||||
{
|
||||
content: commentContent,
|
||||
editedAt: editedAt,
|
||||
updatedAt: editedAt,
|
||||
},
|
||||
commentId,
|
||||
comment.id,
|
||||
);
|
||||
comment.content = commentContent;
|
||||
comment.editedAt = editedAt;
|
||||
comment.updatedAt = editedAt;
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
async remove(commentId: string, authUser: User): Promise<void> {
|
||||
const comment = await this.commentRepo.findById(commentId);
|
||||
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
if (comment.creatorId !== authUser.id) {
|
||||
throw new ForbiddenException('You can only delete your own comments');
|
||||
}
|
||||
|
||||
await this.commentRepo.deleteComment(commentId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user