feat: edit own task comments (3-dots menu, inline editor, updated timestamp) - EXO-88900 - #633
Conversation
On the comments a user authored, the hover trash button is replaced by a
3-dots action menu offering Edit and Delete. Edit opens the comment editor
inline, prefilled with the comment content, with Update and Cancel actions.
Only one editor can be opened at a time: starting an edition closes any
other opened edition or reply editor, and vice versa.
An edition stamps a new TASK_COMMENTS.UPDATED_TIME column (Liquibase
changelog 7.3.0) so the comment timestamp is rendered as "Updated <relative
time>", in the comments list as well as in the last comment preview.
Backend: PUT /rest/tasks/comments/{commentId} + updateComment() down the
Storage and Service layers. Only the author can edit a comment, whereas the
deletion stays allowed to the project managers as well. The new
exo.task.taskCommentUpdate event is broadcast and handled by the content
link listener to reprocess the links of the edited content; the
notification, gamification and analytics listeners are deliberately left
out so that an edition does not notify nor reward again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- use String.replace() for the literal '+' escaping instead of replaceAll() (java:S5361) - optional chaining on the update comment response (javascript:S6582) - declare the emits option on the 3 touched components (javascript:S8961), listing every emitted event and not only the reported ones Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…EXO-88900 New code coverage was at 35% for a 60% threshold, the REST endpoint being the biggest uncovered part (22 lines). - TestTaskRestService.testUpdateComment: unknown comment (404), comment of another author (403), empty content (400), successful update (200 with the update time), URL encoded content and service failure (500) - CommentStorageTest.testUpdateComment: update time stamping, mentions recomputed from the new content, EntityNotFoundException on unknown id - CommentServiceTest.testCloneCommentKeepsUpdateTime: CommentDto.clone() carries the update time Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ahamdi
left a comment
There was a problem hiding this comment.
Could you update the PR description and use English instead of french. Thanks
| } | ||
|
|
||
| @Override | ||
| public CommentDto updateComment(long commentId, String commentText) throws EntityNotFoundException { |
There was a problem hiding this comment.
Could you move the check of the permission of who can edit a comment here instead of the Rest service to centralize the check at the service level
There was a problem hiding this comment.
Done in 1f9b274: the permission check now lives in CommentServiceImpl.updateComment(), so every caller of the service goes through it. Details, including a follow-up NPE fix, in my comment below.
…XO-88900 Following the review: the check of who is allowed to edit a comment moves from the REST layer to CommentServiceImpl, so that every caller of the service goes through it. updateComment() now takes the identity of the user attempting the edition and raises NotAllowedOperationOnEntityException when he is not the author, the same exception already used by the status and user services. The endpoint no longer loads the comment beforehand nor checks the permission: it only maps EntityNotFoundException to 404 and NotAllowedOperationOnEntityException to 403. The permission test moves accordingly from TestTaskRestService to CommentServiceTest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SonarCloud reported a possible NullPointerException (javabugs:S2259) on the update endpoint: CommentStorageImpl.updateComment() returns the result of StorageUtil.commentToDto(), which is null when the DAO update returns null, and the endpoint dereferenced it right away. Since the refactoring, the endpoint no longer loads the comment beforehand, which made that path reachable. The endpoint now answers a server error instead of failing with a NPE, and the case is covered by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Thanks for the review @ahamdi. Both points are addressed, plus a follow-up fix for a quality gate failure they caused. 1. Permission check centralized at the service level (1f9b274)
if (!TaskUtil.canEditComment(identity, comment)) {
throw new NotAllowedOperationOnEntityException(commentId, CommentDto.class, "edit");
}Three choices worth pointing out:
The permission test moved along with the logic, from 2. PR description translated to EnglishDone, same structure as before. 3. NullPointerException reported by SonarCloud after the refactoring (53e6068)The refactoring above turned the quality gate red on The endpoint now answers a server error rather than throwing, and the case is covered by a test: if (updatedComment == null) {
LOG.error("Comment {} could not be updated", commentId);
return Response.serverError().build();
}A server error rather than a 404 on purpose: the service already raises StatusQuality gate is back to passing: 0 issue, reliability and maintainability ratings A, 94.9% coverage on new code. 32 backend tests pass, |
|
@ahamdi one open point I deliberately left out of this PR. Could you tell me whether you want them handled here, in a follow-up, or not at all? 1.
|
Non blocker for me, we can proceed with the PR as is |



Covered scenario
It works on the replies (sub-comments) as well. A single editor can be opened at a time: starting an edition closes any other opened edition or reply editor, and the other way round.
Backend
task.db.changelog-7.3.0.xml: newTASK_COMMENTS.UPDATED_TIMEcolumn (nullable, leftNULLfor the comments which were never edited), declared inconfiguration.xmlupdatedTimecarried alongComment->CommentDto->StorageUtil->CommentEntityupdateComment(commentId, commentText, identity)added toCommentStoragethenCommentService(Storage/Service/REST layering respected): it rewrites the content, stamps the update time and recomputes the mentionsPUT /rest/tasks/comments/{commentId}, with the same URL decoding as the comment creation. The endpoint only maps the service exceptions to the HTTP statuses:EntityNotFoundException-> 404,NotAllowedOperationOnEntityException-> 403CommentServiceImplthroughTaskUtil.canEditComment: only the author can edit a comment, whereascanDeleteCommentalso allows the project managersexo.task.taskCommentUpdateevent, handled byTaskCommentContentLinkListenerto reprocess the content links of the edited text. The notification, gamification and analytics listeners are deliberately left out: an edition must neither notify again nor reward againFrontend
TaskCommentItem.vue:v-menuaction menu (following theTaskDrawer.vuepattern:attach,offset-y, right aligned through:left="!$vuetify.rtl"), icons inicon-default-color, local edition state, mutual exclusion through the root event busTaskCommentEditor.vue:edit-mode— CKEditor prefilled with the raw comment text,object-idset to the comment so that its attachments are loaded, Update/Cancel buttons aligned on the composer left edge, no duplicated avatar. In edition mode the editor ignores the reply global events so that it does not close itselfUpdated <relative time>timestamp in the comments list and in the last comment preview_enbundle only (Crowdin handles the others)Tests
TestTaskRestService.testUpdateComment: empty content (400), unknown comment (404), comment of another author (403), successful update (200 with the update time), URL encoded content and service failure (500)CommentServiceTest: content and update time,EntityNotFoundExceptionon an unknown comment,NotAllowedOperationOnEntityExceptionwhen the user is not the author,CommentDto.clone()carrying the update timeCommentStorageTest.testUpdateComment: update time stamping, mentions recomputed from the new content,EntityNotFoundExceptionon an unknown idnpm run build+ eslint report no errorexoplatform/exo-community:7.3.x-devx-SNAPSHOT: the7.3.0changelog applies without error on an already populated databaseReview notes
exo-confirm-dialogguard could be added if wantedupdate_task_commentMCP tool was exposed, the need being UI only🤖 Generated with Claude Code