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

@@ -26,7 +26,8 @@
</div>
<div class="media-content">
<span class="title" ref="title">{{ actorDisplayName }}</span><br>
<small class="has-text-grey">@{{ participant.actor.preferredUsername }}</small>
<small class="has-text-grey" v-if="participant.actor.domain">@{{ participant.actor.preferredUsername }}@{{ participant.actor.domain }}</small>
<small class="has-text-grey" v-else>@{{ participant.actor.preferredUsername }}</small>
</div>
</div>
</div>
@@ -41,7 +42,7 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IActor, IPerson, Person } from '@/types/actor';
import { Person } from '@/types/actor';
import { IParticipant, ParticipantRole } from '@/types/event.model';
@Component

View File

@@ -0,0 +1,141 @@
<template>
<div>
<b-table
v-show="relayFollowers.elements.length > 0"
:data="relayFollowers.elements"
:loading="$apollo.queries.relayFollowers.loading"
ref="table"
:checked-rows.sync="checkedRows"
:is-row-checkable="(row) => row.id !== 3"
detailed
:show-detail-icon="false"
paginated
backend-pagination
:total="relayFollowers.total"
:per-page="perPage"
@page-change="onPageChange"
checkable
checkbox-position="left">
<template slot-scope="props">
<b-table-column field="actor.id" label="ID" width="40" numeric>
{{ props.row.actor.id }}
</b-table-column>
<b-table-column field="actor.type" :label="$t('Type')" width="80">
<b-icon icon="lan" v-if="isInstance(props.row.actor)" />
<b-icon icon="account-circle" v-else />
</b-table-column>
<b-table-column field="approved" :label="$t('Status')" width="100" sortable centered>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger' }`">
{{ props.row.approved ? $t('Accepted') : $t('Pending') }}
</span>
</b-table-column>
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
<template>
<a @click="toggle(props.row)" v-if="isInstance(props.row.actor)">
{{ props.row.actor.domain }}
</a>
<a @click="toggle(props.row)" v-else>
{{ `${props.row.actor.preferredUsername}@${props.row.actor.domain}` }}
</a>
</template>
</b-table-column>
<b-table-column field="actor.updatedAt" :label="$t('Date')" sortable>
{{ props.row.updatedAt | formatDateTimeString }}
</b-table-column>
</template>
<template slot="detail" slot-scope="props">
<article>
<div class="content">
<strong>{{ props.row.actor.domain }}</strong>
<small>@{{ props.row.actor.preferredUsername }}</small>
<small>31m</small>
<br>
<p v-html="props.row.actor.summary" />
</div>
</article>
</template>
<template slot="bottom-left" v-if="checkedRows.length > 0">
<div class="buttons">
<b-button @click="acceptRelays" type="is-success" v-if="checkedRowsHaveAtLeastOneToApprove">
{{ $tc('No instance to approve|Approve instance|Approve {number} instances', checkedRows.length, { number: checkedRows.length }) }}
</b-button>
<b-button @click="rejectRelays" type="is-danger">
{{ $tc('No instance to reject|Reject instance|Reject {number} instances', checkedRows.length, { number: checkedRows.length }) }}
</b-button>
</div>
</template>
</b-table>
<b-message type="is-danger" v-if="relayFollowers.elements.length === 0">
{{ $t("No instance follows your instance yet.") }}
</b-message>
</div>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import { ACCEPT_RELAY, REJECT_RELAY, RELAY_FOLLOWERS } from '@/graphql/admin';
import { Paginate } from '@/types/paginate';
import { IFollower } from '@/types/actor/follower.model';
import RelayMixin from '@/mixins/relay';
@Component({
apollo: {
relayFollowers: {
query: RELAY_FOLLOWERS,
fetchPolicy: 'cache-and-network',
},
},
metaInfo() {
return {
title: this.$t('Followers') as string,
titleTemplate: '%s | Mobilizon',
};
},
})
export default class Followers extends Mixins(RelayMixin) {
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
async acceptRelays() {
await this.checkedRows.forEach((row: IFollower) => {
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
});
}
async rejectRelays() {
await this.checkedRows.forEach((row: IFollower) => {
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
});
}
async acceptRelay(address: String) {
await this.$apollo.mutate({
mutation: ACCEPT_RELAY,
variables: {
address,
},
});
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
}
async rejectRelay(address: String) {
await this.$apollo.mutate({
mutation: REJECT_RELAY,
variables: {
address,
},
});
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
}
get checkedRowsHaveAtLeastOneToApprove(): boolean {
return this.checkedRows.some(checkedRow => !checkedRow.approved);
}
}
</script>

View File

