Add admin dashboard, event reporting, moderation report screens, moderation log
Close #156 and #158 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
164429964a
commit
27f2597b07
@ -11,7 +11,7 @@
|
||||
<script lang="ts">
|
||||
import NavBar from '@/components/NavBar.vue';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from '@/constants';
|
||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
import Footer from '@/components/Footer.vue';
|
||||
@ -46,14 +46,16 @@ export default class App extends Vue {
|
||||
const userId = localStorage.getItem(AUTH_USER_ID);
|
||||
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
|
||||
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
const role = localStorage.getItem(AUTH_USER_ROLE);
|
||||
|
||||
if (userId && userEmail && accessToken) {
|
||||
if (userId && userEmail && accessToken && role) {
|
||||
return this.$apollo.mutate({
|
||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||
variables: {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
isLoggedIn: true,
|
||||
role,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -74,6 +76,7 @@ export default class App extends Vue {
|
||||
@import "~bulma/sass/components/navbar.sass";
|
||||
@import "~bulma/sass/components/pagination.sass";
|
||||
@import "~bulma/sass/components/dropdown.sass";
|
||||
@import "~bulma/sass/components/breadcrumb.sass";
|
||||
@import "~bulma/sass/elements/box.sass";
|
||||
@import "~bulma/sass/elements/button.sass";
|
||||
@import "~bulma/sass/elements/container.sass";
|
||||
@ -84,6 +87,7 @@ export default class App extends Vue {
|
||||
@import "~bulma/sass/elements/tag.sass";
|
||||
@import "~bulma/sass/elements/title.sass";
|
||||
@import "~bulma/sass/elements/notification";
|
||||
@import "~bulma/sass/elements/table";
|
||||
@import "~bulma/sass/grid/_all.sass";
|
||||
@import "~bulma/sass/layout/_all.sass";
|
||||
|
||||
@ -100,6 +104,7 @@ export default class App extends Vue {
|
||||
@import "~buefy/src/scss/components/upload";
|
||||
@import "~buefy/src/scss/components/radio";
|
||||
@import "~buefy/src/scss/components/switch";
|
||||
@import "~buefy/src/scss/components/table";
|
||||
|
||||
.router-enter-active,
|
||||
.router-leave-active {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ApolloCache } from 'apollo-cache';
|
||||
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||
import { ICurrentUserRole } from '@/types/current-user.model';
|
||||
|
||||
export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
|
||||
cache.writeData({
|
||||
@ -9,18 +10,20 @@ export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObjec
|
||||
id: null,
|
||||
email: null,
|
||||
isLoggedIn: false,
|
||||
role: ICurrentUserRole.USER,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
Mutation: {
|
||||
updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
|
||||
updateCurrentUser: (_, { id, email, isLoggedIn, role }, { cache }) => {
|
||||
const data = {
|
||||
currentUser: {
|
||||
id,
|
||||
email,
|
||||
isLoggedIn,
|
||||
role,
|
||||
__typename: 'CurrentUser',
|
||||
},
|
||||
};
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="tag-container" v-if="event.tags">
|
||||
<b-tag v-for="tag in event.tags.slice(0, 3)" :key="tag.slug" type="is-secondary">{{ tag.title }}</b-tag>
|
||||
</div>
|
||||
<img src="https://picsum.photos/g/400/225/?random">
|
||||
<img src="https://picsum.photos/g/400/225/?random" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<translate
|
||||
v-if="!endsOn"
|
||||
:translate-params="{date: formatDate(beginsOn), time: formatTime(beginsOn)}"
|
||||
>The %{ date } at %{ time }</translate>
|
||||
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span>
|
||||
<translate
|
||||
v-else-if="isSameDay()"
|
||||
:translate-params="{date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}"
|
||||
@ -21,11 +18,13 @@ export default class EventFullDate extends Vue {
|
||||
@Prop({ required: false }) endsOn!: string;
|
||||
|
||||
formatDate(value) {
|
||||
return value ? new Date(value).toLocaleString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) : null;
|
||||
if (!this.$options.filters) return;
|
||||
return this.$options.filters.formatDateString(value);
|
||||
}
|
||||
|
||||
formatTime(value) {
|
||||
return value ? new Date(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }) : null;
|
||||
if (!this.$options.filters) return;
|
||||
return this.$options.filters.formatTimeString(value);
|
||||
}
|
||||
|
||||
isSameDay() {
|
||||
|
@ -44,6 +44,10 @@
|
||||
<router-link :to="{ name: ActorRouteName.CREATE_GROUP }" v-translate>Create group</router-link>
|
||||
</span>
|
||||
|
||||
<span class="navbar-item" v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR">
|
||||
<router-link :to="{ name: AdminRouteName.DASHBOARD }" v-translate>Administration</router-link>
|
||||
</span>
|
||||
|
||||
<a v-translate class="navbar-item" v-on:click="logout()">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -71,10 +75,12 @@ import { LOGGED_PERSON } from '@/graphql/actor';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
import { ICurrentUser, ICurrentUserRole } from '@/types/current-user.model';
|
||||
import Logo from '@/components/Logo.vue';
|
||||
import SearchField from '@/components/SearchField.vue';
|
||||
import { ActorRouteName } from '@/router/actor';
|
||||
import { AdminRouteName } from '@/router/admin';
|
||||
import { RouteName } from '@/router';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -98,9 +104,11 @@ export default class NavBar extends Vue {
|
||||
loggedPerson: IPerson | null = null;
|
||||
config!: IConfig;
|
||||
currentUser!: ICurrentUser;
|
||||
ICurrentUserRole = ICurrentUserRole;
|
||||
showNavbar: boolean = false;
|
||||
|
||||
ActorRouteName = ActorRouteName;
|
||||
AdminRouteName = AdminRouteName;
|
||||
|
||||
@Watch('currentUser')
|
||||
async onCurrentUserChanged() {
|
||||
@ -119,7 +127,8 @@ export default class NavBar extends Vue {
|
||||
async logout() {
|
||||
await logout(this.$apollo.provider.defaultClient);
|
||||
|
||||
return this.$router.push({ path: '/' });
|
||||
if (this.$route.name === RouteName.HOME) return;
|
||||
return this.$router.push({ name: RouteName.HOME });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
45
js/src/components/Report/ReportCard.vue
Normal file
45
js/src/components/Report/ReportCard.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="card" v-if="report">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="report.reported.avatar">
|
||||
<img :src="report.reported.avatar.url" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ report.reported.name }}</p>
|
||||
<p class="subtitle is-6">@{{ report.reported.preferredUsername }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content columns">
|
||||
<div class="column is-one-quarter box">Reported by <img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}</div>
|
||||
<div class="column box" v-if="report.event">
|
||||
<img class="image" v-if="report.event.picture" :src="report.event.picture.url" />
|
||||
<span>{{ report.event.title }}</span>
|
||||
</div>
|
||||
<div class="column box" v-if="report.reportContent">{{ report.reportContent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IReport } from '@/types/report.model';
|
||||
import { EventRouteName } from '@/router/event';
|
||||
|
||||
@Component
|
||||
export default class ReportCard extends Vue {
|
||||
@Prop({ required: true }) report!: IReport;
|
||||
|
||||
EventRouteName = EventRouteName;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.content img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
101
js/src/components/Report/ReportModal.vue
Normal file
101
js/src/components/Report/ReportModal.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head" v-if="title">
|
||||
<p class="modal-card-title">{{ title }}</p>
|
||||
</header>
|
||||
|
||||
<section
|
||||
class="modal-card-body is-flex"
|
||||
:class="{ 'is-titleless': !title }">
|
||||
<div class="media">
|
||||
<div
|
||||
class="media-left">
|
||||
<b-icon
|
||||
icon="alert"
|
||||
type="is-warning"
|
||||
size="is-large"/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p>The report will be sent to the moderators of your instance.
|
||||
You can explain why you report this content below.</p>
|
||||
|
||||
<div class="control">
|
||||
<b-input
|
||||
v-model="content"
|
||||
type="textarea"
|
||||
@keyup.enter="confirm"
|
||||
placeholder="Additional comments"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="outsideDomain">
|
||||
The content came from another server. Transfer an anonymous copy of the report ?
|
||||
</p>
|
||||
|
||||
<div class="control" v-if="outsideDomain">
|
||||
<b-switch v-model="forward">Transfer to {{ outsideDomain }}</b-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<button
|
||||
class="button"
|
||||
ref="cancelButton"
|
||||
@click="cancel('button')">
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
ref="confirmButton"
|
||||
@click="confirm">
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { removeElement } from 'buefy/src/utils/helpers';
|
||||
|
||||
@Component({
|
||||
mounted() {
|
||||
this.$data.isActive = true;
|
||||
},
|
||||
})
|
||||
export default class ReportModal extends Vue {
|
||||
@Prop({ type: Function, default: () => {} }) onConfirm;
|
||||
@Prop({ type: String }) title;
|
||||
@Prop({ type: String, default: '' }) outsideDomain;
|
||||
@Prop({ type: String, default: 'Cancel' }) cancelText;
|
||||
@Prop({ type: String, default: 'Send the report' }) confirmText;
|
||||
|
||||
isActive: boolean = false;
|
||||
content: string = '';
|
||||
forward: boolean = false;
|
||||
|
||||
confirm() {
|
||||
this.onConfirm(this.content, this.forward);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Dialog.
|
||||
*/
|
||||
close() {
|
||||
this.isActive = false;
|
||||
// Timeout for the animation complete before destroying
|
||||
setTimeout(() => {
|
||||
this.$destroy();
|
||||
removeElement(this.$el);
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.modal-card .modal-card-foot {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
@ -3,3 +3,4 @@ export const AUTH_REFRESH_TOKEN = 'auth-refresh-token';
|
||||
export const AUTH_USER_ID = 'auth-user-id';
|
||||
export const AUTH_USER_EMAIL = 'auth-user-email';
|
||||
export const AUTH_USER_ACTOR = 'auth-user-actor';
|
||||
export const AUTH_USER_ROLE = 'auth-user-role';
|
||||
|
19
js/src/filters/datetime.ts
Normal file
19
js/src/filters/datetime.ts
Normal file
@ -0,0 +1,19 @@
|
||||
function parseDateTime(value: string): Date {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
function formatDateString(value: string): string {
|
||||
return parseDateTime(value).toLocaleString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
|
||||
function formatTimeString(value: string): string {
|
||||
return parseDateTime(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' });
|
||||
}
|
||||
|
||||
function formatDateTimeString(value: string): string {
|
||||
return parseDateTime(value).toLocaleTimeString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { formatDateString, formatTimeString, formatDateTimeString };
|
9
js/src/filters/index.ts
Normal file
9
js/src/filters/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { formatDateString, formatTimeString, formatDateTimeString } from './datetime';
|
||||
|
||||
export default {
|
||||
install(vue) {
|
||||
vue.filter('formatDateString', formatDateString);
|
||||
vue.filter('formatTimeString', formatTimeString);
|
||||
vue.filter('formatDateTimeString', formatDateTimeString);
|
||||
},
|
||||
};
|
@ -177,7 +177,7 @@ query($name:String!) {
|
||||
|
||||
export const CREATE_GROUP = gql`
|
||||
mutation CreateGroup(
|
||||
$creatorActorId: Int!,
|
||||
$creatorActorId: ID!,
|
||||
$preferredUsername: String!,
|
||||
$name: String!,
|
||||
$summary: String,
|
||||
|
19
js/src/graphql/admin.ts
Normal file
19
js/src/graphql/admin.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const DASHBOARD = gql`
|
||||
query {
|
||||
dashboard {
|
||||
lastPublicEventPublished {
|
||||
title,
|
||||
picture {
|
||||
alt
|
||||
url
|
||||
},
|
||||
},
|
||||
numberOfUsers,
|
||||
numberOfEvents,
|
||||
numberOfComments,
|
||||
numberOfReports
|
||||
}
|
||||
}
|
||||
`;
|
@ -7,7 +7,8 @@ mutation Login($email: String!, $password: String!) {
|
||||
refreshToken,
|
||||
user {
|
||||
id,
|
||||
email
|
||||
email,
|
||||
role
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -70,8 +70,8 @@ export const FETCH_EVENT = gql`
|
||||
},
|
||||
publishAt,
|
||||
category,
|
||||
online_address,
|
||||
phone_address,
|
||||
onlineAddress,
|
||||
phoneAddress,
|
||||
physicalAddress {
|
||||
${physicalAddressQuery}
|
||||
}
|
||||
@ -218,8 +218,8 @@ export const CREATE_EVENT = gql`
|
||||
},
|
||||
publishAt,
|
||||
category,
|
||||
online_address,
|
||||
phone_address,
|
||||
onlineAddress,
|
||||
phoneAddress,
|
||||
physicalAddress {
|
||||
${physicalAddressQuery}
|
||||
},
|
||||
@ -240,7 +240,7 @@ export const EDIT_EVENT = gql`
|
||||
$description: String,
|
||||
$beginsOn: DateTime,
|
||||
$endsOn: DateTime,
|
||||
$status: Int,
|
||||
$status: EventStatus,
|
||||
$visibility: EventVisibility
|
||||
$tags: [String],
|
||||
$picture: PictureInput,
|
||||
@ -280,8 +280,8 @@ export const EDIT_EVENT = gql`
|
||||
},
|
||||
publishAt,
|
||||
category,
|
||||
online_address,
|
||||
phone_address,
|
||||
onlineAddress,
|
||||
phoneAddress,
|
||||
physicalAddress {
|
||||
${physicalAddressQuery}
|
||||
},
|
||||
@ -296,7 +296,7 @@ export const EDIT_EVENT = gql`
|
||||
`;
|
||||
|
||||
export const JOIN_EVENT = gql`
|
||||
mutation JoinEvent($eventId: Int!, $actorId: Int!) {
|
||||
mutation JoinEvent($eventId: ID!, $actorId: ID!) {
|
||||
joinEvent(
|
||||
eventId: $eventId,
|
||||
actorId: $actorId
|
||||
@ -307,7 +307,7 @@ export const JOIN_EVENT = gql`
|
||||
`;
|
||||
|
||||
export const LEAVE_EVENT = gql`
|
||||
mutation LeaveEvent($eventId: Int!, $actorId: Int!) {
|
||||
mutation LeaveEvent($eventId: ID!, $actorId: ID!) {
|
||||
leaveEvent(
|
||||
eventId: $eventId,
|
||||
actorId: $actorId
|
||||
@ -320,9 +320,9 @@ export const LEAVE_EVENT = gql`
|
||||
`;
|
||||
|
||||
export const DELETE_EVENT = gql`
|
||||
mutation DeleteEvent($id: Int!, $actorId: Int!) {
|
||||
mutation DeleteEvent($eventId: ID!, $actorId: ID!) {
|
||||
deleteEvent(
|
||||
eventId: $id,
|
||||
eventId: $eventId,
|
||||
actorId: $actorId
|
||||
) {
|
||||
id
|
||||
|
@ -12,7 +12,7 @@ query {
|
||||
}`;
|
||||
|
||||
export const CREATE_FEED_TOKEN_ACTOR = gql`
|
||||
mutation createFeedToken($actor_id: Int!) {
|
||||
mutation createFeedToken($actor_id: ID!) {
|
||||
createFeedToken(actorId: $actor_id) {
|
||||
token,
|
||||
actor {
|
||||
|
161
js/src/graphql/report.ts
Normal file
161
js/src/graphql/report.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const REPORTS = gql`
|
||||
query Reports($status: ReportStatus) {
|
||||
reports(status: $status) {
|
||||
id,
|
||||
reported {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
reporter {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
event {
|
||||
id,
|
||||
uuid,
|
||||
title,
|
||||
picture {
|
||||
url
|
||||
}
|
||||
},
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const REPORT_FRAGMENT = gql`
|
||||
fragment ReportFragment on Report {
|
||||
id,
|
||||
reported {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
reporter {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
event {
|
||||
id,
|
||||
uuid,
|
||||
title,
|
||||
description,
|
||||
picture {
|
||||
url
|
||||
}
|
||||
},
|
||||
notes {
|
||||
id,
|
||||
content
|
||||
moderator {
|
||||
preferredUsername,
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
insertedAt
|
||||
},
|
||||
insertedAt,
|
||||
updatedAt,
|
||||
status,
|
||||
content
|
||||
}
|
||||
`;
|
||||
|
||||
export const REPORT = gql`
|
||||
query Report($id: ID!) {
|
||||
report(id: $id) {
|
||||
...ReportFragment
|
||||
}
|
||||
}
|
||||
${REPORT_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const CREATE_REPORT = gql`
|
||||
mutation CreateReport(
|
||||
$eventId: ID!,
|
||||
$reporterActorId: ID!,
|
||||
$reportedActorId: ID!,
|
||||
$content: String
|
||||
) {
|
||||
createReport(eventId: $eventId, reporterActorId: $reporterActorId, reportedActorId: $reportedActorId, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_REPORT = gql`
|
||||
mutation UpdateReport(
|
||||
$reportId: ID!,
|
||||
$moderatorId: ID!,
|
||||
$status: ReportStatus!
|
||||
) {
|
||||
updateReportStatus(reportId: $reportId, moderatorId: $moderatorId, status: $status) {
|
||||
...ReportFragment
|
||||
}
|
||||
}
|
||||
${REPORT_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const CREATE_REPORT_NOTE = gql`
|
||||
mutation CreateReportNote(
|
||||
$reportId: ID!,
|
||||
$moderatorId: ID!,
|
||||
$content: String!
|
||||
) {
|
||||
createReportNote(reportId: $reportId, moderatorId: $moderatorId, content: $content) {
|
||||
id,
|
||||
content,
|
||||
insertedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LOGS = gql`
|
||||
query {
|
||||
actionLogs {
|
||||
id,
|
||||
action,
|
||||
actor {
|
||||
id,
|
||||
preferredUsername
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
object {
|
||||
...on Report {
|
||||
id
|
||||
},
|
||||
... on ReportNote {
|
||||
report {
|
||||
id,
|
||||
}
|
||||
}
|
||||
... on Event {
|
||||
id,
|
||||
title
|
||||
}
|
||||
},
|
||||
insertedAt
|
||||
}
|
||||
}
|
||||
`;
|
@ -31,12 +31,13 @@ query {
|
||||
id,
|
||||
email,
|
||||
isLoggedIn,
|
||||
role
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CURRENT_USER_CLIENT = gql`
|
||||
mutation UpdateCurrentUser($id: Int!, $email: String!, $isLoggedIn: Boolean!) {
|
||||
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn) @client
|
||||
mutation UpdateCurrentUser($id: Int!, $email: String!, $isLoggedIn: Boolean!, $role: UserRole!) {
|
||||
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn, role: $role) @client
|
||||
}
|
||||
`;
|
||||
|
@ -7,6 +7,7 @@ import App from '@/App.vue';
|
||||
import router from '@/router';
|
||||
import { apolloProvider } from './vue-apollo';
|
||||
import { NotifierPlugin } from '@/plugins/notifier';
|
||||
import filters from '@/filters';
|
||||
|
||||
const translations = require('@/i18n/translations.json');
|
||||
|
||||
@ -14,6 +15,7 @@ Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(Buefy);
|
||||
Vue.use(NotifierPlugin);
|
||||
Vue.use(filters);
|
||||
|
||||
const language = (window.navigator as any).userLanguage || window.navigator.language;
|
||||
|
||||
|
16
js/src/router/admin.ts
Normal file
16
js/src/router/admin.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { RouteConfig } from 'vue-router';
|
||||
import Dashboard from '@/views/Admin/Dashboard.vue';
|
||||
|
||||
export enum AdminRouteName {
|
||||
DASHBOARD = 'Dashboard',
|
||||
}
|
||||
|
||||
export const adminRoutes: RouteConfig[] = [
|
||||
{
|
||||
path: '/admin',
|
||||
name: AdminRouteName.DASHBOARD,
|
||||
component: Dashboard,
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
];
|
@ -5,9 +5,11 @@ import Home from '@/views/Home.vue';
|
||||
import { UserRouteName, userRoutes } from './user';
|
||||
import { EventRouteName, eventRoutes } from '@/router/event';
|
||||
import { ActorRouteName, actorRoutes, MyAccountRouteName } from '@/router/actor';
|
||||
import { AdminRouteName, adminRoutes } from '@/router/admin';
|
||||
import { ErrorRouteName, errorRoutes } from '@/router/error';
|
||||
import { authGuardIfNeeded } from '@/router/guards/auth-guard';
|
||||
import Search from '@/views/Search.vue';
|
||||
import { ModerationRouteName, moderationRoutes } from '@/router/moderation';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
@ -35,6 +37,8 @@ export const RouteName = {
|
||||
...EventRouteName,
|
||||
...ActorRouteName,
|
||||
...MyAccountRouteName,
|
||||
...AdminRouteName,
|
||||
...ModerationRouteName,
|
||||
...ErrorRouteName,
|
||||
};
|
||||
|
||||
@ -46,6 +50,8 @@ const router = new Router({
|
||||
...userRoutes,
|
||||
...eventRoutes,
|
||||
...actorRoutes,
|
||||
...adminRoutes,
|
||||
...moderationRoutes,
|
||||
...errorRoutes,
|
||||
{
|
||||
path: '/search/:searchTerm/:searchType?',
|
||||
|
34
js/src/router/moderation.ts
Normal file
34
js/src/router/moderation.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { RouteConfig } from 'vue-router';
|
||||
import ReportList from '@/views/Moderation/ReportList.vue';
|
||||
import Report from '@/views/Moderation/Report.vue';
|
||||
import Logs from '@/views/Moderation/Logs.vue';
|
||||
|
||||
export enum ModerationRouteName {
|
||||
REPORTS = 'Reports',
|
||||
REPORT = 'Report',
|
||||
LOGS = 'Logs',
|
||||
}
|
||||
|
||||
export const moderationRoutes: RouteConfig[] = [
|
||||
{
|
||||
path: '/moderation/reports/:filter?',
|
||||
name: ModerationRouteName.REPORTS,
|
||||
component: ReportList,
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/moderation/report/:reportId',
|
||||
name: ModerationRouteName.REPORT,
|
||||
component: Report,
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/moderation/logs',
|
||||
name: ModerationRouteName.LOGS,
|
||||
component: Logs,
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
];
|
9
js/src/types/admin.model.ts
Normal file
9
js/src/types/admin.model.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { IEvent } from '@/types/event.model';
|
||||
|
||||
export interface IDashboard {
|
||||
lastPublicEventPublished: IEvent;
|
||||
numberOfUsers: number;
|
||||
numberOfEvents: number;
|
||||
numberOfComments: number;
|
||||
numberOfReports: number;
|
||||
}
|
@ -1,5 +1,12 @@
|
||||
export enum ICurrentUserRole {
|
||||
USER = 'USER',
|
||||
MODERATOR = 'MODERATOR',
|
||||
ADMINISTRATOR = 'ADMINISTRATOR',
|
||||
}
|
||||
|
||||
export interface ICurrentUser {
|
||||
id: number;
|
||||
email: string;
|
||||
isLoggedIn: boolean;
|
||||
role: ICurrentUserRole;
|
||||
}
|
||||
|
47
js/src/types/report.model.ts
Normal file
47
js/src/types/report.model.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { IActor, IPerson } from '@/types/actor';
|
||||
import { IEvent } from '@/types/event.model';
|
||||
|
||||
export enum ReportStatusEnum {
|
||||
OPEN = 'OPEN',
|
||||
CLOSED = 'CLOSED',
|
||||
RESOLVED = 'RESOLVED',
|
||||
}
|
||||
|
||||
export interface IReport extends IActionLogObject {
|
||||
id: string;
|
||||
reported: IActor;
|
||||
reporter: IPerson;
|
||||
event?: IEvent;
|
||||
content: string;
|
||||
notes: IReportNote[];
|
||||
insertedAt: Date;
|
||||
updatedAt: Date;
|
||||
status: ReportStatusEnum;
|
||||
}
|
||||
|
||||
export interface IReportNote extends IActionLogObject{
|
||||
id: string;
|
||||
content: string;
|
||||
moderator: IActor;
|
||||
}
|
||||
|
||||
export interface IActionLogObject {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum ActionLogAction {
|
||||
NOTE_CREATION = 'NOTE_CREATION',
|
||||
NOTE_DELETION = 'NOTE_DELETION',
|
||||
REPORT_UPDATE_CLOSED = 'REPORT_UPDATE_CLOSED',
|
||||
REPORT_UPDATE_OPENED = 'REPORT_UPDATE_OPENED',
|
||||
REPORT_UPDATE_RESOLVED = 'REPORT_UPDATE_RESOLVED',
|
||||
EVENT_DELETION = 'EVENT_DELETION',
|
||||
}
|
||||
|
||||
export interface IActionLog {
|
||||
id: string;
|
||||
object: IReport|IReportNote|IEvent;
|
||||
actor: IActor;
|
||||
action: ActionLogAction;
|
||||
insertedAt: Date;
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
|
||||
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from '@/constants';
|
||||
import { ILogin, IToken } from '@/types/login.model';
|
||||
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { onLogout } from '@/vue-apollo';
|
||||
import ApolloClient from 'apollo-client';
|
||||
import { ICurrentUserRole } from '@/types/current-user.model';
|
||||
|
||||
export function saveUserData(obj: ILogin) {
|
||||
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
|
||||
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
|
||||
localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
|
||||
|
||||
saveTokenData(obj);
|
||||
}
|
||||
@ -17,7 +19,7 @@ export function saveTokenData(obj: IToken) {
|
||||
}
|
||||
|
||||
export function deleteUserData() {
|
||||
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN]) {
|
||||
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_ROLE]) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
@ -29,6 +31,7 @@ export function logout(apollo: ApolloClient<any>) {
|
||||
id: null,
|
||||
email: null,
|
||||
isLoggedIn: false,
|
||||
role: ICurrentUserRole.USER,
|
||||
},
|
||||
});
|
||||
|
||||
|
73
js/src/views/Admin/Dashboard.vue
Normal file
73
js/src/views/Admin/Dashboard.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<h1 class="title">Administration</h1>
|
||||
<div class="tile is-ancestor" v-if="dashboard">
|
||||
<div class="tile is-vertical is-4">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical is-6">
|
||||
<article class="tile is-child box">
|
||||
<p class="title">{{ dashboard.numberOfEvents }}</p>
|
||||
<p class="subtitle">événements publiés</p>
|
||||
</article>
|
||||
<article class="tile is-child box">
|
||||
<p class="title">{{ dashboard.numberOfComments}}</p>
|
||||
<p class="subtitle">commentaires</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child box">
|
||||
<p class="title">{{ dashboard.numberOfUsers }}</p>
|
||||
<p class="subtitle">utilisateurices</p>
|
||||
</article>
|
||||
<router-link :to="{ name: ModerationRouteName.REPORTS}">
|
||||
<article class="tile is-child box">
|
||||
<p class="title">{{ dashboard.numberOfReports }}</p>
|
||||
<p class="subtitle">signalements ouverts</p>
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
|
||||
<article class="tile is-child box">
|
||||
<p class="title">Dernier événement publié</p>
|
||||
<p class="subtitle">{{ dashboard.lastPublicEventPublished.title }}</p>
|
||||
<figure class="image is-4by3" v-if="dashboard.lastPublicEventPublished.picture">
|
||||
<img :src="dashboard.lastPublicEventPublished.picture.url" />
|
||||
</figure>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent">
|
||||
<article class="tile is-child box">
|
||||
<div class="content">
|
||||
<p class="title">Bienvenue sur votre espace d'administration</p>
|
||||
<p class="subtitle">With even more content</p>
|
||||
<div class="content">
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam semper diam at erat pulvinar, at pulvinar felis blandit. Vestibulum volutpat tellus diam, consequat gravida libero rhoncus ut. Morbi maximus, leo sit amet vehicula eleifend, nunc dui porta orci, quis semper odio felis ut quam.</p>
|
||||
<p>Suspendisse varius ligula in molestie lacinia. Maecenas varius eget ligula a sagittis. Pellentesque interdum, nisl nec interdum maximus, augue diam porttitor lorem, et sollicitudin felis neque sit amet erat. Maecenas imperdiet felis nisi, fringilla luctus felis hendrerit sit amet. Aenean vitae gravida diam, finibus dignissim turpis. Sed eget varius ligula, at volutpat tortor.</p>
|
||||
<p>Integer sollicitudin, tortor a mattis commodo, velit urna rhoncus erat, vitae congue lectus dolor consequat libero. Donec leo ligula, maximus et pellentesque sed, gravida a metus. Cras ullamcorper a nunc ac porta. Aliquam ut aliquet lacus, quis faucibus libero. Quisque non semper leo.</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { DASHBOARD } from '@/graphql/admin';
|
||||
import { IDashboard } from '@/types/admin.model';
|
||||
import { ModerationRouteName } from '@/router/moderation';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
dashboard: {
|
||||
query: DASHBOARD,
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
dashboard!: IDashboard;
|
||||
ModerationRouteName = ModerationRouteName;
|
||||
}
|
||||
</script>
|
@ -53,8 +53,8 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="column sidebar">
|
||||
<div class="field has-addons" v-if="actorIsOrganizer()">
|
||||
<p class="control">
|
||||
<div class="field has-addons">
|
||||
<p class="control" v-if="actorIsOrganizer()">
|
||||
<router-link
|
||||
class="button"
|
||||
:to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
|
||||
@ -62,11 +62,16 @@
|
||||
<translate>Edit</translate>
|
||||
</router-link>
|
||||
</p>
|
||||
<p class="control">
|
||||
<p class="control" v-if="actorIsOrganizer()">
|
||||
<a class="button is-danger" @click="openDeleteEventModal()">
|
||||
<translate>Delete</translate>
|
||||
</a>
|
||||
</p>
|
||||
<p class="control">
|
||||
<a class="button is-danger" @click="isReportModalActive = true">
|
||||
<translate>Report</translate>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="address-wrapper">
|
||||
<b-icon icon="map" />
|
||||
@ -224,6 +229,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<b-modal :active.sync="isReportModalActive" has-modal-card>
|
||||
<report-modal :on-confirm="reportEvent" title="Report this event" :outside-domain="event.organizerActor.domain" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -241,6 +249,9 @@ import BIcon from 'buefy/src/components/icon/Icon.vue';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import EventFullDate from '@/components/Event/EventFullDate.vue';
|
||||
import ActorLink from '@/components/Account/ActorLink.vue';
|
||||
import ReportModal from '@/components/Report/ReportModal.vue';
|
||||
import { IReport } from '@/types/report.model';
|
||||
import { CREATE_REPORT } from '@/graphql/report';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@ -249,6 +260,7 @@ import ActorLink from '@/components/Account/ActorLink.vue';
|
||||
EventCard,
|
||||
BIcon,
|
||||
DateCalendarIcon,
|
||||
ReportModal,
|
||||
// tslint:disable:space-in-parens
|
||||
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
|
||||
// tslint:enable
|
||||
@ -274,6 +286,7 @@ export default class Event extends Vue {
|
||||
loggedPerson!: IPerson;
|
||||
validationSent: boolean = false;
|
||||
showMap: boolean = false;
|
||||
isReportModalActive: boolean = false;
|
||||
|
||||
EventVisibility = EventVisibility;
|
||||
|
||||
@ -285,24 +298,47 @@ export default class Event extends Vue {
|
||||
type: 'is-danger',
|
||||
title: this.$gettext('Delete event'),
|
||||
message: this.$gettextInterpolate(
|
||||
`${prefix}` +
|
||||
'Are you sure you want to delete this event? This action cannot be reverted. <br /><br />' +
|
||||
'To confirm, type your event title "%{eventTitle}"',
|
||||
{ participants: this.event.participants.length, eventTitle: this.event.title },
|
||||
`${prefix}` +
|
||||
'Are you sure you want to delete this event? This action cannot be reverted. <br /><br />' +
|
||||
'To confirm, type your event title "%{eventTitle}"',
|
||||
{ participants: this.event.participants.length, eventTitle: this.event.title },
|
||||
),
|
||||
confirmText: this.$gettextInterpolate(
|
||||
'Delete %{eventTitle}',
|
||||
{ eventTitle: this.event.title },
|
||||
'Delete %{eventTitle}',
|
||||
{ eventTitle: this.event.title },
|
||||
),
|
||||
inputAttrs: {
|
||||
placeholder: this.event.title,
|
||||
pattern: this.event.title,
|
||||
},
|
||||
|
||||
onConfirm: () => this.deleteEvent(),
|
||||
});
|
||||
}
|
||||
|
||||
async reportEvent(content: string, forward: boolean) {
|
||||
this.isReportModalActive = false;
|
||||
const eventTitle = this.event.title;
|
||||
try {
|
||||
await this.$apollo.mutate<IReport>({
|
||||
mutation: CREATE_REPORT,
|
||||
variables: {
|
||||
eventId: this.event.id,
|
||||
reporterActorId: this.loggedPerson.id,
|
||||
reportedActorId: this.event.organizerActor.id,
|
||||
content,
|
||||
},
|
||||
});
|
||||
this.$buefy.notification.open({
|
||||
message: this.$gettextInterpolate('Event %{eventTitle} reported', { eventTitle }),
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
duration: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async joinEvent() {
|
||||
try {
|
||||
await this.$apollo.mutate<{ joinEvent: IParticipant }>({
|
||||
@ -408,7 +444,7 @@ export default class Event extends Vue {
|
||||
await this.$apollo.mutate<IParticipant>({
|
||||
mutation: DELETE_EVENT,
|
||||
variables: {
|
||||
id: this.event.id,
|
||||
eventId: this.event.id,
|
||||
actorId: this.loggedPerson.id,
|
||||
},
|
||||
});
|
||||
|
80
js/src/views/Moderation/Logs.vue
Normal file
80
js/src/views/Moderation/Logs.vue
Normal file
@ -0,0 +1,80 @@
|
||||
import {ReportStatusEnum} from "@/types/report.model";
|
||||
<template>
|
||||
<section class="container">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><router-link :to="{ name: AdminRouteName.DASHBOARD }">Dashboard</router-link></li>
|
||||
<li class="is-active"><router-link :to="{ name: ModerationRouteName.LOGS }" aria-current="page">Logs</router-link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<ul v-if="actionLogs.length > 0">
|
||||
<li v-for="log in actionLogs">
|
||||
<div class="box">
|
||||
<img class="image" :src="log.actor.avatar.url" />
|
||||
<span>@{{ log.actor.preferredUsername }}</span>
|
||||
<span v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED">
|
||||
closed <router-link :to="{ name: ModerationRouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED">
|
||||
reopened <router-link :to="{ name: ModerationRouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED">
|
||||
marked <router-link :to="{ name: ModerationRouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link> as resolved
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.NOTE_CREATION">
|
||||
added a note on
|
||||
<router-link v-if="log.object.report" :to="{ name: ModerationRouteName.REPORT, params: { reportId: log.object.report.id } }">report #{{ log.object.report.id }}</router-link>
|
||||
<span v-else>a non-existent report</span>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.EVENT_DELETION">
|
||||
deleted an event named « {{ log.object.title }} »
|
||||
</span>
|
||||
<br />
|
||||
<small>{{ log.insertedAt | formatDateTimeString }}</small>
|
||||
</div>
|
||||
<!-- <pre>{{ log }}</pre>-->
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<b-message type="is-info">No moderation logs yet</b-message>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { IActionLog, ActionLogAction } from '@/types/report.model';
|
||||
import { LOGS } from '@/graphql/report';
|
||||
import ReportCard from '@/components/Report/ReportCard.vue';
|
||||
import { AdminRouteName } from '@/router/admin';
|
||||
import { ModerationRouteName } from '@/router/moderation';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ReportCard,
|
||||
},
|
||||
apollo: {
|
||||
actionLogs: {
|
||||
query: LOGS,
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ReportList extends Vue {
|
||||
|
||||
actionLogs?: IActionLog[] = [];
|
||||
|
||||
ActionLogAction = ActionLogAction;
|
||||
AdminRouteName = AdminRouteName;
|
||||
ModerationRouteName = ModerationRouteName;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.container li {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
271
js/src/views/Moderation/Report.vue
Normal file
271
js/src/views/Moderation/Report.vue
Normal file
@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||
<div class="container" v-if="report">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><router-link :to="{ name: AdminRouteName.DASHBOARD }">Dashboard</router-link></li>
|
||||
<li><router-link :to="{ name: ModerationRouteName.REPORTS }">Reports</router-link></li>
|
||||
<li class="is-active"><router-link :to="{ name: ModerationRouteName.REPORT, params: { reportId: this.report.id} }" aria-current="page">Report</router-link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="buttons">
|
||||
<b-button v-if="report.status !== ReportStatusEnum.RESOLVED" @click="updateReport(ReportStatusEnum.RESOLVED)" type="is-primary">Mark as resolved</b-button>
|
||||
<b-button v-if="report.status !== ReportStatusEnum.OPEN" @click="updateReport(ReportStatusEnum.OPEN)" type="is-success">Reopen</b-button>
|
||||
<b-button v-if="report.status !== ReportStatusEnum.CLOSED" @click="updateReport(ReportStatusEnum.CLOSED)" type="is-danger">Close</b-button>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="table-container">
|
||||
<table class="box table is-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Compte signalé</td>
|
||||
<td>
|
||||
<router-link :to="{ name: ActorRouteName.PROFILE, params: { name: report.reported.preferredUsername } }">
|
||||
<img v-if="report.reported.avatar" class="image" :src="report.reported.avatar.url" /> @{{ report.reported.preferredUsername }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Signalé par</td>
|
||||
<td>
|
||||
<router-link :to="{ name: ActorRouteName.PROFILE, params: { name: report.reporter.preferredUsername } }">
|
||||
<img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Signalé</td>
|
||||
<td>{{ report.insertedAt | formatDateTimeString }}</td>
|
||||
</tr>
|
||||
<tr v-if="report.updatedAt !== report.insertedAt">
|
||||
<td>Mis à jour</td>
|
||||
<td>{{ report.updatedAt | formatDateTimeString }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Statut</td>
|
||||
<td>
|
||||
<span v-if="report.status === ReportStatusEnum.OPEN">Ouvert</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.CLOSED">Fermé</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">Résolu</span>
|
||||
<span v-else>Inconnu</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="box">
|
||||
<p v-if="report.content">{{ report.content }}</p>
|
||||
<p v-else>Pas de commentaire</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" v-if="report.event">
|
||||
<router-link :to="{ name: EventRouteName.EVENT, params: { uuid: report.event.uuid }}">
|
||||
<h3 class="title">{{ report.event.title }}</h3>
|
||||
<p v-html="report.event.description"></p>
|
||||
</router-link>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
type="is-primary"
|
||||
:to="{ name: EventRouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"
|
||||
icon-left="pencil"
|
||||
size="is-small">Edit</b-button>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small">Delete</b-button>
|
||||
</div>
|
||||
|
||||
<h2 class="title" v-if="report.notes.length > 0">Notes</h2>
|
||||
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
|
||||
<p>{{ note.content }}</p>
|
||||
<router-link :to="{ name: ActorRouteName.PROFILE, params: { name: note.moderator.preferredUsername } }">
|
||||
<img class="image" :src="note.moderator.avatar.url" /> @{{ note.moderator.preferredUsername }}
|
||||
</router-link><br />
|
||||
<small><a :href="`#note-${note.id}`" v-if="note.insertedAt">{{ note.insertedAt | formatDateTimeString }}</a></small>
|
||||
</div>
|
||||
|
||||
<form @submit="addNote()">
|
||||
<b-field label="Nouvelle note">
|
||||
<b-input type="textarea" v-model="noteContent"></b-input>
|
||||
</b-field>
|
||||
<b-button type="submit" @click="addNote">Ajouter une 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';
|
||||