Introduce group basic federation, event new page and notifications
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,50 +1,41 @@
|
||||
<template>
|
||||
<div class="container root">
|
||||
<h1>{{ $t('Create a new group') }}</h1>
|
||||
<section class="section container">
|
||||
<h1>{{ $t("Create a new group") }}</h1>
|
||||
|
||||
<div>
|
||||
<b-field :label="$t('Group name')">
|
||||
<b-input aria-required="true" required v-model="group.preferred_username"/>
|
||||
<b-input aria-required="true" required v-model="group.preferredUsername" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Group full name')">
|
||||
<b-input aria-required="true" required v-model="group.name"/>
|
||||
<b-input aria-required="true" required v-model="group.name" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Description')">
|
||||
<b-input aria-required="true" required v-model="group.description" type="textarea"/>
|
||||
<b-input aria-required="true" required v-model="group.summary" type="textarea" />
|
||||
</b-field>
|
||||
|
||||
<div>
|
||||
Avatar
|
||||
<picture-upload v-model="avatarFile"></picture-upload>
|
||||
<picture-upload v-model="avatarFile" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Banner
|
||||
<picture-upload v-model="avatarFile"></picture-upload>
|
||||
<picture-upload v-model="avatarFile" />
|
||||
</div>
|
||||
|
||||
<button class="button is-primary" @click="createGroup()">
|
||||
{{ $t('Create my group') }}
|
||||
</button>
|
||||
<button class="button is-primary" @click="createGroup()">{{ $t("Create my group") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.root {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Group, IPerson } from '@/types/actor';
|
||||
import { CREATE_GROUP, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { RouteName } from '@/router';
|
||||
import PictureUpload from '@/components/PictureUpload.vue';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { Group, IPerson } from "@/types/actor";
|
||||
import { CREATE_GROUP, CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -62,6 +53,7 @@ export default class CreateGroup extends Vue {
|
||||
group = new Group();
|
||||
|
||||
avatarFile: File | null = null;
|
||||
|
||||
bannerFile: File | null = null;
|
||||
|
||||
async createGroup() {
|
||||
@@ -74,10 +66,15 @@ export default class CreateGroup extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
await this.$router.push({ name: RouteName.GROUP, params: { identityName: this.group.preferredUsername } });
|
||||
await this.$router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { identityName: this.group.preferredUsername },
|
||||
});
|
||||
|
||||
this.$notifier.success(
|
||||
this.$t('Group {displayName} created', { displayName: this.group.displayName() }) as string,
|
||||
this.$t("Group {displayName} created", {
|
||||
displayName: this.group.displayName(),
|
||||
}) as string
|
||||
);
|
||||
} catch (err) {
|
||||
this.handleError(err);
|
||||
@@ -114,7 +111,12 @@ export default class CreateGroup extends Vue {
|
||||
creatorActorId: this.currentActor.id,
|
||||
};
|
||||
|
||||
return Object.assign({}, this.group, avatarObj, bannerObj, currentActor);
|
||||
return {
|
||||
...this.group,
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
...currentActor,
|
||||
};
|
||||
}
|
||||
|
||||
private handleError(err: any) {
|
||||
|
||||
@@ -1,66 +1,194 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<div v-if="group">
|
||||
<div class="card-image" v-if="group.banner.url">
|
||||
<figure class="image">
|
||||
<img :src="group.banner.url">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="group.avatar.url">
|
||||
<div class="container is-widescreen">
|
||||
<div
|
||||
v-if="group && groupMemberships && groupMemberships.includes(group.id)"
|
||||
class="block-container"
|
||||
>
|
||||
<div class="block-column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="group">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group.preferredUsername) },
|
||||
}"
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="presentation">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h1>{{ group.name }}</h1>
|
||||
<small class="has-text-grey">@{{ usernameWithDomain(group) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="members">
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
:title="
|
||||
$t(`@{username} ({role})`, {
|
||||
username: usernameWithDomain(member.actor),
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in group.members.elements"
|
||||
:key="member.actor.id"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="member.actor.avatar.url"
|
||||
v-if="member.actor.avatar"
|
||||
alt
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div class="organized-events-wrapper" v-if="group.organizedEvents.total > 0">
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
class="organized-event"
|
||||
/>
|
||||
</div>
|
||||
<router-link :to="{}">{{ $t("View all upcoming events") }}</router-link>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Resources") }}</subtitle>
|
||||
<div v-if="group.resources.elements.length > 0">
|
||||
<div v-for="resource in group.resources.elements" :key="resource.id">
|
||||
<resource-item
|
||||
:resource="resource"
|
||||
v-if="resource.type !== 'folder'"
|
||||
:inline="true"
|
||||
/>
|
||||
<folder-item :resource="resource" :group="group" v-else :inline="true" />
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER_ROOT,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all resources") }}</router-link
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
<div class="block-column">
|
||||
<section>
|
||||
<subtitle>{{ $t("Public page") }}</subtitle>
|
||||
<p>{{ $t("Followed by {count} persons", { count: group.members.total }) }}</p>
|
||||
<b-button type="is-light">{{ $t("Edit biography") }}</b-button>
|
||||
<b-button type="is-primary">{{ $t("Post a public message") }}</b-button>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Ongoing tasks") }}</subtitle>
|
||||
<div
|
||||
v-if="group.todoLists.elements.length > 0"
|
||||
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>
|
||||
<router-link :to="{ name: RouteName.TODO_LISTS }">{{ $t("View all todos") }}</router-link>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Discussions") }}</subtitle>
|
||||
<conversation-list-item
|
||||
v-if="group.conversations.total > 0"
|
||||
v-for="conversation in group.conversations.elements"
|
||||
:key="conversation.id"
|
||||
:conversation="conversation"
|
||||
/>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all conversations") }}</router-link
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="group">
|
||||
<section class="presentation">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" alt />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title">{{ group.name }}</p>
|
||||
<p class="subtitle">@{{ group.preferredUsername }}</p>
|
||||
<h2>{{ group.name }}</h2>
|
||||
<small class="has-text-grey">@{{ usernameWithDomain(group) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p v-html="group.summary"></p>
|
||||
</div>
|
||||
</div>
|
||||
<section class="box" v-if="group.organizedEvents.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Organized') }}
|
||||
</subtitle>
|
||||
<div class="columns">
|
||||
<EventCard
|
||||
v-for="event in group.organizedEvents"
|
||||
:event="event"
|
||||
:options="{ hideDetails: true }"
|
||||
:key="event.uuid"
|
||||
class="column is-one-third"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="group.members.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Members') }}
|
||||
</subtitle>
|
||||
<div class="columns">
|
||||
<span
|
||||
v-for="member in group.members"
|
||||
:key="member.actor.preferredUsername"
|
||||
>{{ member.actor.preferredUsername }}</span>
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div class="organized-events-wrapper" v-if="group.organizedEvents.total > 0">
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
class="organized-event"
|
||||
/>
|
||||
<router-link :to="{}">{{ $t("View all upcoming events") }}</router-link>
|
||||
</div>
|
||||
<span v-else>{{ $t("No public upcoming events") }}</span>
|
||||
</section>
|
||||
<!-- {{ group }}-->
|
||||
<section>
|
||||
<subtitle>{{ $t("Latest posts") }}</subtitle>
|
||||
</section>
|
||||
</div>
|
||||
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
|
||||
{{ $t('No group found') }}
|
||||
{{ $t("No group found") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IGroup } from '@/types/actor';
|
||||
import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
||||
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
|
||||
import ConversationListItem from "@/components/Conversation/ConversationListItem.vue";
|
||||
import ResourceItem from "@/components/Resource/ResourceItem.vue";
|
||||
import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -68,49 +196,141 @@ import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
},
|
||||
currentActor: {
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
person: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
components: {
|
||||
ConversationListItem,
|
||||
EventMinimalistCard,
|
||||
CompactTodo,
|
||||
Subtitle,
|
||||
EventCard,
|
||||
FolderItem,
|
||||
ResourceItem,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
// @ts-ignore
|
||||
title: this.groupTitle,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
meta: [
|
||||
// @ts-ignore
|
||||
{ name: "description", content: this.groupSummary },
|
||||
],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Group extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
currentActor!: IActor;
|
||||
|
||||
person!: IPerson;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
loading = true;
|
||||
|
||||
created() {
|
||||
this.fetchData();
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
@Watch("currentActor")
|
||||
watchCurrentActor(currentActor: IActor, oldActor: IActor) {
|
||||
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
|
||||
this.$apollo.queries.group.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('$route')
|
||||
onRouteChanged() {
|
||||
// call again the method if the route changes
|
||||
this.fetchData();
|
||||
get groupTitle() {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.preferredUsername;
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/actors/${this.name}`, this.$store)
|
||||
// .then(response => response.json())
|
||||
// .then((response) => {
|
||||
// this.group = response.data;
|
||||
// this.loading = false;
|
||||
// console.log(this.group);
|
||||
// });
|
||||
get groupSummary() {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.summary;
|
||||
}
|
||||
|
||||
get groupMemberships() {
|
||||
if (!this.person || !this.person.id) return undefined;
|
||||
return this.person.memberships.elements.map(({ parent: { id } }) => id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
section.container {
|
||||
min-height: 30em;
|
||||
div.container {
|
||||
background: white;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem 0;
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.block-column {
|
||||
flex: 1;
|
||||
margin: 0 2rem;
|
||||
|
||||
section {
|
||||
/deep/ h2 span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.presentation {
|
||||
.members {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.organized-events-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.organized-event {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.presentation {
|
||||
.media-left {
|
||||
span.icon.is-large {
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
|
||||
/deep/ i.mdi.mdi-account-group.mdi-48px:before {
|
||||
font-size: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-content {
|
||||
h2 {
|
||||
color: #3c376e;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +1,62 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<h1>
|
||||
{{ $t('Group List') }}
|
||||
</h1>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<section class="container section">
|
||||
<h1>{{ $t("Group List") }} ({{ groups.total }})</h1>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div class="columns">
|
||||
<GroupCard
|
||||
v-for="group in groups"
|
||||
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>
|
||||
<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 { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LIST_GROUPS } from "@/graphql/actor";
|
||||
import { Group, IGroup } from "@/types/actor";
|
||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
groups: {
|
||||
query: LIST_GROUPS,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GroupCard,
|
||||
},
|
||||
})
|
||||
export default class GroupList extends Vue {
|
||||
groups = [];
|
||||
groups: { elements: IGroup[]; total: number } = { elements: [], total: 0 };
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
//
|
||||
// usernameWithDomain(actor) {
|
||||
// return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
// }
|
||||
|
||||
created() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
usernameWithDomain(actor) {
|
||||
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch('/groups', this.$store)
|
||||
// .then(response => response.json())
|
||||
// .then((data) => {
|
||||
// console.log(data);
|
||||
// this.loading = false;
|
||||
// this.groups = data.data;
|
||||
// });
|
||||
}
|
||||
|
||||
deleteGroup(group) {
|
||||
const router = this.$router;
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/groups/${this.usernameWithDomain(group)}`, this.$store, { method: 'DELETE' })
|
||||
// .then(response => response.json())
|
||||
// .then(() => router.push('/groups'));
|
||||
}
|
||||
|
||||
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) } }));
|
||||
}
|
||||
// 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>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
62
js/src/views/Group/GroupMembers.vue
Normal file
62
js/src/views/Group/GroupMembers.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<section class="container section" v-if="group">
|
||||
<form @submit.prevent="inviteMember">
|
||||
<b-field :label="$t('Invite a new member')" custom-class="add-relay" horizontal>
|
||||
<b-field grouped expanded size="is-large">
|
||||
<p class="control">
|
||||
<b-input v-model="newMemberUsername" :placeholder="$t('Ex: someone@mobilizon.org')" />
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{ $t("Invite member") }}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<h1>{{ $t("Group Members") }} ({{ group.members.total }})</h1>
|
||||
<pre>{{ group.members }}</pre>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import RouteName from "../../router/name";
|
||||
import { FETCH_GROUP } from "../../graphql/actor";
|
||||
import { INVITE_MEMBER } from "../../graphql/member";
|
||||
import { IGroup } from "../../types/actor";
|
||||
import { IMember } from "../../types/actor/group.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.$route.params.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class GroupMembers extends Vue {
|
||||
group!: IGroup;
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
newMemberUsername = "";
|
||||
|
||||
async inviteMember() {
|
||||
await this.$apollo.mutate<{ inviteMember: IMember }>({
|
||||
mutation: INVITE_MEMBER,
|
||||
variables: {
|
||||
groupId: this.group.id,
|
||||
targetActorUsername: this.newMemberUsername,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
102
js/src/views/Group/MyGroups.vue
Normal file
102
js/src/views/Group/MyGroups.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title">{{ $t("My groups") }}</h1>
|
||||
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t("Create group") }}</router-link>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<section v-if="invitations && invitations.length > 0">
|
||||
<InvitationCard
|
||||
v-for="member in invitations"
|
||||
:key="member.id"
|
||||
:member="member"
|
||||
@accept="acceptInvitation"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="memberships && memberships.length > 0">
|
||||
<GroupCard v-for="member in memberships" :key="member.id" :member="member" />
|
||||
</section>
|
||||
<b-message v-if="$apollo.loading === false && memberships.length === 0" type="is-danger">
|
||||
{{ $t("No groups found") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IGroup, IMember, MemberRole } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import { ACCEPT_INVITATION } from "../../graphql/member";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
GroupCard,
|
||||
InvitationCard,
|
||||
},
|
||||
apollo: {
|
||||
paginatedGroups: {
|
||||
query: LOGGED_USER_MEMBERSHIPS,
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
beforeDateTime: new Date().toISOString(),
|
||||
},
|
||||
update: (data) => data.loggedUser.memberships,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t("My groups") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class MyEvents extends Vue {
|
||||
paginatedGroups!: Paginate<IMember>;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get invitations() {
|
||||
if (!this.paginatedGroups) return [];
|
||||
return this.paginatedGroups.elements.filter((member) => member.role === MemberRole.INVITED);
|
||||
}
|
||||
|
||||
get memberships() {
|
||||
if (!this.paginatedGroups) return [];
|
||||
return this.paginatedGroups.elements.filter((member) => member.role !== MemberRole.INVITED);
|
||||
}
|
||||
|
||||
async acceptInvitation(id: string) {
|
||||
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||
mutation: ACCEPT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.participation {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
section {
|
||||
.upcoming-month {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
js/src/views/Group/Settings.vue
Normal file
96
js/src/views/Group/Settings.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<aside class="section container">
|
||||
<h1 class="title">{{ $t("Settings") }}</h1>
|
||||
<div class="columns">
|
||||
<SettingsMenu class="column is-one-quarter-desktop" :menu="menu" />
|
||||
<div class="column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li
|
||||
v-for="route in routes.get($route.name)"
|
||||
:class="{ 'is-active': route.to.name === $route.name }"
|
||||
:key="route.title"
|
||||
>
|
||||
<router-link :to="{ name: route.to.name }">{{ route.title }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import SettingsMenu from "@/components/Settings/SettingsMenu.vue";
|
||||
import { ISettingMenuSection } from "@/types/setting-menu.model";
|
||||
import { Route } from "vue-router";
|
||||
import { IGroup, IPerson } from "@/types/actor";
|
||||
import { FETCH_GROUP } from "@/graphql/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: { SettingsMenu },
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Settings extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
menu: ISettingMenuSection[] = [];
|
||||
|
||||
group!: IGroup[];
|
||||
|
||||
mounted() {
|
||||
this.menu = [
|
||||
{
|
||||
title: this.$t("Settings") as string,
|
||||
to: { name: RouteName.GROUP_SETTINGS } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t("Public") as string,
|
||||
to: { name: RouteName.GROUP_PUBLIC_SETTINGS } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t("Members") as string,
|
||||
to: { name: RouteName.GROUP_MEMBERS_SETTINGS } as Route,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get routes(): Map<string, Route[]> {
|
||||
return this.getPath(this.menu);
|
||||
}
|
||||
|
||||
getPath(object: ISettingMenuSection[]) {
|
||||
function iter(menu: ISettingMenuSection[] | ISettingMenuSection, acc: ISettingMenuSection[]) {
|
||||
if (Array.isArray(menu)) {
|
||||
return menu.forEach((item: ISettingMenuSection) => {
|
||||
iter(item, acc.concat(item));
|
||||
});
|
||||
}
|
||||
if (menu.items && menu.items.length > 0) {
|
||||
return menu.items.forEach((item: ISettingMenuSection) => {
|
||||
iter(item, acc.concat(item));
|
||||
});
|
||||
}
|
||||
result.set(menu.to.name, acc);
|
||||
}
|
||||
|
||||
const result = new Map();
|
||||
iter(object, []);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
aside.section {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user