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:
Philip Okugbe
2025-07-29 21:36:48 +01:00
committed by GitHub
parent ec12e80423
commit ca9558b246
23 changed files with 927 additions and 341 deletions
@@ -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);
}
}