From 8c9546ff2aedcb8b539132e8c4cb334a962319ac Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 15 Oct 2020 14:23:55 +0200 Subject: [PATCH] Improve member management Signed-off-by: Thomas Citharel --- js/src/components/Group/Invitations.vue | 3 + js/src/graphql/group.ts | 1 + js/src/i18n/en_US.json | 4 +- js/src/i18n/fr_FR.json | 4 +- js/src/mixins/group.ts | 3 +- js/src/views/Group/Group.vue | 13 +++ js/src/views/Group/GroupMembers.vue | 119 +++++++++++++----------- js/src/views/Group/MyGroups.vue | 2 +- lib/graphql/resolvers/member.ex | 35 ++++++- priv/gettext/fr/LC_MESSAGES/errors.po | 8 +- 10 files changed, 126 insertions(+), 66 deletions(-) diff --git a/js/src/components/Group/Invitations.vue b/js/src/components/Group/Invitations.vue index f11916eca..d999a737b 100644 --- a/js/src/components/Group/Invitations.vue +++ b/js/src/components/Group/Invitations.vue @@ -14,6 +14,7 @@ import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member"; import { IMember } from "@/types/actor"; import { Component, Prop, Vue } from "vue-property-decorator"; import InvitationCard from "@/components/Group/InvitationCard.vue"; +import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor"; @Component({ components: { @@ -30,6 +31,7 @@ export default class Invitations extends Vue { variables: { id, }, + refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }], }); if (data) { this.$emit("accept-invitation", data.acceptInvitation); @@ -49,6 +51,7 @@ export default class Invitations extends Vue { variables: { id, }, + refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }], }); if (data) { this.$emit("reject-invitation", data.rejectInvitation); diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts index e09d34a1d..a3afb9f55 100644 --- a/js/src/graphql/group.ts +++ b/js/src/graphql/group.ts @@ -125,6 +125,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql` } members { elements { + id role actor { id diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 44a48d423..8e5569753 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -803,5 +803,7 @@ "Please read the {fullRules} published by {instance}'s administrators.": "Please read the {fullRules} published by {instance}'s administrators.", "Instances following you": "Instances following you", "Instances you follow": "Instances you follow", - "Last group created": "Last group created" + "Last group created": "Last group created", + "{username} was invited to {group}": "{username} was invited to {group}", + "The member was removed from the group {group}": "The member was removed from the group {group}" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 5ae9009cc..8fc756106 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -853,5 +853,7 @@ "Please read the {fullRules} published by {instance}'s administrators.": "Merci de lire les {fullRules} publiées par les administrateur·ices de {instance}.", "Instances following you": "Instances vous suivant", "Instances you follow": "Instances que vous suivez", - "Last group created": "Dernier groupe créé" + "Last group created": "Dernier groupe créé", + "{username} was invited to {group}": "{username} a été invité à {group}", + "The member was removed from the group {group}": "Le ou la membre a été supprimé·e du groupe {group}" } diff --git a/js/src/mixins/group.ts b/js/src/mixins/group.ts index d5fd8ccdb..0781d06be 100644 --- a/js/src/mixins/group.ts +++ b/js/src/mixins/group.ts @@ -38,6 +38,7 @@ import { Component, Vue } from "vue-property-decorator"; }) export default class GroupMixin extends Vue { group: IGroup = new Group(); + currentActor!: IActor; person!: IPerson; @@ -51,7 +52,7 @@ export default class GroupMixin extends Vue { ); } - handleErrors(errors: any[]) { + handleErrors(errors: any[]): void { if ( errors.some((error) => error.status_code === 404) || errors.some(({ message }) => message.includes("has invalid value $uuid")) diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index cbc85e31e..05ae51bf8 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -23,6 +23,7 @@ v-if="isCurrentActorAnInvitedGroupMember" :invitations="[groupMember]" @acceptInvitation="acceptInvitation" + @reject-invitation="rejectInvitation" /> {{ $t("You have been removed from this group's members.") }} @@ -431,6 +432,16 @@ export default class Group extends mixins(GroupMixin) { } } + rejectInvitation({ id: memberId }: { id: string }): void { + const index = this.person.memberships.elements.findIndex( + (membership) => membership.role === MemberRole.INVITED && membership.id === memberId + ); + if (index > -1) { + this.person.memberships.elements.splice(index, 1); + this.person.memberships.total -= 1; + } + } + async reportGroup(content: string, forward: boolean): Promise { this.isReportModalActive = false; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -611,6 +622,8 @@ div.container { div.address { flex: 1; text-align: right; + justify-content: flex-end; + display: flex; .map-show-button { cursor: pointer; diff --git a/js/src/views/Group/GroupMembers.vue b/js/src/views/Group/GroupMembers.vue index 7aaaeb4a7..e39f6f915 100644 --- a/js/src/views/Group/GroupMembers.vue +++ b/js/src/views/Group/GroupMembers.vue @@ -181,12 +181,28 @@ import { Component, Watch } from "vue-property-decorator"; import GroupMixin from "@/mixins/group"; import { mixins } from "vue-class-component"; +import { FETCH_GROUP } from "@/graphql/group"; import RouteName from "../../router/name"; import { INVITE_MEMBER, GROUP_MEMBERS, REMOVE_MEMBER, UPDATE_MEMBER } from "../../graphql/member"; import { IGroup, usernameWithDomain } from "../../types/actor"; import { IMember, MemberRole } from "../../types/actor/group.model"; -@Component +@Component({ + apollo: { + members: { + query: GROUP_MEMBERS, + variables() { + return { + name: this.$route.params.preferredUsername, + page: 1, + limit: this.MEMBERS_PER_PAGE, + roles: this.roles, + }; + }, + update: (data) => data.group.members, + }, + }, +}) export default class GroupMembers extends mixins(GroupMixin) { loading = true; @@ -221,31 +237,16 @@ export default class GroupMembers extends mixins(GroupMixin) { groupId: this.group.id, targetActorUsername: this.newMemberUsername, }, - update: (store, { data }) => { - if (data == null) return; - const query = { - query: GROUP_MEMBERS, - variables: { - name: this.$route.params.preferredUsername, - page: 1, - limit: this.MEMBERS_PER_PAGE, - roles: this.roles, - }, - }; - const memberData: IMember = data.inviteMember; - const groupData = store.readQuery<{ group: IGroup }>(query); - if (!groupData) return; - const { group } = groupData; - const index = group.members.elements.findIndex((m) => m.actor.id === memberData.actor.id); - if (index === -1) { - group.members.elements.push(memberData); - group.members.total += 1; - } else { - group.members.elements.splice(index, 1, memberData); - } - store.writeQuery({ ...query, data: { group } }); - }, + refetchQueries: [ + { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } }, + ], }); + this.$notifier.success( + this.$t("{username} was invited to {group}", { + username: this.newMemberUsername, + group: this.group.name || usernameWithDomain(this.group), + }) as string + ); this.newMemberUsername = ""; } catch (error) { console.error(error); @@ -283,34 +284,30 @@ export default class GroupMembers extends mixins(GroupMixin) { } async removeMember(memberId: string): Promise { - await this.$apollo.mutate<{ removeMember: IMember }>({ - mutation: REMOVE_MEMBER, - variables: { - groupId: this.group.id, - memberId, - }, - update: (store, { data }) => { - if (data == null) return; - const query = { - query: GROUP_MEMBERS, - variables: { - name: this.$route.params.preferredUsername, - page: 1, - limit: this.MEMBERS_PER_PAGE, - roles: this.roles, - }, - }; - const groupData = store.readQuery<{ group: IGroup }>(query); - if (!groupData) return; - const { group } = groupData; - const index = group.members.elements.findIndex((m) => m.id === memberId); - if (index !== -1) { - group.members.elements.splice(index, 1); - group.members.total -= 1; - store.writeQuery({ ...query, data: { group } }); - } - }, - }); + console.log("removeMember", memberId); + try { + await this.$apollo.mutate<{ removeMember: IMember }>({ + mutation: REMOVE_MEMBER, + variables: { + groupId: this.group.id, + memberId, + }, + refetchQueries: [ + { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } }, + ], + }); + this.$notifier.success( + this.$t("The member was removed from the group {group}", { + username: this.newMemberUsername, + group: this.group.name || usernameWithDomain(this.group), + }) as string + ); + } catch (error) { + console.error(error); + if (error.graphQLErrors && error.graphQLErrors.length > 0) { + this.$notifier.error(error.graphQLErrors[0].message); + } + } } promoteMember(member: IMember): void { @@ -341,7 +338,23 @@ export default class GroupMembers extends mixins(GroupMixin) { memberId, role, }, + refetchQueries: [ + { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } }, + ], }); + let successMessage; + switch (role) { + case MemberRole.MODERATOR: + successMessage = "The member role was updated to moderator"; + break; + case MemberRole.ADMINISTRATOR: + successMessage = "The member role was updated to administrator"; + break; + case MemberRole.MEMBER: + default: + successMessage = "The member role was updated to simple member"; + } + this.$notifier.success(this.$t(successMessage) as string); } catch (error) { console.error(error); if (error.graphQLErrors && error.graphQLErrors.length > 0) { diff --git a/js/src/views/Group/MyGroups.vue b/js/src/views/Group/MyGroups.vue index ae9ee9494..98fc3ee0f 100644 --- a/js/src/views/Group/MyGroups.vue +++ b/js/src/views/Group/MyGroups.vue @@ -71,7 +71,7 @@ import RouteName from "../../router/name"; }; }, }) -export default class MyEvents extends Vue { +export default class MyGroups extends Vue { membershipsPages!: Paginate; RouteName = RouteName; diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index 222269492..19bfdb5b9 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -66,10 +66,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do {:has_rights_to_invite, {:ok, %Member{role: role}}} when role in [:moderator, :administrator, :creator] <- {:has_rights_to_invite, Actors.get_member(actor_id, group_id)}, + target_actor_username <- + target_actor_username |> String.trim() |> String.trim_leading("@"), {:target_actor_username, {:ok, %Actor{id: target_actor_id} = target_actor}} <- {:target_actor_username, ActivityPub.find_or_make_actor_from_nickname(target_actor_username)}, - true <- check_member_not_existant_or_rejected(target_actor_id, group.id), + {:existant, true} <- + {:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)}, {:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do {:ok, member} else @@ -88,6 +91,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do {:has_rights_to_invite, _} -> {:error, dgettext("errors", "You cannot invite to this group")} + {:existant, _} -> + {:error, dgettext("errors", "Profile is already a member of this group")} + + # Remove me ? {:ok, %Member{}} -> {:error, dgettext("errors", "Profile is already a member of this group")} end @@ -115,7 +122,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do def reject_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do with %Actor{id: actor_id} <- Users.get_actor_for_user(user), - %Member{actor: %Actor{id: member_actor_id}} = member <- Actors.get_member(member_id), + {:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <- + {:invitation_exists, Actors.get_member(member_id)}, {:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id}, {:ok, _activity, %Member{} = member} <- ActivityPub.reject( @@ -127,6 +135,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do else {:is_same_actor, false} -> {:error, dgettext("errors", "You can't reject this invitation with this profile.")} + + {:invitation_exists, _} -> + {:error, dgettext("errors", "This invitation doesn't exist.")} end end @@ -158,13 +169,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do context: %{current_user: %User{} = user} }) do with %Actor{id: moderator_id} = moderator <- Users.get_actor_for_user(user), - %Member{} = member <- Actors.get_member(member_id), + %Member{role: role} = member when role != :rejected <- Actors.get_member(member_id), %Actor{type: :Group} = group <- Actors.get_actor(group_id), - {:has_rights_to_invite, {:ok, %Member{role: role}}} + {:has_rights_to_remove, {:ok, %Member{role: role}}} when role in [:moderator, :administrator, :creator] <- - {:has_rights_to_invite, Actors.get_member(moderator_id, group_id)}, + {:has_rights_to_remove, Actors.get_member(moderator_id, group_id)}, {:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do {:ok, member} + else + %Member{role: :rejected} -> + {:error, + dgettext( + "errors", + "This member already has been rejected." + )} + + {:has_rights_to_remove, _} -> + {:error, + dgettext( + "errors", + "You don't have the right to remove this member." + )} end end diff --git a/priv/gettext/fr/LC_MESSAGES/errors.po b/priv/gettext/fr/LC_MESSAGES/errors.po index 110283942..7160ab367 100644 --- a/priv/gettext/fr/LC_MESSAGES/errors.po +++ b/priv/gettext/fr/LC_MESSAGES/errors.po @@ -479,7 +479,7 @@ msgstr "Le profil invité n'existe pas" #, elixir-format #: lib/graphql/resolvers/member.ex:92 msgid "Profile is already a member of this group" -msgstr "Vous êtes déjà membre de ce groupe" +msgstr "Ce profil est déjà membre de ce groupe" #, elixir-format #: lib/graphql/resolvers/post.ex:131 lib/graphql/resolvers/post.ex:171 @@ -549,12 +549,12 @@ msgstr "Membre non trouvé" #, elixir-format #: lib/graphql/resolvers/person.ex:235 msgid "You already have a profile for this user" -msgstr "Vous êtes déjà membre de ce groupe" +msgstr "Vous avez déjà un profil pour cet utilisateur" #, elixir-format #: lib/graphql/resolvers/participant.ex:134 msgid "You are already a participant of this event" -msgstr "Vous êtes déjà membre de ce groupe" +msgstr "Vous êtes déjà un·e participant·e à cet événement" #, elixir-format #: lib/graphql/resolvers/discussion.ex:185 @@ -564,7 +564,7 @@ msgstr "Vous n'êtes pas un membre du groupe dans lequel se fait la discussion" #, elixir-format #: lib/graphql/resolvers/member.ex:86 msgid "You are not a member of this group" -msgstr "Vous êtes déjà membre de ce groupe" +msgstr "Vous n'êtes pas membre de ce groupe" #, elixir-format #: lib/graphql/resolvers/member.ex:143