From 62ed72ec4e79c5736f6a975bc641c1f3966fe830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Richard?= Date: Wed, 27 Jan 2021 08:25:42 -0500 Subject: [PATCH] Allow comment deletion (#224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow comment deletion * Add delete_changeset for comment, decrement comments count * Add comment delete resolver test * Don't use pipe on single function call Co-authored-by: Simon Prévost * Improve controller functions * Run format Co-authored-by: Simon Prévost --- lib/accent/auth/canada_implementations.ex | 6 ++- lib/accent/schemas/comment.ex | 14 ++++++- lib/graphql/helpers/authorization.ex | 9 +++++ lib/graphql/mutations/comment.ex | 6 +++ lib/graphql/resolvers/comment.ex | 12 +++++- test/graphql/resolvers/comment_test.exs | 11 ++++++ webapp/app/locales/en-us.json | 16 +++++++- .../project-comments-list/component.ts | 1 + .../project-comments-list/item/component.ts | 1 + .../project-comments-list/item/template.hbs | 1 + .../project-comments-list/template.hbs | 1 + .../translation-comment-delete/component.ts | 28 ++++++++++++++ .../translation-comment-delete/styles.scss | 0 .../translation-comment-delete/template.hbs | 6 +++ .../translation-comments-list/component.ts | 1 + .../item/component.ts | 22 +++++++++++ .../item/styles.scss | 1 + .../item/template.hbs | 4 ++ .../translation-comments-list/template.hbs | 5 ++- .../translation-conversation/component.ts | 1 + .../translation-conversation/template.hbs | 1 + .../logged-in/project/comments/controller.ts | 38 +++++++++++++++++++ .../logged-in/project/comments/template.hbs | 1 + .../translation/comments/controller.ts | 32 ++++++++++++++++ .../project/translation/comments/template.hbs | 1 + webapp/app/queries/delete-comment.ts | 13 +++++++ 26 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 webapp/app/pods/components/translation-comment-delete/component.ts create mode 100644 webapp/app/pods/components/translation-comment-delete/styles.scss create mode 100644 webapp/app/pods/components/translation-comment-delete/template.hbs create mode 100644 webapp/app/queries/delete-comment.ts diff --git a/lib/accent/auth/canada_implementations.ex b/lib/accent/auth/canada_implementations.ex index 4c619c1c6..8ced96d9b 100644 --- a/lib/accent/auth/canada_implementations.ex +++ b/lib/accent/auth/canada_implementations.ex @@ -1,5 +1,5 @@ defimpl Canada.Can, for: Accent.User do - alias Accent.{User, Project, Revision} + alias Accent.{User, Project, Revision, Comment} def can?(%User{permissions: permissions}, action, project_id) when is_binary(project_id) do validate_role(permissions, action, project_id) @@ -13,6 +13,10 @@ defimpl Canada.Can, for: Accent.User do validate_role(permissions, action, project_id) end + def can?(%User{id: user_id}, _action, %Comment{user_id: comment_user_id}) do + user_id == comment_user_id + end + def can?(%User{email: email}, action, _) do Accent.EmailAbilities.can?(email, action) end diff --git a/lib/accent/schemas/comment.ex b/lib/accent/schemas/comment.ex index ee0abef48..3b785b8df 100644 --- a/lib/accent/schemas/comment.ex +++ b/lib/accent/schemas/comment.ex @@ -14,7 +14,7 @@ defmodule Accent.Comment do @required_fields ~w(text user_id translation_id)a - def changeset(model, params) do + def create_changeset(model, params) do model |> cast(params, @required_fields ++ []) |> validate_required(@required_fields) @@ -28,4 +28,16 @@ defmodule Accent.Comment do changeset end) end + + def delete_changeset(model) do + model + |> change() + |> prepare_changes(fn changeset -> + Accent.Translation + |> where(id: ^model.translation_id) + |> changeset.repo.update_all(inc: [comments_count: -1]) + + changeset + end) + end end diff --git a/lib/graphql/helpers/authorization.ex b/lib/graphql/helpers/authorization.ex index f277c036b..ee577a716 100644 --- a/lib/graphql/helpers/authorization.ex +++ b/lib/graphql/helpers/authorization.ex @@ -3,6 +3,7 @@ defmodule Accent.GraphQL.Helpers.Authorization do alias Accent.{ Collaborator, + Comment, Document, Integration, Operation, @@ -148,4 +149,12 @@ defmodule Accent.GraphQL.Helpers.Authorization do authorize(action, integration.project_id, info, do: func.(integration, args, info)) end end + + def comment_authorize(action, func) do + fn _, args, info -> + comment = Repo.get(Comment, args.id) + + authorize(action, comment, info, do: func.(comment, args, info)) + end + end end diff --git a/lib/graphql/mutations/comment.ex b/lib/graphql/mutations/comment.ex index 117360ca3..5dfc4cc9a 100644 --- a/lib/graphql/mutations/comment.ex +++ b/lib/graphql/mutations/comment.ex @@ -11,6 +11,12 @@ defmodule Accent.GraphQL.Mutations.Comment do resolve(translation_authorize(:create_comment, &Accent.GraphQL.Resolvers.Comment.create/3)) end + field :delete_comment, :mutated_comment do + arg(:id, non_null(:id)) + + resolve(comment_authorize(:delete_comment, &Accent.GraphQL.Resolvers.Comment.delete/3)) + end + field :create_translation_comments_subscription, :mutated_translation_comments_subscription do arg(:translation_id, non_null(:id)) arg(:user_id, non_null(:id)) diff --git a/lib/graphql/resolvers/comment.ex b/lib/graphql/resolvers/comment.ex index 47814e42f..46d9c1a3f 100644 --- a/lib/graphql/resolvers/comment.ex +++ b/lib/graphql/resolvers/comment.ex @@ -21,7 +21,7 @@ defmodule Accent.GraphQL.Resolvers.Comment do "translation_id" => translation.id } - changeset = Comment.changeset(%Comment{}, comment_params) + changeset = Comment.create_changeset(%Comment{}, comment_params) case Repo.insert(changeset) do {:ok, comment} -> @@ -45,6 +45,16 @@ defmodule Accent.GraphQL.Resolvers.Comment do end end + @spec delete(Comment.t(), any(), GraphQLContext.t()) :: comment_operation + def delete(comment, _, _) do + {:ok, comment} = + comment + |> Comment.delete_changeset() + |> Repo.delete() + + {:ok, %{comment: comment, errors: nil}} + end + @spec list_project(Project.t(), %{page: number()}, GraphQLContext.t()) :: {:ok, Paginated.t(Comment.t())} def list_project(project, args, _) do Comment diff --git a/test/graphql/resolvers/comment_test.exs b/test/graphql/resolvers/comment_test.exs index 3617ee7ce..caf8b6ec4 100644 --- a/test/graphql/resolvers/comment_test.exs +++ b/test/graphql/resolvers/comment_test.exs @@ -54,6 +54,17 @@ defmodule AccentTest.GraphQL.Resolvers.Comment do assert get_in(Repo.all(Comment), [Access.all(), Access.key(:text)]) == ["First comment"] end + test "delete", %{translation: translation, user: user} do + comment = %Comment{translation_id: translation.id, text: "test", user: user} |> Repo.insert!() + + assert get_in(Repo.all(Comment), [Access.all(), Access.key(:id)]) == [comment.id] + + {:ok, result} = Resolver.delete(comment, nil, nil) + + assert get_in(result, [:errors]) == nil + assert Repo.all(Comment) == [] + end + test "list project", %{project: project, translation: translation, user: user} do comment = %Comment{translation_id: translation.id, text: "test", user: user} |> Repo.insert!() diff --git a/webapp/app/locales/en-us.json b/webapp/app/locales/en-us.json index 374b922ad..8bcad89b9 100644 --- a/webapp/app/locales/en-us.json +++ b/webapp/app/locales/en-us.json @@ -677,6 +677,10 @@ "conflict_on_slave": "The text has been moved to review and is now:" } }, + "translation_comment_delete": { + "delete": "Delete", + "delete_comment_confirm": "Are you sure you want to delete this comment? This action cannot be undone." + }, "translation_comment_form": { "comment_button": "Comment", "comment_placeholder": "Leave a comment…", @@ -940,6 +944,12 @@ "promote_master_revision_failure": "The language could not be promoted as master", "promote_master_revision_success": "The language has been promoted as master with success" } + }, + "comments": { + "flash_messages": { + "delete_error": "The comment could not be deleted", + "delete_success": "The comment has been deleted with success" + } } }, "projects": { @@ -971,7 +981,11 @@ } }, "comments": { - "loading_content": "Fetching comments…" + "loading_content": "Fetching comments…", + "flash_messages": { + "delete_error": "The comment could not be deleted", + "delete_success": "The comment has been deleted with success" + } } } } diff --git a/webapp/app/pods/components/project-comments-list/component.ts b/webapp/app/pods/components/project-comments-list/component.ts index 09c61bd28..abcf47451 100644 --- a/webapp/app/pods/components/project-comments-list/component.ts +++ b/webapp/app/pods/components/project-comments-list/component.ts @@ -3,6 +3,7 @@ import Component from '@glimmer/component'; interface Args { project: any; comments: any; + onDeleteComment: (comment: {id: string}) => Promise; } export default class ProjectCommentsList extends Component { diff --git a/webapp/app/pods/components/project-comments-list/item/component.ts b/webapp/app/pods/components/project-comments-list/item/component.ts index cb07aac13..22874866b 100644 --- a/webapp/app/pods/components/project-comments-list/item/component.ts +++ b/webapp/app/pods/components/project-comments-list/item/component.ts @@ -5,6 +5,7 @@ import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key'; interface Args { groupedComment: any; project: any; + onDeleteComment: (comment: {id: string}) => Promise; } export default class ProjectCommentsListItem extends Component { diff --git a/webapp/app/pods/components/project-comments-list/item/template.hbs b/webapp/app/pods/components/project-comments-list/item/template.hbs index 41c4acda4..b9b6c8fff 100644 --- a/webapp/app/pods/components/project-comments-list/item/template.hbs +++ b/webapp/app/pods/components/project-comments-list/item/template.hbs @@ -38,5 +38,6 @@ "translationCommentsList translationRemoved" "translationCommentsList" }} + @onDeleteComment={{@onDeleteComment}} /> diff --git a/webapp/app/pods/components/project-comments-list/template.hbs b/webapp/app/pods/components/project-comments-list/template.hbs index 872c01d9d..fb6c3a134 100644 --- a/webapp/app/pods/components/project-comments-list/template.hbs +++ b/webapp/app/pods/components/project-comments-list/template.hbs @@ -3,6 +3,7 @@ {{else}} void; +} + +export default class TranslationCommentDelete extends Component { + @service('intl') + intl: IntlService; + + @action + deleteComment() { + const message = this.intl.t( + 'components.translation_comment_delete.delete_comment_confirm' + ); + + // eslint-disable-next-line no-alert + if (!window.confirm(message)) { + return; + } + + this.args.onSubmit(); + } +} diff --git a/webapp/app/pods/components/translation-comment-delete/styles.scss b/webapp/app/pods/components/translation-comment-delete/styles.scss new file mode 100644 index 000000000..e69de29bb diff --git a/webapp/app/pods/components/translation-comment-delete/template.hbs b/webapp/app/pods/components/translation-comment-delete/template.hbs new file mode 100644 index 000000000..5cdfb4b72 --- /dev/null +++ b/webapp/app/pods/components/translation-comment-delete/template.hbs @@ -0,0 +1,6 @@ + + {{t "components.translation_comment_delete.delete"}} + diff --git a/webapp/app/pods/components/translation-comments-list/component.ts b/webapp/app/pods/components/translation-comments-list/component.ts index d949ccc11..199bd031a 100644 --- a/webapp/app/pods/components/translation-comments-list/component.ts +++ b/webapp/app/pods/components/translation-comments-list/component.ts @@ -2,6 +2,7 @@ import Component from '@glimmer/component'; interface Args { comments: any; + onDeleteComment: (comment: {id: string}) => Promise; } export default class TranslationsCommentsList extends Component {} diff --git a/webapp/app/pods/components/translation-comments-list/item/component.ts b/webapp/app/pods/components/translation-comments-list/item/component.ts index b26d130b1..34ae4058d 100644 --- a/webapp/app/pods/components/translation-comments-list/item/component.ts +++ b/webapp/app/pods/components/translation-comments-list/item/component.ts @@ -1,6 +1,10 @@ +import {inject as service} from '@ember/service'; +import {readOnly} from '@ember/object/computed'; import Component from '@glimmer/component'; import MarkdownIt from 'markdown-it'; import {htmlSafe} from '@ember/string'; +import Session from 'accent-webapp/services/session'; +import {dropTask} from 'ember-concurrency-decorators'; const markdown = MarkdownIt({ html: false, @@ -10,17 +14,35 @@ const markdown = MarkdownIt({ interface Args { comment: { + id: string; text: string; insertedAt: Date; user: { + id: string; fullname: string; pictureUrl: string; }; }; + onDeleteComment: (comment: {id: string}) => Promise; } export default class TranslationsCommentsListItem extends Component { + @service('session') + session: Session; + + @readOnly('session.credentials.user') + currentUser: any; + + get isAuthor() { + return this.currentUser.id === this.args.comment.user.id; + } + get text() { return htmlSafe(markdown.render(this.args.comment.text)); } + + @dropTask + *deleteComment() { + yield this.args.onDeleteComment(this.args.comment); + } } diff --git a/webapp/app/pods/components/translation-comments-list/item/styles.scss b/webapp/app/pods/components/translation-comments-list/item/styles.scss index 5d058ba7a..761635178 100644 --- a/webapp/app/pods/components/translation-comments-list/item/styles.scss +++ b/webapp/app/pods/components/translation-comments-list/item/styles.scss @@ -21,6 +21,7 @@ .date { color: var(--color-grey); font-size: 11px; + margin-right: 6px; } .content { diff --git a/webapp/app/pods/components/translation-comments-list/item/template.hbs b/webapp/app/pods/components/translation-comments-list/item/template.hbs index eeaad5413..523cf311a 100644 --- a/webapp/app/pods/components/translation-comments-list/item/template.hbs +++ b/webapp/app/pods/components/translation-comments-list/item/template.hbs @@ -12,6 +12,10 @@ + + {{#if this.isAuthor }} + + {{/if}}
diff --git a/webapp/app/pods/components/translation-comments-list/template.hbs b/webapp/app/pods/components/translation-comments-list/template.hbs index cc87aad74..abd3ac7ed 100644 --- a/webapp/app/pods/components/translation-comments-list/template.hbs +++ b/webapp/app/pods/components/translation-comments-list/template.hbs @@ -1,7 +1,10 @@
    {{#each @comments key="id" as |comment|}}
  • - +
  • {{else}} Promise; onDeleteSubscription: (subscription: any) => Promise; onSubmit: (text: string) => Promise; + onDeleteComment: (comment: {id: string}) => Promise; onSelectPage: (page: number) => void; } diff --git a/webapp/app/pods/components/translation-conversation/template.hbs b/webapp/app/pods/components/translation-conversation/template.hbs index 340756b47..5b9678678 100644 --- a/webapp/app/pods/components/translation-conversation/template.hbs +++ b/webapp/app/pods/components/translation-conversation/template.hbs @@ -11,6 +11,7 @@
    diff --git a/webapp/app/pods/logged-in/project/comments/controller.ts b/webapp/app/pods/logged-in/project/comments/controller.ts index 6d2a953b1..8207be82c 100644 --- a/webapp/app/pods/logged-in/project/comments/controller.ts +++ b/webapp/app/pods/logged-in/project/comments/controller.ts @@ -1,11 +1,30 @@ +import {inject as service} from '@ember/service'; import {action} from '@ember/object'; import {equal, and} from '@ember/object/computed'; import Controller from '@ember/controller'; import {tracked} from '@glimmer/tracking'; +import IntlService from 'ember-intl/services/intl'; + +import commentDeleteQuery from 'accent-webapp/queries/delete-comment'; +import ApolloMutate from 'accent-webapp/services/apollo-mutate'; +import FlashMessages from 'ember-cli-flash/services/flash-messages'; + +const FLASH_MESSAGE_PREFIX = 'pods.project.comments.flash_messages.'; +const FLASH_MESSAGE_DELETE_COMMENT_SUCCESS = `${FLASH_MESSAGE_PREFIX}delete_success`; +const FLASH_MESSAGE_DELETE_COMMENT_ERROR = `${FLASH_MESSAGE_PREFIX}delete_error`; export default class CommentsController extends Controller { queryParams = ['page']; + @service('apollo-mutate') + apolloMutate: ApolloMutate; + + @service('flash-messages') + flashMessages: FlashMessages; + + @service('intl') + intl: IntlService; + @tracked page: number | null = 1; @@ -21,4 +40,23 @@ export default class CommentsController extends Controller { this.page = page; } + + @action + async deleteComment(comment: {id: string}) { + const response = await this.apolloMutate.mutate({ + mutation: commentDeleteQuery, + refetchQueries: ['ProjectComments'], + variables: { + commentId: comment.id, + }, + }); + + if (response.errors) { + this.flashMessages.error(this.intl.t(FLASH_MESSAGE_DELETE_COMMENT_ERROR)); + } else { + this.flashMessages.success( + this.intl.t(FLASH_MESSAGE_DELETE_COMMENT_SUCCESS) + ); + } + } } diff --git a/webapp/app/pods/logged-in/project/comments/template.hbs b/webapp/app/pods/logged-in/project/comments/template.hbs index eca2453d5..24a366e11 100644 --- a/webapp/app/pods/logged-in/project/comments/template.hbs +++ b/webapp/app/pods/logged-in/project/comments/template.hbs @@ -8,6 +8,7 @@ {{/if}} diff --git a/webapp/app/queries/delete-comment.ts b/webapp/app/queries/delete-comment.ts new file mode 100644 index 000000000..88edf4cc1 --- /dev/null +++ b/webapp/app/queries/delete-comment.ts @@ -0,0 +1,13 @@ +import gql from 'graphql-tag'; + +export default gql` + mutation CommentDelete($commentId: ID!) { + deleteComment(id: $commentId) { + comment { + id + } + + errors + } + } +`;