Add admin interface to manage instances subscriptions

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-12-03 11:29:51 +01:00
parent 0a96d70348
commit 334d66bf5d
141 changed files with 4198 additions and 1923 deletions

View File

@@ -7,7 +7,7 @@
<div class="list is-hoverable">
<a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)">
<div class="media">
<img class="media-left image" v-if="identity.avatar" :src="identity.avatar.url" alt="" />
<img class="media-left image is-48x48" v-if="identity.avatar" :src="identity.avatar.url" alt="" />
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<div class="media-content">
<h3>@{{ identity.preferredUsername }}</h3>
@@ -17,7 +17,7 @@
</a>
</div>
</section>
<slot name="footer"></slot>
<slot name="footer" />
</div>
</template>
<script lang="ts">

View File

@@ -1,7 +1,9 @@
<template>
<div class="identity-picker">
<span v-if="inline" class="inline">
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/>
<b-icon v-else size="is-small" icon="account-circle" />
{{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
<b-button type="is-text" @click="isComponentModalActive = true">
{{ $t('Change') }}
</b-button>

View File

@@ -38,6 +38,13 @@
</article>
</router-link>
</div>
<div class="tile is-parent">
<router-link :to="{ name: RouteName.RELAYS }">
<article class="tile is-child box">
<p class="subtitle">{{ $t('Instances') }}</p>
</article>
</router-link>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
@@ -67,6 +74,12 @@ import { RouteName } from '@/router';
query: DASHBOARD,
},
},
metaInfo() {
return {
title: this.$t('Administration') as string,
titleTemplate: '%s | Mobilizon',
};
},
})
export default class Dashboard extends Vue {
dashboard!: IDashboard;

View File

@@ -0,0 +1,57 @@
<template>
<div class="container">
<h1 class="title">{{ $t('Instances') }}</h1>
<div class="tabs is-boxed">
<ul>
<router-link tag="li" active-class="is-active" :to="{name: RouteName.RELAY_FOLLOWINGS}" exact>
<a>
<b-icon icon="inbox-arrow-down"></b-icon>
<span>{{ $t('Followings') }} <b-tag rounded> {{ relayFollowings.total }} </b-tag> </span>
</a>
</router-link>
<router-link tag="li" active-class="is-active" :to="{name: RouteName.RELAY_FOLLOWERS}" exact>
<a>
<b-icon icon="inbox-arrow-up"></b-icon>
<span>{{ $t('Followers') }} <b-tag rounded> {{ relayFollowers.total }} </b-tag> </span>
</a>
</router-link>
</ul>
</div>
<router-view></router-view>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from '@/graphql/admin';
import { Paginate } from '@/types/paginate';
import { IFollower } from '@/types/actor/follower.model';
@Component({
apollo: {
relayFollowings: {
query: RELAY_FOLLOWINGS,
fetchPolicy: 'cache-and-network',
},
relayFollowers: {
query: RELAY_FOLLOWERS,
fetchPolicy: 'cache-and-network',
},
},
})
export default class Follows extends Vue {
RouteName = RouteName;
activeTab: number = 0;
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
}
</script>
<style lang="scss">
.tab-item {
form {
margin-bottom: 1.5rem;
}
}
</style>

View File

@@ -29,7 +29,7 @@
<address-auto-complete v-model="event.physicalAddress" />
<b-field :label="$t('Organizer')">
<identity-picker-wrapper v-model="event.organizerActor"></identity-picker-wrapper>
<identity-picker-wrapper v-model="event.organizerActor" />
</b-field>
<div class="field">
@@ -92,7 +92,7 @@
<div class="box" v-if="limitedPlaces">
<b-field :label="$t('Number of places')">
<b-numberinput controls-position="compact" min="0" v-model="event.options.maximumAttendeeCapacity"></b-numberinput>
<b-numberinput controls-position="compact" min="0" v-model="event.options.maximumAttendeeCapacity" />
</b-field>
<!--
<b-field>
@@ -145,21 +145,21 @@
name="status"
type="is-warning"
:native-value="EventStatus.TENTATIVE">
<b-icon icon="calendar-question"></b-icon>
<b-icon icon="calendar-question" />
{{ $t('Tentative: Will be confirmed later') }}
</b-radio-button>
<b-radio-button v-model="event.status"
name="status"
type="is-success"
:native-value="EventStatus.CONFIRMED">
<b-icon icon="calendar-check"></b-icon>
<b-icon icon="calendar-check" />
{{ $t('Confirmed: Will happen') }}
</b-radio-button>
<b-radio-button v-model="event.status"
name="status"
type="is-danger"
:native-value="EventStatus.CANCELLED">
<b-icon icon="calendar-remove"></b-icon>
<b-icon icon="calendar-remove" />
{{ $t("Cancelled: Won't happen") }}
</b-radio-button>
</b-field>
@@ -191,7 +191,7 @@
</div>
</form>
</b-modal>
<span ref="bottomObserver"></span>
<span ref="bottomObserver" />
<nav role="navigation" aria-label="main navigation" class="navbar" :class="{'is-fixed-bottom': showFixedNavbar }">
<div class="container">
<div class="navbar-menu">
@@ -395,6 +395,11 @@ export default class EditEvent extends Vue {
}
}
@Watch('currentActor')
setCurrentActor() {
this.event.organizerActor = this.currentActor;
}
private validateForm() {
const form = this.$refs.form as HTMLFormElement;
if (form.checkValidity()) {

View File

@@ -1,6 +1,8 @@
import {ParticipantRole} from "@/types/event.model";
import {ParticipantRole} from "@/types/event.model";
<template>
<div class="container">
<b-loading :active.sync="$apollo.loading"></b-loading>
<b-loading :active.sync="$apollo.loading" />
<transition appear name="fade" mode="out-in">
<div>
<div class="header-picture" v-if="event.picture" :style="`background-image: url('${event.picture.url}')`" />
@@ -9,7 +11,7 @@
<div class="title-and-participate-button">
<div class="title-wrapper">
<div class="date-component">
<date-calendar-icon :date="event.beginsOn"></date-calendar-icon>
<date-calendar-icon :date="event.beginsOn" />
</div>
<div class="title-and-informations">
<h1 class="title">{{ event.title }}</h1>
@@ -49,7 +51,7 @@
<template>
<span>{{ $t('Event already passed')}}</span>
</template>
<b-icon icon="menu-down"></b-icon>
<b-icon icon="menu-down" />
</button>
</div>
</div>
@@ -65,6 +67,9 @@
<b-tag type="is-info" v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('Public event') }}</b-tag>
<b-tag type="is-info" v-if="event.visibility === EventVisibility.UNLISTED">{{ $t('Private event') }}</b-tag>
</span>
<span v-if="!event.local">
<b-tag type="is-primary">{{ event.organizerActor.domain }}</b-tag>
</span>
<router-link
v-if="event.tags && event.tags.length > 0"
v-for="tag in event.tags"
@@ -136,7 +141,7 @@
</b-modal>
</div>
<span class="online-address" v-if="event.onlineAddress && urlToHostname(event.onlineAddress)">
<b-icon icon="link"></b-icon>
<b-icon icon="link" />
<a
target="_blank"
rel="noopener noreferrer"
@@ -250,8 +255,14 @@
</template>
<script lang="ts">
import { EVENT_PERSON_PARTICIPATION, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { Component, Prop } from 'vue-property-decorator';
import {
EVENT_PERSON_PARTICIPATION,
EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
FETCH_EVENT,
JOIN_EVENT,
LEAVE_EVENT,
} from '@/graphql/event';
import { Component, Prop, Watch } from 'vue-property-decorator';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { EventModel, EventStatus, EventVisibility, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson, Person } from '@/types/actor';
@@ -311,6 +322,15 @@ import 'intersection-observer';
actorId: this.currentActor.id,
};
},
subscribeToMore: {
document: EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED,
variables() {
return {
eventId: this.event.id,
actorId: this.currentActor.id,
};
},
},
update: (data) => {
if (data && data.person) return data.person.participations;
return [];
@@ -341,6 +361,7 @@ export default class Event extends EventMixin {
currentActor!: IPerson;
identity: IPerson = new Person();
participations: IParticipant[] = [];
oldParticipationRole!: String;
showMap: boolean = false;
isReportModalActive: boolean = false;
isJoinModalActive: boolean = false;
@@ -432,14 +453,10 @@ export default class Event extends EventMixin {
reporterId: this.currentActor.id,
reportedId: this.event.organizerActor.id,
content,
forward,
},
});
this.$buefy.notification.open({
message: this.$t('Event {eventTitle} reported', { eventTitle }) as string,
type: 'is-success',
position: 'is-bottom-right',
duration: 5000,
});
this.$notifier.success(this.$t('Event {eventTitle} reported', { eventTitle }) as string);
} catch (error) {
console.error(error);
}
@@ -493,12 +510,11 @@ export default class Event extends EventMixin {
},
});
if (data) {
this.$buefy.notification.open({
message: (data.joinEvent.role === ParticipantRole.NOT_APPROVED ? this.$t('Your participation has been requested') : this.$t('Your participation has been confirmed')) as string,
type: 'is-success',
position: 'is-bottom-right',
duration: 5000,
});
if (data.joinEvent.role === ParticipantRole.NOT_APPROVED) {
this.participationRequestedMessage();
} else {
this.participationConfirmedMessage();
}
}
} catch (error) {
console.error(error);
@@ -563,18 +579,55 @@ export default class Event extends EventMixin {
},
});
if (data) {
this.$buefy.notification.open({
message: this.$t('You have cancelled your participation') as string,
type: 'is-success',
position: 'is-bottom-right',
duration: 5000,
});
this.participationCancelledMessage();
}
} catch (error) {
console.error(error);
}
}
@Watch('participations')
watchParticipations() {
if (this.participations.length > 0) {
if (this.oldParticipationRole
&& this.participations[0].role !== ParticipantRole.NOT_APPROVED
&& this.oldParticipationRole !== this.participations[0].role) {
switch (this.participations[0].role) {
case ParticipantRole.PARTICIPANT:
this.participationConfirmedMessage();
break;
case ParticipantRole.REJECTED:
this.participationRejectedMessage();
break;
default:
this.participationChangedMessage();
break;
}
}
this.oldParticipationRole = this.participations[0].role;
}
}
private participationConfirmedMessage() {
this.$notifier.success(this.$t('Your participation has been confirmed') as string);
}
private participationRequestedMessage() {
this.$notifier.success(this.$t('Your participation has been requested') as string);
}
private participationRejectedMessage() {
this.$notifier.error(this.$t('Your participation has been rejected') as string);
}
private participationChangedMessage() {
this.$notifier.info(this.$t('Your participation status has been changed') as string);
}
private participationCancelledMessage() {
this.$notifier.success(this.$t('You have cancelled your participation') as string);
}
async downloadIcsEvent() {
const data = await (await fetch(`${GRAPHQL_API_ENDPOINT}/events/${this.uuid}/export/ics`)).text();
const blob = new Blob([data], { type: 'text/calendar' });

View File

@@ -64,7 +64,7 @@ export default class ReportList extends Vue {
RouteName = RouteName;
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.container li {
margin: 10px auto;
}

View File

@@ -27,7 +27,10 @@
</tr>
<tr>
<td>{{ $t('Reported by') }}</td>
<td>
<td v-if="report.reporter.type === ActorType.APPLICATION">
{{ report.reporter.domain }}
</td>
<td v-else>
<router-link :to="{ name: RouteName.PROFILE, params: { name: report.reporter.preferredUsername } }">
<img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}
</router-link>
@@ -55,15 +58,15 @@
<td>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid }}">{{ report.event.title }}</router-link>
<span class="is-pulled-right">
<b-button
tag="router-link"
type="is-primary"
:to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"
icon-left="pencil"
size="is-small">{{ $t('Edit') }}</b-button>
<!-- <b-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
<b-button
type="is-danger"
@click="confirmDelete()"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small">{{ $t('Delete') }}</b-button>
</span>
@@ -74,24 +77,24 @@
</div>
<div class="box report-content">
<p v-if="report.content" v-html="nl2br(report.content)"></p>
<p v-if="report.content" v-html="nl2br(report.content)" />
<p v-else>{{ $t('No comment') }}</p>
</div>
<div class="box" v-if="report.event && report.comments.length === 0">
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid }}">
<h3 class="title">{{ report.event.title }}</h3>
<p v-html="report.event.description"></p>
<p v-html="report.event.description" />
</router-link>
<b-button
tag="router-link"
type="is-primary"
:to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"
icon-left="pencil"
size="is-small">{{ $t('Edit') }}</b-button>
<!-- <b-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
<b-button
type="is-danger"
@click="confirmDelete()"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small">{{ $t('Delete') }}</b-button>
</div>
@@ -101,17 +104,25 @@
<div class="box" v-if="comment">
<article class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="comment.actor.avatar">
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image">
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content">
<div class="content">
<strong>{{ comment.actor.name }}</strong> <small>@{{ comment.actor.preferredUsername }}</small>
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong> <small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ $t('Unknown actor') }}</span>
<br>
<p v-html="comment.text"></p>
<p v-html="comment.text" />
</div>
<b-button
type="is-danger"
@click="confirmCommentDelete(comment)"
icon-left="delete"
size="is-small">{{ $t('Delete') }}</b-button>
</div>
</article>
</div>
@@ -131,21 +142,23 @@
<b-field :label="$t('New note')">
<b-input type="textarea" v-model="noteContent"></b-input>
</b-field>
<b-button type="submit" @click="addNote">{{ $t('Ajouter une note') }}</b-button>
<b-button type="submit" @click="addNote">{{ $t('Add a note') }}</b-button>
</form>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { CREATE_REPORT_NOTE, REPORT, REPORTS, UPDATE_REPORT } from '@/graphql/report';
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from '@/graphql/report';
import { IReport, IReportNote, ReportStatusEnum } from '@/types/report.model';
import { RouteName } from '@/router';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { IPerson, ActorType } from '@/types/actor';
import { DELETE_EVENT } from '@/graphql/event';
import { uniq } from 'lodash';
import { nl2br } from '@/utils/html';
import { DELETE_COMMENT } from '@/graphql/comment';
import { IComment } from '@/types/comment.model';
@Component({
apollo: {
@@ -164,6 +177,12 @@ import { nl2br } from '@/utils/html';
query: CURRENT_ACTOR_CLIENT,
},
},
metaInfo() {
return {
title: this.$t('Report') as string,
titleTemplate: '%s | Mobilizon',
};
},
})
export default class Report extends Vue {
@Prop({ required: true }) reportId!: number;
@@ -173,6 +192,7 @@ export default class Report extends Vue {
ReportStatusEnum = ReportStatusEnum;
RouteName = RouteName;
ActorType = ActorType;
nl2br = nl2br;
noteContent: string = '';
@@ -210,7 +230,7 @@ export default class Report extends Vue {
}
}
confirmDelete() {
confirmEventDelete() {
this.$buefy.dialog.confirm({
title: this.$t('Deleting event') as string,
message: this.$t('Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.') as string,
@@ -221,6 +241,17 @@ export default class Report extends Vue {
});
}
confirmCommentDelete(comment: IComment) {
this.$buefy.dialog.confirm({
title: this.$t('Deleting comment') as string,
message: this.$t('Are you sure you want to <b>delete</b> this comment? This action cannot be undone.') as string,
confirmText: this.$t('Delete Comment') as string,
type: 'is-danger',
hasIcon: true,
onConfirm: () => this.deleteComment(comment),
});
}
async deleteEvent() {
if (!this.report.event || !this.report.event.id) return;
const eventTitle = this.report.event.title;
@@ -245,6 +276,21 @@ export default class Report extends Vue {
}
}
async deleteComment(comment: IComment) {
try {
await this.$apollo.mutate({
mutation: DELETE_COMMENT,
variables: {
commentId: comment.id,
actorId: this.currentActor.id,
},
});
this.$notifier.success(this.$t('Comment deleted') as string);
} catch (error) {
console.error(error);
}
}
async updateReport(status: ReportStatusEnum) {
try {
await this.$apollo.mutate({
@@ -289,10 +335,6 @@ export default class Report extends Vue {
<style lang="scss" scoped>
@import "@/variables.scss";
.container li {
margin: 10px auto;
}
tbody td img.image, .note img.image {
display: inline;
height: 1.5em;

View File

@@ -9,15 +9,15 @@
<b-field>
<b-radio-button v-model="filterReports"
:native-value="ReportStatusEnum.OPEN">
Ouvert
{{ $t('Open') }}
</b-radio-button>
<b-radio-button v-model="filterReports"
:native-value="ReportStatusEnum.RESOLVED">
Résolus
{{ $t('Resolved') }}
</b-radio-button>
<b-radio-button v-model="filterReports"
:native-value="ReportStatusEnum.CLOSED">
Fermés
{{ $t('Closed') }}
</b-radio-button>
</b-field>
<ul v-if="reports.length > 0">
@@ -28,9 +28,9 @@
</li>
</ul>
<div v-else>
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">No open reports yet</b-message>
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">No resolved reports yet</b-message>
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">No closed reports yet</b-message>
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">{{ $t('No open reports yet') }}</b-message>
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">{{ $t('No resolved reports yet') }}</b-message>
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">{{ $t('No closed reports yet') }}</b-message>
</div>
</section>
</template>
@@ -80,8 +80,3 @@ export default class ReportList extends Vue {
}
}
</script>
<style lang="scss">
.container li {
margin: 10px auto;
}
</style>