Allow group admins to moderate new members

Closes #881

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2021-11-12 15:42:52 +01:00
parent ae24fa17d5
commit 6eba531c89
28 changed files with 795 additions and 212 deletions

View File

@@ -148,6 +148,11 @@ export default class GroupActivityItem extends mixins(ActivityMixin) {
case Openness.INVITE_ONLY:
details.push("The group can now only be joined with an invite.");
break;
case Openness.MODERATED:
details.push(
"The group can now be joined by anyone, but new members need to be approved by an administrator."
);
break;
case Openness.OPEN:
details.push("The group can now be joined by anyone.");
break;

View File

@@ -9,13 +9,7 @@
:inline="true"
slot="member"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.object.actor),
})
}}</b
></popover-actor-card
<b> {{ displayName(activity.object.actor) }}</b></popover-actor-card
>
<b slot="member" v-else>{{
subjectParams.member_actor_federated_username
@@ -25,13 +19,7 @@
:inline="true"
slot="profile"
>
<b>
{{
$t("@{username}", {
username: usernameWithDomain(activity.author),
})
}}</b
></popover-actor-card
<b> {{ displayName(activity.author) }}</b></popover-actor-card
></i18n
>
<small class="has-text-grey-dark activity-date">{{
@@ -41,7 +29,7 @@
</div>
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { displayName } from "@/types/actor";
import { ActivityMemberSubject, MemberRole } from "@/types/enums";
import { Component } from "vue-property-decorator";
import RouteName from "../../router/name";
@@ -62,7 +50,7 @@ export const MEMBER_ROLE_VALUE: Record<string, number> = {
},
})
export default class MemberActivityItem extends mixins(ActivityMixin) {
usernameWithDomain = usernameWithDomain;
displayName = displayName;
RouteName = RouteName;
ActivityMemberSubject = ActivityMemberSubject;
@@ -83,6 +71,14 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
return "You added the member {member}.";
}
return "{profile} added the member {member}.";
case ActivityMemberSubject.MEMBER_APPROVED:
if (this.isAuthorCurrentActor) {
return "You approved {member}'s membership.";
}
if (this.isObjectMemberCurrentActor) {
return "Your membership was approved by {profile}.";
}
return "{profile} approved {member}'s membership.";
case ActivityMemberSubject.MEMBER_JOINED:
return "{member} joined the group.";
case ActivityMemberSubject.MEMBER_UPDATED:
@@ -94,6 +90,12 @@ export default class MemberActivityItem extends mixins(ActivityMixin) {
}
return "{profile} updated the member {member}.";
case ActivityMemberSubject.MEMBER_REMOVED:
if (this.subjectParams.member_role === MemberRole.NOT_APPROVED) {
if (this.isAuthorCurrentActor) {
return "You rejected {member}'s membership request.";
}
return "{profile} rejected {member}'s membership request.";
}
if (this.isAuthorCurrentActor) {
return "You excluded member {member}.";
}

View File

@@ -1,57 +1,65 @@
<template>
<div class="media">
<div class="media-content">
<div class="content">
<i18n
tag="p"
path="You have been invited by {invitedBy} to the following group:"
>
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
</i18n>
</div>
<div class="media subfield">
<div class="media-left">
<figure class="image is-48x48" v-if="member.parent.avatar">
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-large" icon="account-group" />
<div class="card">
<div class="card-content media">
<div class="media-content">
<div class="content">
<i18n
tag="p"
path="You have been invited by {invitedBy} to the following group:"
>
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
</i18n>
</div>
<div class="media-content">
<div class="level">
<div class="level-left">
<div class="level-item">
<router-link
:to="{
name: RouteName.GROUP,
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="member.parent.domain">
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
<div class="media subfield">
<div class="media-left">
<figure class="image is-48x48" v-if="member.parent.avatar">
<img class="is-rounded" :src="member.parent.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-large" icon="account-group" />
</div>
<div class="media-content">
<div class="level">
<div class="level-left">
<div class="level-item mr-3">
<router-link
:to="{
name: RouteName.GROUP,
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3 class="is-size-5">{{ member.parent.name }}</h3>
<p class="is-size-7 has-text-grey-dark">
<span v-if="member.parent.domain">
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<b-button type="is-success" @click="$emit('accept', member.id)">
{{ $t("Accept") }}
</b-button>
</div>
<div class="level-item">
<b-button type="is-danger" @click="$emit('reject', member.id)">
{{ $t("Decline") }}
</b-button>
<div class="level-right">
<div class="level-item">
<b-button
type="is-success"
@click="$emit('accept', member.id)"
>
{{ $t("Accept") }}
</b-button>
</div>
<div class="level-item">
<b-button
type="is-danger"
@click="$emit('reject', member.id)"
>
{{ $t("Decline") }}
</b-button>
</div>
</div>
</div>
</div>
@@ -82,4 +90,7 @@ export default class InvitationCard extends Vue {
background: lighten($primary, 40%);
padding: 10px;
}
h3 {
color: $violet-3;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<section v-if="invitations && invitations.length > 0">
<section class="card my-3" v-if="invitations && invitations.length > 0">
<InvitationCard
v-for="member in invitations"
:key="member.id"
@@ -13,8 +13,9 @@
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { Component, Prop, Vue } from "vue-property-decorator";
import InvitationCard from "@/components/Group/InvitationCard.vue";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import { PERSON_STATUS_GROUP } from "@/graphql/actor";
import { IMember } from "@/types/actor/member.model";
import { IGroup, IPerson, usernameWithDomain } from "@/types/actor";
@Component({
components: {
@@ -26,18 +27,25 @@ export default class Invitations extends Vue {
async acceptInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
{
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("accept-invitation", data.acceptInvitation);
}
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries({ data }) {
const profile = data?.acceptInvitation?.actor as IPerson;
const group = data?.acceptInvitation?.parent as IGroup;
if (profile && group) {
return [
{
query: PERSON_STATUS_GROUP,
variables: { id: profile.id, group: usernameWithDomain(group) },
},
];
}
return [];
},
});
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -48,18 +56,25 @@ export default class Invitations extends Vue {
async rejectInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
{
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("reject-invitation", data.rejectInvitation);
}
await this.$apollo.mutate<{ rejectInvitation: IMember }>({
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries({ data }) {
const profile = data?.rejectInvitation?.actor as IPerson;
const group = data?.rejectInvitation?.parent as IGroup;
if (profile && group) {
return [
{
query: PERSON_STATUS_GROUP,
variables: { id: profile.id, group: usernameWithDomain(group) },
},
];
}
return [];
},
});
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {

View File

@@ -37,9 +37,10 @@ export const ACCEPT_INVITATION = gql`
export const REJECT_INVITATION = gql`
mutation RejectInvitation($id: ID!) {
rejectInvitation(id: $id) {
id
...MemberFragment
}
}
${MEMBER_FRAGMENT}
`;
export const GROUP_MEMBERS = gql`
@@ -72,13 +73,22 @@ export const UPDATE_MEMBER = gql`
`;
export const REMOVE_MEMBER = gql`
mutation RemoveMember($groupId: ID!, $memberId: ID!) {
removeMember(groupId: $groupId, memberId: $memberId) {
mutation RemoveMember($memberId: ID!, $exclude: Boolean) {
removeMember(memberId: $memberId, exclude: $exclude) {
id
}
}
`;
export const APPROVE_MEMBER = gql`
mutation ApproveMember($memberId: ID!) {
approveMember(memberId: $memberId) {
...MemberFragment
}
}
${MEMBER_FRAGMENT}
`;
export const JOIN_GROUP = gql`
mutation JoinGroup($groupId: ID!) {
joinGroup(groupId: $groupId) {

View File

@@ -881,6 +881,7 @@
"{member} was invited by {profile}.": "{member} was invited by {profile}.",
"You added the member {member}.": "You added the member {member}.",
"{profile} added the member {member}.": "{profile} added the member {member}.",
"{member} joined the group.": "{member} joined the group.",
"{member} rejected the invitation to join the group.": "{member} rejected the invitation to join the group.",
"{member} accepted the invitation to join the group.": "{member} accepted the invitation to join the group.",
"You excluded member {member}.": "You excluded member {member}.",
@@ -1233,5 +1234,17 @@
"Any type": "Any type",
"In person": "In person",
"In the past": "In the past",
"Only registered users may fetch remote events from their URL.": "Only registered users may fetch remote events from their URL."
"Only registered users may fetch remote events from their URL.": "Only registered users may fetch remote events from their URL.",
"Moderate new members": "Moderate new members",
"Anyone can request being a member, but an administrator needs to approve the membership.": "Anyone can request being a member, but an administrator needs to approve the membership.",
"Cancel membership request": "Cancel membership request",
"group's upcoming public events": "group's upcoming public events",
"access to the group's private content as well": "access to the group's private content as well",
"Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.": "Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.",
"The group can now be joined by anyone, but new members need to be approved by an administrator.": "The group can now be joined by anyone, but new members need to be approved by an administrator.",
"You approved {member}'s membership.": "You approved {member}'s membership.",
"Your membership was approved by {profile}.": "Your membership was approved by {profile}.",
"{profile} approved {member}'s membership.": "{profile} approved {member}'s membership.",
"You rejected {member}'s membership request.": "You rejected {member}'s membership request.",
"{profile} rejected {member}'s membership request.": "{profile} rejected {member}'s membership request."
}

View File

@@ -1077,7 +1077,7 @@
"You excluded member {member}.": "Vous avez exclu le ou la membre {member}.",
"You have been disconnected": "Vous avez été déconnecté⋅e",
"You have been invited by {invitedBy} to the following group:": "Vous avez été invité par {invitedBy} à rejoindre le groupe suivant :",
"You have been removed from this group's members.": "Vous avez été exclu des membres de ce groupe.",
"You have been removed from this group's members.": "Vous avez été exclu⋅e des membres de ce groupe.",
"You have cancelled your participation": "Vous avez annulé votre participation",
"You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
"You have one event today.": "Vous n'avez pas d'événement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
@@ -1231,6 +1231,7 @@
"{old_group_name} was renamed to {group}.": "{old_group_name} a été renommé en {group}.",
"{profile} (by default)": "{profile} (par défault)",
"{profile} added the member {member}.": "{profile} a ajouté le ou la membre {member}.",
"{member} joined the group.": "{member} a rejoint le groupe.",
"{profile} archived the discussion {discussion}.": "{profile} a archivé la discussion {discussion}.",
"{profile} created the discussion {discussion}.": "{profile} a créé la discussion {discussion}.",
"{profile} created the folder {resource}.": "{profile} a créé le dossier {resource}.",
@@ -1337,5 +1338,17 @@
"Any type": "N'importe quel type",
"In person": "En personne",
"In the past": "Dans le passé",
"Only registered users may fetch remote events from their URL.": "Seul⋅es les utilisateur⋅ices enregistré⋅es peuvent récupérer des événements depuis leur URL."
"Only registered users may fetch remote events from their URL.": "Seul⋅es les utilisateur⋅ices enregistré⋅es peuvent récupérer des événements depuis leur URL.",
"Moderate new members": "Modérer les nouvelles et nouveaux membres",
"Anyone can request being a member, but an administrator needs to approve the membership.": "N'importe qui peut demander à être membre, mais un⋅e administrateur⋅ice devra approuver leur adhésion.",
"Cancel membership request": "Annuler la demande d'adhésion",
"group's upcoming public events": "prochains événements publics du groupe",
"access to the group's private content as well": "accédez également au contenu privé du groupe",
"Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.": "Suivre le groupe vous permettra d'être informé⋅e des {group_upcoming_public_events}, alors que rejoindre le groupe signfie que vous {access_to_group_private_content_as_well}, y compris les discussion de groupe, les resources du groupe et les billets réservés au groupe.",
"The group can now be joined by anyone, but new members need to be approved by an administrator.": "Le groupe peut maintenant être rejoint par n'importe qui, mais les nouvelles et nouveaux membres doivent être approuvées par un⋅e modérateur⋅ice.",
"You approved {member}'s membership.": "Vous avez approuvé la demande d'adhésion de {member}.",
"Your membership was approved by {profile}.": "Votre demande d'adhésion a été approuvée par {profile}.",
"{profile} approved {member}'s membership.": "{profile} a approuvé la demande d'adhésion de {member}.",
"You rejected {member}'s membership request.": "Vous avez rejeté la demande d'adhésion de {member}.",
"{profile} rejected {member}'s membership request.": "{profile} a rejeté la demande d'adhésion de {member}."
}

View File

@@ -99,6 +99,10 @@ export default class GroupMixin extends Vue {
]);
}
get isCurrentActorAPendingGroupMember(): boolean {
return this.hasCurrentActorThisRole([MemberRole.NOT_APPROVED]);
}
hasCurrentActorThisRole(givenRole: string | string[]): boolean {
const roles = Array.isArray(givenRole) ? givenRole : [givenRole];
return (

View File

@@ -23,29 +23,6 @@
</ul>
</nav>
<b-loading :active.sync="$apollo.loading"></b-loading>
<invitations
v-if="isCurrentActorAnInvitedGroupMember"
:invitations="[groupMember]"
@acceptInvitation="acceptInvitation"
@reject-invitation="rejectInvitation"
/>
<b-message v-if="isCurrentActorARejectedGroupMember" type="is-danger">
{{ $t("You have been removed from this group's members.") }}
</b-message>
<b-message
v-if="
isCurrentActorAGroupMember &&
isCurrentActorARecentMember &&
isCurrentActorOnADifferentDomainThanGroup
"
type="is-info"
>
{{
$t(
"Since you are a new member, private content can take a few minutes to appear."
)
}}
</b-message>
<header class="block-container presentation" v-if="group">
<div class="banner-container">
<lazy-image-wrapper :picture="group.banner" />
@@ -137,7 +114,7 @@
<b-tooltip
v-if="
(!isCurrentActorAGroupMember || previewPublic) &&
group.openness !== Openness.OPEN
group.openness === Openness.INVITE_ONLY
"
:label="$t('This group is invite-only')"
position="is-bottom"
@@ -148,7 +125,9 @@
>
<b-button
v-else-if="
(!isCurrentActorAGroupMember || previewPublic) &&
((!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember) ||
previewPublic) &&
currentActor.id
"
@click="joinGroup"
@@ -157,6 +136,14 @@
:disabled="previewPublic"
>{{ $t("Join group") }}</b-button
>
<b-button
outlined
v-else-if="isCurrentActorAPendingGroupMember"
@click="leaveGroup"
@keyup.enter="leaveGroup"
type="is-primary"
>{{ $t("Cancel membership request") }}</b-button
>
<b-button
tag="router-link"
:to="{
@@ -310,6 +297,49 @@
</b-dropdown>
</div>
</div>
<invitations
v-if="isCurrentActorAnInvitedGroupMember"
:invitations="[groupMember]"
/>
<b-message v-if="isCurrentActorARejectedGroupMember" type="is-danger">
{{ $t("You have been removed from this group's members.") }}
</b-message>
<b-message
v-if="
isCurrentActorAGroupMember &&
isCurrentActorARecentMember &&
isCurrentActorOnADifferentDomainThanGroup
"
type="is-info"
>
{{
$t(
"Since you are a new member, private content can take a few minutes to appear."
)
}}
</b-message>
<b-message
v-if="
!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember &&
!isCurrentActorPendingFollow &&
!isCurrentActorFollowing
"
type="is-info"
has-icon
class="m-3"
>
<i18n
path="Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts."
>
<b slot="group_upcoming_public_events">{{
$t("group's upcoming public events")
}}</b>
<b slot="access_to_group_private_content_as_well">{{
$t("access to the group's private content as well")
}}</b>
</i18n>
</b-message>
</div>
</header>
</div>
@@ -893,31 +923,6 @@ export default class Group extends mixins(GroupMixin) {
});
}
acceptInvitation(): void {
if (this.groupMember) {
const index = this.person.memberships.elements.findIndex(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
({ id }: IMember) => id === this.groupMember.id
);
const member = this.groupMember;
member.role = MemberRole.MEMBER;
this.person.memberships.elements.splice(index, 1, member);
this.$apollo.queries.group.refetch();
}
}
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> {
this.isReportModalActive = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -195,6 +195,20 @@
</b-table-column>
<b-table-column field="actions" :label="$t('Actions')" v-slot="props">
<div class="buttons" v-if="props.row.actor.id !== currentActor.id">
<b-button
type="is-success"
v-if="props.row.role === MemberRole.NOT_APPROVED"
@click="approveMember(props.row)"
icon-left="check"
>{{ $t("Approve member") }}</b-button
>
<b-button
type="is-danger"
v-if="props.row.role === MemberRole.NOT_APPROVED"
@click="rejectMember(props.row)"
icon-left="exit-to-app"
>{{ $t("Reject member") }}</b-button
>
<b-button
v-if="
[MemberRole.MEMBER, MemberRole.MODERATOR].includes(
@@ -217,7 +231,7 @@
>
<b-button
v-if="props.row.role === MemberRole.MEMBER"
@click="removeMember(props.row.id)"
@click="removeMember(props.row)"
type="is-danger"
icon-left="exit-to-app"
>{{ $t("Remove") }}</b-button
@@ -250,8 +264,9 @@ import {
GROUP_MEMBERS,
REMOVE_MEMBER,
UPDATE_MEMBER,
APPROVE_MEMBER,
} from "../../graphql/member";
import { usernameWithDomain } from "../../types/actor";
import { usernameWithDomain, displayName } from "../../types/actor";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
@Component({
@@ -332,7 +347,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
this.$notifier.success(
this.$t("{username} was invited to {group}", {
username: this.newMemberUsername,
group: this.group.name || usernameWithDomain(this.group),
group: displayName(this.group),
}) as string
);
this.newMemberUsername = "";
@@ -375,7 +390,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
});
}
async removeMember(memberId: string): Promise<void> {
async removeMember(oldMember: IMember): Promise<void> {
const { roles, MEMBERS_PER_PAGE, group, page } = this;
const variables = {
name: usernameWithDomain(group),
@@ -388,7 +403,7 @@ export default class GroupMembers extends mixins(GroupMixin) {
mutation: REMOVE_MEMBER,
variables: {
groupId: this.group.id,
memberId,
memberId: oldMember.id,
},
refetchQueries: [
{
@@ -397,12 +412,18 @@ export default class GroupMembers extends mixins(GroupMixin) {
},
],
});
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
);
let message = this.$t("The member was removed from the group {group}", {
group: displayName(this.group),
}) as string;
if (oldMember.role === MemberRole.NOT_APPROVED) {
message = this.$t(
"The membership request from {profile} was rejected",
{
group: displayName(oldMember.actor),
}
) as string;
}
this.$notifier.success(message);
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -414,29 +435,49 @@ export default class GroupMembers extends mixins(GroupMixin) {
promoteMember(member: IMember): void {
if (!member.id) return;
if (member.role === MemberRole.MODERATOR) {
this.updateMember(member.id, MemberRole.ADMINISTRATOR);
this.updateMember(member, MemberRole.ADMINISTRATOR);
}
if (member.role === MemberRole.MEMBER) {
this.updateMember(member.id, MemberRole.MODERATOR);
this.updateMember(member, MemberRole.MODERATOR);
}
}
demoteMember(member: IMember): void {
if (!member.id) return;
if (member.role === MemberRole.MODERATOR) {
this.updateMember(member.id, MemberRole.MEMBER);
this.updateMember(member, MemberRole.MEMBER);
}
if (member.role === MemberRole.ADMINISTRATOR) {
this.updateMember(member.id, MemberRole.MODERATOR);
this.updateMember(member, MemberRole.MODERATOR);
}
}
async updateMember(memberId: string, role: MemberRole): Promise<void> {
async approveMember(member: IMember): Promise<void> {
try {
await this.$apollo.mutate<{ approveMember: IMember }>({
mutation: APPROVE_MEMBER,
variables: { memberId: member.id },
});
this.$notifier.success(this.$t("The member was approved") as string);
} catch (error: any) {
console.error(error);
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
}
rejectMember(member: IMember): void {
if (!member.id) return;
this.removeMember(member);
}
async updateMember(oldMember: IMember, role: MemberRole): Promise<void> {
try {
await this.$apollo.mutate<{ updateMember: IMember }>({
mutation: UPDATE_MEMBER,
variables: {
memberId,
memberId: oldMember.id,
role,
},
refetchQueries: [
@@ -455,8 +496,14 @@ export default class GroupMembers extends mixins(GroupMixin) {
successMessage = "The member role was updated to administrator";
break;
case MemberRole.MEMBER:
if (oldMember.role === MemberRole.NOT_APPROVED) {
successMessage = "The member was approved";
} else {
successMessage = "The member role was updated to simple member";
}
break;
default:
successMessage = "The member role was updated to simple member";
successMessage = "The member role was updated";
}
this.$notifier.success(this.$t(successMessage) as string);
} catch (error: any) {

View File

@@ -128,6 +128,19 @@
}}</small>
</b-radio>
</div>
<div class="field">
<b-radio
v-model="editableGroup.openness"
name="groupOpenness"
:native-value="Openness.MODERATED"
>{{ $t("Moderate new members") }}<br />
<small>{{
$t(
"Anyone can request being a member, but an administrator needs to approve the membership."
)
}}</small>
</b-radio>
</div>
<div class="field">
<b-radio
v-model="editableGroup.openness"