Improve and activate groups
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { Component, Watch } from "vue-property-decorator";
|
||||
import { Group, IPerson, usernameWithDomain, MemberRole } from "@/types/actor";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { CREATE_GROUP } from "@/graphql/group";
|
||||
@@ -84,7 +84,7 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
async createGroup() {
|
||||
async createGroup(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_GROUP,
|
||||
@@ -125,6 +125,7 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get host(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
@@ -152,10 +153,12 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
|
||||
if (this.bannerFile) {
|
||||
bannerObj = {
|
||||
picture: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
banner: {
|
||||
picture: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -173,18 +176,7 @@ export default class CreateGroup extends mixins(IdentityEditionMixin) {
|
||||
}
|
||||
|
||||
private handleError(err: any) {
|
||||
this.errors.push(
|
||||
...err.graphQLErrors.map(({ message }: { message: string }) => this.convertMessage(message))
|
||||
);
|
||||
}
|
||||
|
||||
private convertMessage(message: string): string {
|
||||
switch (message) {
|
||||
case "A group with this name already exists":
|
||||
return this.$t("A group with this name already exists") as string;
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
this.errors.push(...err.graphQLErrors.map(({ message }: { message: string }) => message));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<header class="block-container presentation">
|
||||
<div class="block-column media">
|
||||
<div class="media-left">
|
||||
<figure class="image rounded is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" />
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img class="is-rounded" :src="group.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
@@ -56,13 +56,6 @@
|
||||
class="button is-outlined"
|
||||
>{{ $t("Group settings") }}</router-link
|
||||
>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
v-if="isCurrentActorAGroupMember"
|
||||
outlined
|
||||
@click="leaveGroup"
|
||||
>{{ $t("Leave group") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,6 +107,7 @@
|
||||
>{{ $t("Show map") }}</span
|
||||
>
|
||||
</div>
|
||||
<img v-if="group.banner && group.banner.url" :src="group.banner.url" alt="" />
|
||||
</header>
|
||||
</div>
|
||||
<div v-if="isCurrentActorAGroupMember" class="block-container">
|
||||
@@ -186,50 +180,6 @@
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
<!-- Todos -->
|
||||
<group-section
|
||||
:title="$t('Ongoing tasks')"
|
||||
icon="checkbox-multiple-marked"
|
||||
:route="{
|
||||
name: RouteName.TODO_LISTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<div v-if="group.todoLists.elements.length > 0">
|
||||
<div v-for="todoList in group.todoLists.elements" :key="todoList.id">
|
||||
<router-link :to="{ name: RouteName.TODO_LIST, params: { id: todoList.id } }">
|
||||
<h2 class="is-size-3">
|
||||
{{
|
||||
$tc("{title} ({count} todos)", todoList.todos.total, {
|
||||
count: todoList.todos.total,
|
||||
title: todoList.title,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
</router-link>
|
||||
<compact-todo
|
||||
:todo="todo"
|
||||
v-for="todo in todoList.todos.elements.slice(0, 3)"
|
||||
:key="todo.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="content has-text-grey has-text-centered">
|
||||
<p>{{ $t("No ongoing todos") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:create>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.TODO_LISTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
class="button is-primary"
|
||||
>{{ $t("+ Add a todo") }}</router-link
|
||||
>
|
||||
</template>
|
||||
</group-section>
|
||||
</div>
|
||||
<!-- Public things -->
|
||||
<div class="block-column">
|
||||
@@ -349,7 +299,7 @@
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { FETCH_GROUP, LEAVE_GROUP } from "@/graphql/group";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import {
|
||||
IActor,
|
||||
IGroup,
|
||||
@@ -369,7 +319,6 @@ import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import { Address } from "@/types/address.model";
|
||||
import Invitations from "@/components/Group/Invitations.vue";
|
||||
import addMinutes from "date-fns/addMinutes";
|
||||
import { Route } from "vue-router";
|
||||
import GroupSection from "../../components/Group/GroupSection.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@@ -451,16 +400,6 @@ export default class Group extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async leaveGroup(): Promise<Route> {
|
||||
await this.$apollo.mutate({
|
||||
mutation: LEAVE_GROUP,
|
||||
variables: {
|
||||
groupId: this.group.id,
|
||||
},
|
||||
});
|
||||
return this.$router.push({ name: RouteName.MY_GROUPS });
|
||||
}
|
||||
|
||||
acceptInvitation(): void {
|
||||
if (this.groupMember) {
|
||||
const index = this.person.memberships.elements.findIndex(
|
||||
@@ -477,7 +416,7 @@ export default class Group extends Vue {
|
||||
|
||||
get groupTitle(): undefined | string {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.preferredUsername;
|
||||
return this.group.name || this.group.preferredUsername;
|
||||
}
|
||||
|
||||
get groupSummary(): undefined | string {
|
||||
@@ -583,6 +522,8 @@ div.container {
|
||||
&.presentation {
|
||||
border: 2px solid $purple-2;
|
||||
padding: 10px 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
color: $purple-1;
|
||||
@@ -593,6 +534,20 @@ div.container {
|
||||
.button.is-outlined {
|
||||
border-color: $purple-2;
|
||||
}
|
||||
|
||||
& > *:not(img) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
& > img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.members {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<section class="container section">
|
||||
<h1>{{ $t("Group List") }} ({{ groups.total }})</h1>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div class="columns">
|
||||
<GroupMemberCard
|
||||
v-for="group in groups.elements"
|
||||
:key="group.uuid"
|
||||
:group="group"
|
||||
class="column is-one-quarter-desktop is-half-mobile"
|
||||
/>
|
||||
</div>
|
||||
<router-link class="button" :to="{ name: RouteName.CREATE_GROUP }">{{
|
||||
$t("Create group")
|
||||
}}</router-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LIST_GROUPS } from "@/graphql/group";
|
||||
import { Group, IGroup } from "@/types/actor";
|
||||
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
groups: {
|
||||
query: {
|
||||
query: LIST_GROUPS,
|
||||
fetchPolicy: "network-only",
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GroupMemberCard,
|
||||
},
|
||||
})
|
||||
export default class GroupList extends Vue {
|
||||
groups!: Paginate<IGroup>;
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
//
|
||||
// usernameWithDomain(actor) {
|
||||
// return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
// }
|
||||
|
||||
// viewActor(actor) {
|
||||
// this.$router.push({
|
||||
// name: RouteName.GROUP,
|
||||
// params: { name: this.usernameWithDomain(actor) },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// joinGroup(group) {
|
||||
// const router = this.$router;
|
||||
// // FIXME: remove eventFetch
|
||||
// // eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' })
|
||||
// // .then(response => response.json())
|
||||
// // .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } }));
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
@@ -210,14 +210,14 @@ export default class GroupMembers extends Vue {
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
mounted() {
|
||||
mounted(): void {
|
||||
const roleQuery = this.$route.query.role as string;
|
||||
if (Object.values(MemberRole).includes(roleQuery as MemberRole)) {
|
||||
this.roles = roleQuery as MemberRole;
|
||||
}
|
||||
}
|
||||
|
||||
async inviteMember() {
|
||||
async inviteMember(): Promise<void> {
|
||||
await this.$apollo.mutate<{ inviteMember: IMember }>({
|
||||
mutation: INVITE_MEMBER,
|
||||
variables: {
|
||||
@@ -253,7 +253,7 @@ export default class GroupMembers extends Vue {
|
||||
}
|
||||
|
||||
@Watch("page")
|
||||
loadMoreMembers() {
|
||||
loadMoreMembers(): void {
|
||||
this.$apollo.queries.event.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
@@ -279,7 +279,7 @@ export default class GroupMembers extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async removeMember(memberId: string) {
|
||||
async removeMember(memberId: string): Promise<void> {
|
||||
await this.$apollo.mutate<{ removeMember: IMember }>({
|
||||
mutation: REMOVE_MEMBER,
|
||||
variables: {
|
||||
@@ -310,15 +310,15 @@ export default class GroupMembers extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
promoteMember(memberId: string) {
|
||||
promoteMember(memberId: string): Promise<void> {
|
||||
return this.updateMember(memberId, MemberRole.ADMINISTRATOR);
|
||||
}
|
||||
|
||||
demoteMember(memberId: string) {
|
||||
demoteMember(memberId: string): Promise<void> {
|
||||
return this.updateMember(memberId, MemberRole.MEMBER);
|
||||
}
|
||||
|
||||
async updateMember(memberId: string, role: MemberRole) {
|
||||
async updateMember(memberId: string, role: MemberRole): Promise<void> {
|
||||
await this.$apollo.mutate<{ updateMember: IMember }>({
|
||||
mutation: UPDATE_MEMBER,
|
||||
variables: {
|
||||
|
||||
@@ -37,8 +37,23 @@
|
||||
<b-input v-model="group.name" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Group short description')">
|
||||
<editor mode="basic" v-model="group.summary"
|
||||
<editor mode="basic" v-model="group.summary" :maxSize="500"
|
||||
/></b-field>
|
||||
<b-field :label="$t('Avatar')">
|
||||
<picture-upload
|
||||
:textFallback="$t('Avatar')"
|
||||
v-model="avatarFile"
|
||||
:defaultImageSrc="group.avatar ? group.avatar.url : null"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Banner')">
|
||||
<picture-upload
|
||||
:textFallback="$t('Banner')"
|
||||
v-model="bannerFile"
|
||||
:defaultImageSrc="group.banner ? group.banner.url : null"
|
||||
/>
|
||||
</b-field>
|
||||
<p class="label">{{ $t("Group visibility") }}</p>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
@@ -106,6 +121,7 @@
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
|
||||
import { Route } from "vue-router";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { FETCH_GROUP, UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
|
||||
import { IGroup, usernameWithDomain } from "../../types/actor";
|
||||
@@ -129,6 +145,7 @@ import { Group } from "../../types/actor/group.model";
|
||||
},
|
||||
components: {
|
||||
FullAddressAutoComplete,
|
||||
PictureUpload,
|
||||
editor: () => import("../../components/Editor.vue"),
|
||||
},
|
||||
})
|
||||
@@ -141,6 +158,10 @@ export default class GroupSettings extends Vue {
|
||||
|
||||
newMemberUsername = "";
|
||||
|
||||
avatarFile: File | null = null;
|
||||
|
||||
bannerFile: File | null = null;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
GroupVisibility = {
|
||||
@@ -151,19 +172,12 @@ export default class GroupSettings extends Vue {
|
||||
showCopiedTooltip = false;
|
||||
|
||||
async updateGroup(): Promise<void> {
|
||||
const variables = { ...this.group };
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.__typename;
|
||||
if (variables.physicalAddress) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.physicalAddress.__typename;
|
||||
}
|
||||
const variables = this.buildVariables();
|
||||
await this.$apollo.mutate<{ updateGroup: IGroup }>({
|
||||
mutation: UPDATE_GROUP,
|
||||
variables,
|
||||
});
|
||||
this.$notifier.success(this.$t("Group settings saved") as string);
|
||||
}
|
||||
|
||||
confirmDeleteGroup(): void {
|
||||
@@ -198,6 +212,52 @@ export default class GroupSettings extends Vue {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private buildVariables() {
|
||||
let avatarObj = {};
|
||||
let bannerObj = {};
|
||||
const variables = { ...this.group };
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.__typename;
|
||||
if (variables.physicalAddress) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete variables.physicalAddress.__typename;
|
||||
}
|
||||
delete variables.avatar;
|
||||
delete variables.banner;
|
||||
|
||||
if (this.avatarFile) {
|
||||
avatarObj = {
|
||||
avatar: {
|
||||
picture: {
|
||||
name: this.avatarFile.name,
|
||||
alt: `${this.group.preferredUsername}'s avatar`,
|
||||
file: this.avatarFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (this.bannerFile) {
|
||||
bannerObj = {
|
||||
banner: {
|
||||
picture: {
|
||||
name: this.bannerFile.name,
|
||||
alt: `${this.group.preferredUsername}'s banner`,
|
||||
file: this.bannerFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...variables,
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get canShowCopyButton(): boolean {
|
||||
return window.isSecureContext;
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t("Create group") }}</router-link>
|
||||
<div class="buttons">
|
||||
<router-link class="button is-primary" :to="{ name: RouteName.CREATE_GROUP }">{{
|
||||
$t("Create group")
|
||||
}}</router-link>
|
||||
</div>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<invitations
|
||||
:invitations="invitations"
|
||||
@@ -16,7 +20,13 @@
|
||||
@rejectInvitation="rejectInvitation"
|
||||
/>
|
||||
<section v-if="memberships && memberships.length > 0">
|
||||
<GroupMemberCard v-for="member in memberships" :key="member.id" :member="member" />
|
||||
<GroupMemberCard
|
||||
class="group-member-card"
|
||||
v-for="member in memberships"
|
||||
:key="member.id"
|
||||
:member="member"
|
||||
@leave="leaveGroup(member.parent)"
|
||||
/>
|
||||
</section>
|
||||
<b-message v-if="$apollo.loading === false && memberships.length === 0" type="is-danger">
|
||||
{{ $t("No groups found") }}
|
||||
@@ -27,10 +37,13 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { LEAVE_GROUP } from "@/graphql/group";
|
||||
import GroupMemberCard from "@/components/Group/GroupMemberCard.vue";
|
||||
import Invitations from "@/components/Group/Invitations.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor";
|
||||
import { IGroup, IMember, MemberRole, usernameWithDomain } from "@/types/actor";
|
||||
import { Route } from "vue-router";
|
||||
import { ApolloError } from "apollo-client";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@@ -64,14 +77,14 @@ export default class MyEvents extends Vue {
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
acceptInvitation(member: IMember) {
|
||||
acceptInvitation(member: IMember): Promise<Route> {
|
||||
return this.$router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(member.parent) },
|
||||
});
|
||||
}
|
||||
|
||||
rejectInvitation({ id: memberId }: { id: string }) {
|
||||
rejectInvitation({ id: memberId }: { id: string }): void {
|
||||
const index = this.membershipsPages.elements.findIndex(
|
||||
(membership) => membership.role === MemberRole.INVITED && membership.id === memberId
|
||||
);
|
||||
@@ -81,14 +94,23 @@ export default class MyEvents extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
get invitations() {
|
||||
async leaveGroup(group: IGroup): Promise<void> {
|
||||
await this.$apollo.mutate({
|
||||
mutation: LEAVE_GROUP,
|
||||
variables: {
|
||||
groupId: group.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get invitations(): IMember[] {
|
||||
if (!this.membershipsPages) return [];
|
||||
return this.membershipsPages.elements.filter(
|
||||
(member: IMember) => member.role === MemberRole.INVITED
|
||||
);
|
||||
}
|
||||
|
||||
get memberships() {
|
||||
get memberships(): IMember[] {
|
||||
if (!this.membershipsPages) return [];
|
||||
return this.membershipsPages.elements.filter(
|
||||
(member: IMember) => ![MemberRole.INVITED, MemberRole.REJECTED].includes(member.role)
|
||||
@@ -114,4 +136,8 @@ section {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.group-member-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
</aside>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { IGroup, IPerson } from "@/types/actor";
|
||||
import { FETCH_GROUP } from "@/graphql/group";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import SettingMenuSection from "../../components/Settings/SettingMenuSection.vue";
|
||||
import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue";
|
||||
|
||||
Reference in New Issue
Block a user