@@ -0,0 +1,142 @@
<template>
<div>
<form @submit="followRelay">
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal>
<b-field grouped expanded size="is-large">
<p class="control">
<b-input v-model="newRelayAddress" :placeholder="$t('Ex: test.mobilizon.org')" />
</p>
<p class="control">
<b-button type="is-primary" native-type="submit">{{ $t('Add an instance') }}</b-button>
</p>
</b-field>
</b-field>
</form>
<b-table
v-show="relayFollowings.elements.length > 0"
:data="relayFollowings.elements"
:loading="$apollo.queries.relayFollowings.loading"
ref="table"
:checked-rows.sync="checkedRows"
:is-row-checkable="(row) => row.id !== 3"
detailed
:show-detail-icon="false"
paginated
backend-pagination
:total="relayFollowings.total"
:per-page="perPage"
@page-change="onPageChange"
checkable
checkbox-position="left">
<template slot-scope="props">
<b-table-column field="targetActor.id" label="ID" width="40" numeric>
{{ props.row.targetActor.id }}
</b-table-column>
<b-table-column field="targetActor.type" :label="$t('Type')" width="80">
<b-icon icon="lan" v-if="isInstance(props.row.targetActor)" />
<b-icon icon="account-circle" v-else />
</b-table-column>
<b-table-column field="approved" :label="$t('Status')" width="100" sortable centered>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger' }`">
{{ props.row.approved ? $t('Accepted') : $t('Pending') }}
</span>
</b-table-column>
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
<template>
<a @click="toggle(props.row)" v-if="isInstance(props.row.targetActor)">
{{ props.row.targetActor.domain }}
</a>
<a @click="toggle(props.row)" v-else>
{{ `${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}` }}
</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable>
{{ props.row.updatedAt | formatDateTimeString }}
</b-table-column>
</template>
<template slot="detail" slot-scope="props">
<article>
<div class="content">
<strong>{{ props.row.targetActor.domain }}</strong>
<small>@{{ props.row.targetActor.preferredUsername }}</small>
<small>31m</small>
<br>
<p v-html="props.row.targetActor.summary" />
</div>
</article>
</template>
<template slot="bottom-left" v-if="checkedRows.length > 0">
<b-button @click="removeRelays" type="is-danger">
{{ $tc('No instance to remove|Remove instance|Remove {number} instances', checkedRows.length, { number: checkedRows.length }) }}
</b-button>
</template>
</b-table>
<b-message type="is-danger" v-if="relayFollowings.elements.length === 0">
{{ $t("You don't follow any instances yet.") }}
</b-message>
</div>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import { ADD_RELAY, RELAY_FOLLOWINGS, REMOVE_RELAY } from '@/graphql/admin';
import { IFollower } from '@/types/actor/follower.model';
import { Paginate } from '@/types/paginate';
import RelayMixin from '@/mixins/relay';
@Component({
apollo: {
relayFollowings: {
query: RELAY_FOLLOWINGS,
fetchPolicy: 'cache-and-network',
},
},
metaInfo() {
return {
title: this.$t('Followings') as string,
titleTemplate: '%s | Mobilizon',
};
},
})
export default class Followings extends Mixins(RelayMixin) {
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
newRelayAddress: String = '';
async followRelay(e) {
e.preventDefault();
await this.$apollo.mutate({
mutation: ADD_RELAY,
variables: {
address: this.newRelayAddress,
},
// TODO: Handle cache update properly without refreshing
});
await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = '';
}
async removeRelays() {
await this.checkedRows.forEach((row: IFollower) => {
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
});
}
async removeRelay(address: String) {
await this.$apollo.mutate({
mutation: REMOVE_RELAY,
variables: {
address,
},
});
await this.$apollo.queries.relayFollowings.refetch();
this.checkedRows = [];
}
}
</script>

View File

