Improve and activate groups

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-09-29 09:53:48 +02:00
parent 1ca46a6863
commit 49a5725da3
131 changed files with 16440 additions and 1929 deletions

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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";