Improve member management

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-10-15 14:23:55 +02:00
parent 3be91d7e6c
commit 8c9546ff2a
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
10 changed files with 126 additions and 66 deletions

View File

@ -14,6 +14,7 @@ import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { IMember } from "@/types/actor"; import { IMember } from "@/types/actor";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import InvitationCard from "@/components/Group/InvitationCard.vue"; import InvitationCard from "@/components/Group/InvitationCard.vue";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
@Component({ @Component({
components: { components: {
@ -30,6 +31,7 @@ export default class Invitations extends Vue {
variables: { variables: {
id, id,
}, },
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}); });
if (data) { if (data) {
this.$emit("accept-invitation", data.acceptInvitation); this.$emit("accept-invitation", data.acceptInvitation);
@ -49,6 +51,7 @@ export default class Invitations extends Vue {
variables: { variables: {
id, id,
}, },
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}); });
if (data) { if (data) {
this.$emit("reject-invitation", data.rejectInvitation); this.$emit("reject-invitation", data.rejectInvitation);

View File

@ -125,6 +125,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
} }
members { members {
elements { elements {
id
role role
actor { actor {
id id

View File

@ -803,5 +803,7 @@
"Please read the {fullRules} published by {instance}'s administrators.": "Please read the {fullRules} published by {instance}'s administrators.", "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 following you": "Instances following you",
"Instances you follow": "Instances you follow", "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}"
} }

View File

@ -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}.", "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 following you": "Instances vous suivant",
"Instances you follow": "Instances que vous suivez", "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}"
} }

View File

@ -38,6 +38,7 @@ import { Component, Vue } from "vue-property-decorator";
}) })
export default class GroupMixin extends Vue { export default class GroupMixin extends Vue {
group: IGroup = new Group(); group: IGroup = new Group();
currentActor!: IActor; currentActor!: IActor;
person!: IPerson; person!: IPerson;
@ -51,7 +52,7 @@ export default class GroupMixin extends Vue {
); );
} }
handleErrors(errors: any[]) { handleErrors(errors: any[]): void {
if ( if (
errors.some((error) => error.status_code === 404) || errors.some((error) => error.status_code === 404) ||
errors.some(({ message }) => message.includes("has invalid value $uuid")) errors.some(({ message }) => message.includes("has invalid value $uuid"))

View File

@ -23,6 +23,7 @@
v-if="isCurrentActorAnInvitedGroupMember" v-if="isCurrentActorAnInvitedGroupMember"
:invitations="[groupMember]" :invitations="[groupMember]"
@acceptInvitation="acceptInvitation" @acceptInvitation="acceptInvitation"
@reject-invitation="rejectInvitation"
/> />
<b-message v-if="isCurrentActorARejectedGroupMember" type="is-danger"> <b-message v-if="isCurrentActorARejectedGroupMember" type="is-danger">
{{ $t("You have been removed from this group's members.") }} {{ $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<void> { async reportGroup(content: string, forward: boolean): Promise<void> {
this.isReportModalActive = false; this.isReportModalActive = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -611,6 +622,8 @@ div.container {
div.address { div.address {
flex: 1; flex: 1;
text-align: right; text-align: right;
justify-content: flex-end;
display: flex;
.map-show-button { .map-show-button {
cursor: pointer; cursor: pointer;

View File

@ -181,12 +181,28 @@
import { Component, Watch } from "vue-property-decorator"; import { Component, Watch } from "vue-property-decorator";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { mixins } from "vue-class-component"; import { mixins } from "vue-class-component";
import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { INVITE_MEMBER, GROUP_MEMBERS, REMOVE_MEMBER, UPDATE_MEMBER } from "../../graphql/member"; import { INVITE_MEMBER, GROUP_MEMBERS, REMOVE_MEMBER, UPDATE_MEMBER } from "../../graphql/member";
import { IGroup, usernameWithDomain } from "../../types/actor"; import { IGroup, usernameWithDomain } from "../../types/actor";
import { IMember, MemberRole } from "../../types/actor/group.model"; 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) { export default class GroupMembers extends mixins(GroupMixin) {
loading = true; loading = true;
@ -221,31 +237,16 @@ export default class GroupMembers extends mixins(GroupMixin) {
groupId: this.group.id, groupId: this.group.id,
targetActorUsername: this.newMemberUsername, targetActorUsername: this.newMemberUsername,
}, },
update: (store, { data }) => { refetchQueries: [
if (data == null) return; { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } },
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 } });
},
}); });
this.$notifier.success(
this.$t("{username} was invited to {group}", {
username: this.newMemberUsername,
group: this.group.name || usernameWithDomain(this.group),
}) as string
);
this.newMemberUsername = ""; this.newMemberUsername = "";
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -283,34 +284,30 @@ export default class GroupMembers extends mixins(GroupMixin) {
} }
async removeMember(memberId: string): Promise<void> { async removeMember(memberId: string): Promise<void> {
await this.$apollo.mutate<{ removeMember: IMember }>({ console.log("removeMember", memberId);
mutation: REMOVE_MEMBER, try {
variables: { await this.$apollo.mutate<{ removeMember: IMember }>({
groupId: this.group.id, mutation: REMOVE_MEMBER,
memberId, variables: {
}, groupId: this.group.id,
update: (store, { data }) => { memberId,
if (data == null) return; },
const query = { refetchQueries: [
query: GROUP_MEMBERS, { query: FETCH_GROUP, variables: { name: this.$route.params.preferredUsername } },
variables: { ],
name: this.$route.params.preferredUsername, });
page: 1, this.$notifier.success(
limit: this.MEMBERS_PER_PAGE, this.$t("The member was removed from the group {group}", {
roles: this.roles, username: this.newMemberUsername,
}, group: this.group.name || usernameWithDomain(this.group),
}; }) as string
const groupData = store.readQuery<{ group: IGroup }>(query); );
if (!groupData) return; } catch (error) {
const { group } = groupData; console.error(error);
const index = group.members.elements.findIndex((m) => m.id === memberId); if (error.graphQLErrors && error.graphQLErrors.length > 0) {
if (index !== -1) { this.$notifier.error(error.graphQLErrors[0].message);
group.members.elements.splice(index, 1); }
group.members.total -= 1; }
store.writeQuery({ ...query, data: { group } });
}
},
});
} }
promoteMember(member: IMember): void { promoteMember(member: IMember): void {
@ -341,7 +338,23 @@ export default class GroupMembers extends mixins(GroupMixin) {
memberId, memberId,
role, 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) { } catch (error) {
console.error(error); console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {

View File

@ -71,7 +71,7 @@ import RouteName from "../../router/name";
}; };
}, },
}) })
export default class MyEvents extends Vue { export default class MyGroups extends Vue {
membershipsPages!: Paginate<IMember>; membershipsPages!: Paginate<IMember>;
RouteName = RouteName; RouteName = RouteName;

View File

@ -66,10 +66,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
{:has_rights_to_invite, {:ok, %Member{role: role}}} {:has_rights_to_invite, {:ok, %Member{role: role}}}
when role in [:moderator, :administrator, :creator] <- when role in [:moderator, :administrator, :creator] <-
{:has_rights_to_invite, Actors.get_member(actor_id, group_id)}, {: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, {:ok, %Actor{id: target_actor_id} = target_actor}} <-
{:target_actor_username, {:target_actor_username,
ActivityPub.find_or_make_actor_from_nickname(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, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do
{:ok, member} {:ok, member}
else else
@ -88,6 +91,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
{:has_rights_to_invite, _} -> {:has_rights_to_invite, _} ->
{:error, dgettext("errors", "You cannot invite to this group")} {: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{}} -> {:ok, %Member{}} ->
{:error, dgettext("errors", "Profile is already a member of this group")} {:error, dgettext("errors", "Profile is already a member of this group")}
end end
@ -115,7 +122,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
def reject_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) 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), 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}, {:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id},
{:ok, _activity, %Member{} = member} <- {:ok, _activity, %Member{} = member} <-
ActivityPub.reject( ActivityPub.reject(
@ -127,6 +135,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
else else
{:is_same_actor, false} -> {:is_same_actor, false} ->
{:error, dgettext("errors", "You can't reject this invitation with this profile.")} {:error, dgettext("errors", "You can't reject this invitation with this profile.")}
{:invitation_exists, _} ->
{:error, dgettext("errors", "This invitation doesn't exist.")}
end end
end end
@ -158,13 +169,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
context: %{current_user: %User{} = user} context: %{current_user: %User{} = user}
}) do }) do
with %Actor{id: moderator_id} = moderator <- Users.get_actor_for_user(user), 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), %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] <- 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, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do
{:ok, member} {: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
end end

View File

@ -479,7 +479,7 @@ msgstr "Le profil invité n'existe pas"
#, elixir-format #, elixir-format
#: lib/graphql/resolvers/member.ex:92 #: lib/graphql/resolvers/member.ex:92
msgid "Profile is already a member of this group" 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 #, elixir-format
#: lib/graphql/resolvers/post.ex:131 lib/graphql/resolvers/post.ex:171 #: lib/graphql/resolvers/post.ex:131 lib/graphql/resolvers/post.ex:171
@ -549,12 +549,12 @@ msgstr "Membre non trouvé"
#, elixir-format #, elixir-format
#: lib/graphql/resolvers/person.ex:235 #: lib/graphql/resolvers/person.ex:235
msgid "You already have a profile for this user" 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 #, elixir-format
#: lib/graphql/resolvers/participant.ex:134 #: lib/graphql/resolvers/participant.ex:134
msgid "You are already a participant of this event" 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 #, elixir-format
#: lib/graphql/resolvers/discussion.ex:185 #: 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 #, elixir-format
#: lib/graphql/resolvers/member.ex:86 #: lib/graphql/resolvers/member.ex:86
msgid "You are not a member of this group" 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 #, elixir-format
#: lib/graphql/resolvers/member.ex:143 #: lib/graphql/resolvers/member.ex:143