@@ -11,7 +11,8 @@
<div class="content">
<span class="first-line" v-if="!comment.deletedAt">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
<small v-if="comment.actor.domain">@{{ comment.actor.preferredUsername }}@{{ comment.actor.domain }}</small>
<small v-else>@{{ comment.actor.preferredUsername }}</small>
<a class="comment-link has-text-grey" :href="commentId">
<small>{{ timeago(new Date(comment.updatedAt)) }}</small>
</a>
@@ -202,7 +203,7 @@ export default class Comment extends Vue {
timeago(dateTime): String {
if (this.timeAgoInstance != null) {
// @ts-ignore
// @ts-ignore
return this.timeAgoInstance.format(dateTime);
}
return '';
@@ -213,7 +214,7 @@ export default class Comment extends Vue {
}
get commentFromOrganizer(): boolean {
return this.event.organizerActor !== undefined && this.comment.actor.id === this.event.organizerActor.id;
return this.event.organizerActor !== undefined && this.comment.actor && this.comment.actor.id === this.event.organizerActor.id;
}
get commentId(): String {
@@ -230,6 +231,7 @@ export default class Comment extends Vue {
title: this.$t('Report this comment'),
comment: this.comment,
onConfirm: this.reportComment,
outsideDomain: this.comment.actor.domain,
},
});
}
@@ -244,6 +246,7 @@ export default class Comment extends Vue {
reportedId: this.comment.actor.id,
commentsIds: [this.comment.id],
content,
forward,
},
});
this.$buefy.notification.open({

View File

@@ -221,7 +221,7 @@ export default class CommentTree extends Vue {
data: { thread: replies },
});
// @ts-ignore
// @ts-ignore
const parentCommentIndex = oldComments.findIndex(oldComment => oldComment.id === comment.originComment.id);
const parentComment = oldComments[parentCommentIndex];
parentComment.replies = replies;

View File

@@ -409,9 +409,9 @@ export default class EditorComponent extends Vue {
}
replyToComment(comment: IComment) {
console.log('called replyToComment', comment);
const actorModel = new Actor(comment.actor);
if (!this.editor) return;
console.log(this.editor.commands);
this.editor.commands.mention({ id: actorModel.id, label: actorModel.usernameWithDomain().substring(1) });
this.editor.focus();
}

View File

@@ -112,7 +112,7 @@ export default class AddressAutoComplete extends Vue {
addressData: IAddress[] = [];
selected: IAddress = new Address();
isFetching: boolean = false;
queryText: string = this.value && (new Address(this.value)).fullName || '';
queryText: string = (this.value && (new Address(this.value)).fullName) || '';
addressModalActive: boolean = false;
private gettingLocation: boolean = false;
private location!: Position;
@@ -164,6 +164,7 @@ export default class AddressAutoComplete extends Vue {
@Watch('value')
updateEditing() {
if (!(this.value && this.value.id)) return;
this.selected = this.value;
const address = new Address(this.selected);
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;

View File

@@ -26,11 +26,11 @@ A button to set your participation
<div class="participation-button">
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
<button class="button is-success" type="button" slot="trigger">
<b-icon icon="check"></b-icon>
<b-icon icon="check" />
<template>
<span>{{ $t('I participate') }}</span>
</template>
<b-icon icon="menu-down"></b-icon>
<b-icon icon="menu-down" />
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
@@ -45,11 +45,11 @@ A button to set your participation
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<button class="button is-success" type="button" slot="trigger">
<b-icon icon="timer-sand-empty"></b-icon>
<b-icon icon="timer-sand-empty" />
<template>
<span>{{ $t('I participate') }}</span>
</template>
<b-icon icon="menu-down"></b-icon>
<b-icon icon="menu-down" />
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
@@ -73,7 +73,7 @@ A button to set your participation
<template>
<span>{{ $t('Participate') }}</span>
</template>
<b-icon icon="menu-down"></b-icon>
<b-icon icon="menu-down" />
</button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
@@ -84,12 +84,12 @@ A button to set your participation
</figure>
</div>
<div class="media-content">
<span>{{ $t('with {identity}', {identity: currentActor.preferredUsername }) }}</span>
<span>{{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}</span>
</div>
</div>
</b-dropdown-item>
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal">
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal" v-if="identities.length > 1">
{{ $t('with another identity…')}}
</b-dropdown-item>
</b-dropdown>
@@ -99,14 +99,32 @@ A button to set your participation
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson } from '@/types/actor';
import { IPerson, Person } from '@/types/actor';
import { IDENTITIES } from '@/graphql/actor';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model';
@Component
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
},
identities: {
query: IDENTITIES,
update: ({ identities }) => identities ? identities.map(identity => new Person(identity)) : [],
skip() {
return this.currentUser.isLoggedIn === false;
},
},
},
})
export default class ParticipationButton extends Vue {
@Prop({ required: true }) participation!: IParticipant;
@Prop({ required: true }) currentActor!: IPerson;
ParticipantRole = ParticipantRole;
currentUser!: ICurrentUser;
identities: IPerson[] = [];
joinEvent(actor: IPerson) {
this.$emit('joinEvent', actor);

View File

@@ -16,7 +16,7 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
@Component({
beforeDestroy() {
// @ts-ignore
// @ts-ignore
this.parentContainer.removeLayer(this);
},
})

View File

@@ -20,7 +20,14 @@
</div>
<div class="content columns">
<div class="column is-one-quarter-desktop">Reported by <img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}</div>
<div class="column is-one-quarter-desktop">
<span v-if="report.reporter.type === ActorType.APPLICATION">
{{ $t('Reported by someone on {domain}', { domain: report.reporter.domain}) }}
</span>
<span v-else>
{{ $t('Reported by {reporter}', { reporter: report.reporter.preferredUsername}) }}
</span>
</div>
<div class="column" v-if="report.content">{{ report.content }}</div>
</div>
</div>
@@ -29,10 +36,13 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IReport } from '@/types/report.model';
import { ActorType } from '@/types/actor';
@Component
export default class ReportCard extends Vue {
@Prop({ required: true }) report!: IReport;
ActorType = ActorType;
}
</script>
<style lang="scss">

View File

@@ -44,11 +44,8 @@
/>
</div>
<p v-if="outsideDomain">
{{ $t('The content came from another server. Transfer an anonymous copy of the report?') }}
</p>
<div class="control" v-if="outsideDomain">
<p>{{ $t('The content came from another server. Transfer an anonymous copy of the report?') }}</p>
<b-switch v-model="forward">{{ $t('Transfer to {outsideDomain}', { outsideDomain }) }}</b-switch>
</div>
</div>