Migrate to Vue 3 and Vite

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-07-12 10:55:28 +02:00
parent 8f4099ee33
commit ee20e03cc2
464 changed files with 31515 additions and 32758 deletions

View File

@@ -22,180 +22,193 @@
:alt="log.actor.avatar.alt || ''"
v-if="log.actor.avatar"
/>
<i18n
<i18n-t
v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED"
tag="span"
path="{moderator} closed {report}"
keypath="{moderator} closed {report}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.id },
}"
slot="report"
>{{
$t("report #{report_number}", {
report_number: log.object.id,
})
}}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #report>
<router-link
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.id },
}"
>{{
$t("report #{report_number}", {
report_number: log.object.id,
})
}}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED"
tag="span"
path="{moderator} reopened {report}"
keypath="{moderator} reopened {report}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.id },
}"
slot="report"
>{{
$t("report #{report_number}", {
report_number: log.object.id,
})
}}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #report>
<router-link
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.id },
}"
>{{
$t("report #{report_number}", {
report_number: log.object.id,
})
}}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED"
tag="span"
path="{moderator} marked {report} as resolved"
keypath="{moderator} marked {report} as resolved"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.id },
}"
slot="report"
>{{
$t("report #{report_number}", {
report_number: log.object.id,
})
}}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #report>
<router-link
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.id },
}"
>{{
$t("report #{report_number}", {
report_number: log.object.id,
})
}}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="log.action === ActionLogAction.NOTE_CREATION"
tag="span"
path="{moderator} added a note on {report}"
keypath="{moderator} added a note on {report}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
v-if="log.object.report"
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.report.id },
}"
slot="report"
>{{
$t("report #{report_number}", {
report_number: log.object.report.id,
})
}}
</router-link>
<span v-else slot="report">{{
$t("a non-existent report")
}}</span>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #report>
<router-link
v-if="log.object.report"
:to="{
name: RouteName.REPORT,
params: { reportId: log.object.report.id },
}"
>{{
$t("report #{report_number}", {
report_number: log.object.report.id,
})
}}
</router-link>
<span v-else>{{ $t("a non-existent report") }}</span>
</template>
</i18n-t>
<i18n-t
v-else-if="log.action === ActionLogAction.EVENT_DELETION"
tag="span"
path='{moderator} deleted an event named "{title}"'
keypath='{moderator} deleted an event named "{title}"'
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<b slot="title">{{ log.object.title }}</b>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #title>
<b>{{ log.object.title }}</b>
</template>
</i18n-t>
<i18n-t
v-else-if="
log.action === ActionLogAction.ACTOR_SUSPENSION &&
log.object.__typename == 'Person'
"
tag="span"
path="{moderator} suspended profile {profile}"
keypath="{moderator} suspended profile {profile}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #profile>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="
log.action === ActionLogAction.ACTOR_UNSUSPENSION &&
log.object.__typename == 'Person'
"
tag="span"
path="{moderator} has unsuspended profile {profile}"
keypath="{moderator} has unsuspended profile {profile}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #profile>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="
log.action === ActionLogAction.ACTOR_SUSPENSION &&
log.object.__typename == 'Group'
@@ -203,155 +216,167 @@
tag="span"
path="{moderator} suspended group {profile}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{
name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #profile>
<router-link
:to="{
name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="
log.action === ActionLogAction.ACTOR_UNSUSPENSION &&
log.object.__typename == 'Group'
"
tag="span"
path="{moderator} has unsuspended group {profile}"
keypath="{moderator} has unsuspended group {profile}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="profile"
:to="{
name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</i18n>
<i18n
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #profile>
<router-link
:to="{
name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id },
}"
>{{ displayNameAndUsername(log.object) }}
</router-link>
</template>
</i18n-t>
<i18n-t
v-else-if="log.action === ActionLogAction.USER_DELETION"
tag="span"
path="{moderator} has deleted user {user}"
keypath="{moderator} has deleted user {user}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
v-if="log.object.confirmedAt"
slot="user"
:to="{
name: RouteName.ADMIN_USER_PROFILE,
params: { id: log.object.id },
}"
>{{ log.object.email }}
</router-link>
<b v-else slot="user">{{ log.object.email }}</b>
</i18n>
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #user>
<router-link
v-if="log.object.confirmedAt"
:to="{
name: RouteName.ADMIN_USER_PROFILE,
params: { id: log.object.id },
}"
>{{ log.object.email }}
</router-link>
<b v-else>{{ log.object.email }}</b>
</template>
</i18n-t>
<span
v-else-if="
log.action === ActionLogAction.COMMENT_DELETION &&
log.object.event
"
>
<i18n
<i18n-t
tag="span"
path="{moderator} has deleted a comment from {author} under the event {event}"
keypath="{moderator} has deleted a comment from {author} under the event {event}"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
v-if="log.object.event && log.object.event.uuid"
slot="event"
:to="{
name: RouteName.EVENT,
params: { uuid: log.object.event.uuid },
}"
>{{ log.object.event.title }}
</router-link>
<b v-else slot="event">{{ log.object.event.title }}</b>
<router-link
slot="author"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.actor.id },
}"
>{{ displayNameAndUsername(log.object.actor) }}
</router-link>
</i18n>
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #event>
<router-link
v-if="log.object.event && log.object.event.uuid"
:to="{
name: RouteName.EVENT,
params: { uuid: log.object.event.uuid },
}"
>{{ log.object.event.title }}
</router-link>
<b v-else>{{ log.object.event.title }}</b>
</template>
<template #author>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.actor.id },
}"
>{{ displayNameAndUsername(log.object.actor) }}
</router-link>
</template>
</i18n-t>
<pre v-html="log.object.text" />
</span>
<span v-else-if="log.action === ActionLogAction.COMMENT_DELETION">
<i18n
<i18n-t
tag="span"
path="{moderator} has deleted a comment from {author}"
keypath="{moderator} has deleted a comment from {author}"
>
<template #moderator>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</template>
<template #author>
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.actor.id },
}"
>{{ displayNameAndUsername(log.object.actor) }}
</router-link>
</template>
</i18n-t>
<pre v-html="log.object.text" />
</span>
<i18n-t
v-else
tag="span"
keypath="{moderator} has done an unknown action"
>
<template #moderator>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
<router-link
slot="author"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.object.actor.id },
}"
>{{ displayNameAndUsername(log.object.actor) }}
</router-link>
</i18n>
<pre v-html="log.object.text" />
</span>
<i18n
v-else
tag="span"
path="{moderator} has done an unknown action"
>
<router-link
slot="moderator"
:to="{
name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id },
}"
>@{{ log.actor.preferredUsername }}</router-link
>
</i18n>
</template>
</i18n-t>
<br />
<small>{{ log.insertedAt | formatDateTimeString }}</small>
<small>{{ formatDateTimeString(log.insertedAt) }}</small>
</div>
</li>
</ul>
<b-pagination
<o-pagination
:total="actionLogs.total"
v-model="page"
:per-page="LOGS_PER_PAGE"
@@ -360,70 +385,49 @@
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</o-pagination>
</section>
<div v-else>
<b-message type="is-info">{{ $t("No moderation logs yet") }}</b-message>
<o-notification variant="info">{{
$t("No moderation logs yet")
}}</o-notification>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
<script lang="ts" setup>
import { IActionLog } from "@/types/report.model";
import { LOGS } from "@/graphql/report";
import ReportCard from "@/components/Report/ReportCard.vue";
import { ActionLogAction } from "@/types/enums";
import RouteName from "../../router/name";
import { displayNameAndUsername } from "../../types/actor";
import { Paginate } from "@/types/paginate";
import { useQuery } from "@vue/apollo-composable";
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
import { useHead } from "@vueuse/head";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { formatDateTimeString } from "@/filters/datetime";
@Component({
components: {
ReportCard,
},
apollo: {
actionLogs: {
fetchPolicy: "cache-and-network",
query: LOGS,
variables() {
return {
page: this.page,
limit: this.LOGS_PER_PAGE,
};
},
},
},
metaInfo() {
return {
title: this.$t("Moderation logs") as string,
};
},
})
export default class ReportList extends Vue {
actionLogs?: Paginate<IActionLog> = { total: 0, elements: [] };
const LOGS_PER_PAGE = 10;
page = parseInt((this.$route.query.page as string) || "1", 10);
const page = useRouteQuery("page", 1, integerTransformer);
LOGS_PER_PAGE = 10;
const { result: actionLogsResult } = useQuery<{
actionLogs: Paginate<IActionLog>;
}>(LOGS, () => ({
page: page.value,
limit: LOGS_PER_PAGE,
}));
ActionLogAction = ActionLogAction;
const actionLogs = computed(
() => actionLogsResult.value?.actionLogs ?? { total: 0, elements: [] }
);
RouteName = RouteName;
const { t } = useI18n({ useScope: "global" });
displayNameAndUsername = displayNameAndUsername;
mounted(): void {
this.page = parseInt((this.$route.query.page as string) || "1", 10);
}
@Watch("page")
triggerLoadMoreMemberPageChange(page: string): void {
this.$router.replace({
name: RouteName.REPORT_LOGS,
query: { ...this.$route.query, page },
});
}
}
useHead({
title: computed(() => t("Moderation logs")),
});
</script>
<style lang="scss" scoped>
img.image {

View File

@@ -5,54 +5,54 @@
:links="[
{
name: RouteName.MODERATION,
text: $t('Moderation'),
text: t('Moderation'),
},
{
name: RouteName.REPORTS,
text: $t('Reports'),
text: t('Reports'),
},
{
name: RouteName.REPORT,
params: { id: report.id },
text: $t('Report #{reportNumber}', { reportNumber: report.id }),
text: t('Report #{reportNumber}', { reportNumber: report.id }),
},
]"
/>
<section>
<b-message
<o-notification
title="Error"
type="is-danger"
variant="danger"
v-for="error in errors"
:key="error"
>
{{ error }}
</b-message>
<div class="container" v-if="report">
<div class="buttons">
<b-button
</o-notification>
<div class="container mx-auto" v-if="report">
<div class="flex flex-wrap gap-2">
<o-button
v-if="report.status !== ReportStatusEnum.RESOLVED"
@click="updateReport(ReportStatusEnum.RESOLVED)"
type="is-primary"
>{{ $t("Mark as resolved") }}</b-button
variant="primary"
>{{ t("Mark as resolved") }}</o-button
>
<b-button
<o-button
v-if="report.status !== ReportStatusEnum.OPEN"
@click="updateReport(ReportStatusEnum.OPEN)"
type="is-success"
>{{ $t("Reopen") }}</b-button
variant="success"
>{{ t("Reopen") }}</o-button
>
<b-button
<o-button
v-if="report.status !== ReportStatusEnum.CLOSED"
@click="updateReport(ReportStatusEnum.CLOSED)"
type="is-danger"
>{{ $t("Close") }}</b-button
variant="danger"
>{{ t("Close") }}</o-button
>
</div>
<div class="table-container">
<table class="table is-striped is-fullwidth">
<div class="w-full">
<table class="table w-full">
<tbody>
<tr v-if="report.reported.__typename === 'Group'">
<td>{{ $t("Reported group") }}</td>
<td>{{ t("Reported group") }}</td>
<td>
<router-link
:to="{
@@ -72,7 +72,7 @@
</tr>
<tr v-else>
<td>
{{ $t("Reported identity") }}
{{ t("Reported identity") }}
</td>
<td>
<router-link
@@ -92,7 +92,7 @@
</td>
</tr>
<tr>
<td>{{ $t("Reported by") }}</td>
<td>{{ t("Reported by") }}</td>
<td v-if="report.reporter.type === ActorType.APPLICATION">
{{ report.reporter.domain }}
</td>
@@ -114,30 +114,30 @@
</td>
</tr>
<tr>
<td>{{ $t("Reported") }}</td>
<td>{{ report.insertedAt | formatDateTimeString }}</td>
<td>{{ t("Reported") }}</td>
<td>{{ formatDateTimeString(report.insertedAt) }}</td>
</tr>
<tr v-if="report.updatedAt !== report.insertedAt">
<td>{{ $t("Updated") }}</td>
<td>{{ report.updatedAt | formatDateTimeString }}</td>
<td>{{ t("Updated") }}</td>
<td>{{ formatDateTimeString(report.updatedAt) }}</td>
</tr>
<tr>
<td>{{ $t("Status") }}</td>
<td>{{ t("Status") }}</td>
<td>
<span v-if="report.status === ReportStatusEnum.OPEN">{{
$t("Open")
t("Open")
}}</span>
<span v-else-if="report.status === ReportStatusEnum.CLOSED">
{{ $t("Closed") }}
{{ t("Closed") }}
</span>
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">
{{ $t("Resolved") }}
{{ t("Resolved") }}
</span>
<span v-else>{{ $t("Unknown") }}</span>
<span v-else>{{ t("Unknown") }}</span>
</td>
</tr>
<tr v-if="report.event && report.comments.length > 0">
<td>{{ $t("Event") }}</td>
<td>{{ t("Event") }}</td>
<td>
<router-link
:to="{
@@ -148,18 +148,18 @@
{{ report.event.title }}
</router-link>
<span class="is-pulled-right">
<!-- <b-button-->
<!-- <o-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- variant="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"
<!-- size="small">{{ t('Edit') }}</o-button>-->
<o-button
variant="danger"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
size="small"
>{{ t("Delete") }}</o-button
>
</span>
</td>
@@ -168,68 +168,68 @@
</table>
</div>
<div class="box report-content">
<div class="">
<p v-if="report.content" v-html="nl2br(report.content)" />
<p v-else>{{ $t("No comment") }}</p>
<p v-else>{{ t("No comment") }}</p>
</div>
<div class="box" v-if="report.event && report.comments.length === 0">
<div class="" 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" />
</router-link>
<!-- <b-button-->
<!-- <o-button-->
<!-- tag="router-link"-->
<!-- type="is-primary"-->
<!-- variant="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"
<!-- size="small">{{ t('Edit') }}</o-button>-->
<o-button
variant="danger"
@click="confirmEventDelete()"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
size="small"
>{{ t("Delete") }}</o-button
>
</div>
<div v-if="report.comments.length > 0">
<ul v-for="comment in report.comments" :key="comment.id">
<li>
<div class="box" v-if="comment">
<article class="media">
<div class="media-left">
<div class="" v-if="comment">
<article class="flex gap-1">
<div class="">
<figure
class="image is-48x48"
class=""
v-if="comment.actor && comment.actor.avatar"
>
<img :src="comment.actor.avatar.url" alt="Image" />
<img
:src="comment.actor.avatar.url"
alt=""
width="48"
height="48"
/>
</figure>
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<AccountCircle :size="48" v-else />
</div>
<div class="media-content">
<div class="content">
<div class="">
<div class="prose dark:prose-invert">
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ $t("Unknown actor") }}</span>
<span v-else>{{ t("Unknown actor") }}</span>
<br />
<p v-html="comment.text" />
</div>
<b-button
type="is-danger"
<o-button
variant="danger"
@click="confirmCommentDelete(comment)"
icon-left="delete"
size="is-small"
>{{ $t("Delete") }}</b-button
size="small"
>{{ t("Delete") }}</o-button
>
</div>
</article>
@@ -238,7 +238,7 @@
</ul>
</div>
<h2 class="title" v-if="report.notes.length > 0">{{ $t("Notes") }}</h2>
<h2 v-if="report.notes.length > 0">{{ t("Notes") }}</h2>
<div
class="box note"
v-for="note in report.notes"
@@ -253,8 +253,8 @@
}"
>
<img
alt
class="image"
alt=""
class="rounded-full"
:src="note.moderator.avatar.url"
v-if="note.moderator.avatar"
/>
@@ -263,243 +263,245 @@
<br />
<small>
<a :href="`#note-${note.id}`" v-if="note.insertedAt">
{{ note.insertedAt | formatDateTimeString }}
{{ formatDateTimeString(note.insertedAt) }}
</a>
</small>
</div>
<form @submit="addNote()">
<b-field :label="$t('New note')" label-for="newNoteInput">
<b-input
<form
@submit="
createReportNoteMutation({
reportId: report?.id,
content: noteContent,
})
"
>
<o-field :label="t('New note')" label-for="newNoteInput">
<o-input
type="textarea"
v-model="noteContent"
id="newNoteInput"
></b-input>
</b-field>
<b-button type="submit" @click="addNote">{{
$t("Add a note")
}}</b-button>
></o-input>
</o-field>
<o-button class="mt-2" type="submit">{{ t("Add a note") }}</o-button>
</form>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
<script lang="ts" setup>
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report";
import { IReport, IReportNote } from "@/types/report.model";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { IPerson, displayNameAndUsername } from "@/types/actor";
import { displayNameAndUsername } from "@/types/actor";
import { DELETE_EVENT } from "@/graphql/event";
import uniq from "lodash/uniq";
import { nl2br } from "@/utils/html";
import { DELETE_COMMENT } from "@/graphql/comment";
import { IComment } from "@/types/comment.model";
import { ActorType, ReportStatusEnum } from "@/types/enums";
import RouteName from "../../router/name";
import RouteName from "@/router/name";
import { GraphQLError } from "graphql";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useHead } from "@vueuse/head";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { ref, computed, inject } from "vue";
import { formatDateTimeString } from "@/filters/datetime";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
import { Dialog } from "@/plugins/dialog";
import { Notifier } from "@/plugins/notifier";
@Component({
apollo: {
report: {
const router = useRouter();
const props = defineProps<{ reportId: string }>();
const { currentActor } = useCurrentActorClient();
const { result: reportResult, onError: onReportQueryError } = useQuery<{
report: IReport;
}>(REPORT, () => ({
id: props.reportId,
}));
const report = computed(() => reportResult.value?.report);
onReportQueryError(({ graphQLErrors }) => {
errors.value = uniq(
graphQLErrors.map(({ message }: GraphQLError) => message)
);
});
const { t } = useI18n({ useScope: "global" });
useHead({
title: computed(() => t("Report")),
});
const notifier = inject<Notifier>("notifier");
const errors = ref<string[]>([]);
const noteContent = ref("");
const {
mutate: createReportNoteMutation,
onDone: createReportNoteMutationDone,
onError: createReportNoteMutationError,
} = useMutation<{
createReportNote: IReportNote;
}>(CREATE_REPORT_NOTE, () => ({
update: (
store: ApolloCache<{ createReportNote: IReportNote }>,
{ data }: FetchResult
) => {
if (data == null) return;
const cachedData = store.readQuery<{ report: IReport }>({
query: REPORT,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.reportId,
};
},
error({ graphQLErrors }) {
this.errors = uniq(
graphQLErrors.map(({ message }: GraphQLError) => message)
);
},
},
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
variables: { id: report.value.id },
});
if (cachedData == null) return;
const { report } = cachedData;
if (report === null) {
console.error("Cannot update event notes cache, because of null value.");
return;
}
const note = data.createReportNote;
note.moderator = currentActor.value;
report.notes = report.notes.concat([note]);
store.writeQuery({
query: REPORT,
variables: { id: report.value.id },
data: { report },
});
},
metaInfo() {
return {
title: this.$t("Report") as string,
titleTemplate: "%s | Mobilizon",
}));
createReportNoteMutationDone(() => {
noteContent.value = "";
});
createReportNoteMutationError((error) => {
console.error(error);
});
const dialog = inject<Dialog>("dialog");
const confirmEventDelete = (): void => {
dialog?.confirm({
title: t("Deleting event"),
message: t(
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead."
),
confirmText: t("Delete Event"),
type: "danger",
hasIcon: true,
onConfirm: () => deleteEvent(),
});
};
const confirmCommentDelete = (comment: IComment): void => {
dialog?.confirm({
title: t("Deleting comment"),
message: t(
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone."
),
confirmText: t("Delete Comment"),
type: "danger",
hasIcon: true,
onConfirm: () => deleteCommentMutation({ commentId: comment.id }),
});
};
const {
mutate: deleteEventMutation,
onDone: deleteEventMutationDone,
onError: deleteEventMutationError,
} = useMutation<{ deleteEvent: { id: string } }>(DELETE_EVENT);
deleteEventMutationDone(() => {
const eventTitle = report.value?.event?.title;
notifier?.success(
t("Event {eventTitle} deleted", {
eventTitle,
})
);
});
deleteEventMutationError((error) => {
console.error(error);
});
const deleteEvent = async (): Promise<void> => {
if (!report.value?.event?.id) return;
deleteEventMutation({ eventId: report.value.event.id });
};
const {
mutate: deleteCommentMutation,
onDone: deleteCommentMutationDone,
onError: deleteCommentMutationError,
} = useMutation<{ deleteComment: { id: string } }>(DELETE_COMMENT);
deleteCommentMutationDone(() => {
notifier?.success(t("Comment deleted") as string);
});
deleteCommentMutationError((error) => {
console.error(error);
});
const {
mutate: updateReportMutation,
onDone: onUpdateReportMutation,
onError: onUpdateReportError,
} = useMutation(UPDATE_REPORT, () => ({
update: (
store: ApolloCache<{ updateReportStatus: IReport }>,
{ data }: FetchResult
) => {
if (data == null) return;
const reportCachedData = store.readQuery<{ report: IReport }>({
query: REPORT,
variables: { id: report.value.id },
});
if (reportCachedData == null) return;
const { report } = reportCachedData;
if (report === null) {
console.error("Cannot update event notes cache, because of null value.");
return;
}
const updatedReport = {
...report,
status: data.updateReportStatus.status,
};
store.writeQuery({
query: REPORT,
variables: { id: report.value.id },
data: { report: updatedReport },
});
},
})
export default class Report extends Vue {
@Prop({ required: true }) reportId!: number;
}));
report!: IReport;
onUpdateReportMutation(() => {
router.push({ name: RouteName.REPORTS });
});
currentActor!: IPerson;
onUpdateReportError((error) => {
console.error(error);
});
errors: string[] = [];
ReportStatusEnum = ReportStatusEnum;
RouteName = RouteName;
ActorType = ActorType;
nl2br = nl2br;
noteContent = "";
displayNameAndUsername = displayNameAndUsername;
addNote(): void {
try {
this.$apollo.mutate<{ createReportNote: IReportNote }>({
mutation: CREATE_REPORT_NOTE,
variables: {
reportId: this.report.id,
content: this.noteContent,
},
update: (
store: ApolloCache<{ createReportNote: IReportNote }>,
{ data }: FetchResult
) => {
if (data == null) return;
const cachedData = store.readQuery<{ report: IReport }>({
query: REPORT,
variables: { id: this.report.id },
});
if (cachedData == null) return;
const { report } = cachedData;
if (report === null) {
console.error(
"Cannot update event notes cache, because of null value."
);
return;
}
const note = data.createReportNote;
note.moderator = this.currentActor;
report.notes = report.notes.concat([note]);
store.writeQuery({
query: REPORT,
variables: { id: this.report.id },
data: { report },
});
},
});
this.noteContent = "";
} catch (error) {
console.error(error);
}
}
confirmEventDelete(): void {
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 discussion with the event creator or edit its event instead."
) as string,
confirmText: this.$t("Delete Event") as string,
type: "is-danger",
hasIcon: true,
onConfirm: () => this.deleteEvent(),
});
}
confirmCommentDelete(comment: IComment): void {
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(): Promise<void> {
if (!this.report.event || !this.report.event.id) return;
const eventTitle = this.report.event.title;
try {
await this.$apollo.mutate({
mutation: DELETE_EVENT,
variables: {
eventId: this.report.event.id.toString(),
},
});
this.$buefy.notification.open({
message: this.$t("Event {eventTitle} deleted", {
eventTitle,
}) as string,
type: "is-success",
position: "is-bottom-right",
duration: 5000,
});
} catch (error) {
console.error(error);
}
}
async deleteComment(comment: IComment): Promise<void> {
try {
await this.$apollo.mutate({
mutation: DELETE_COMMENT,
variables: {
commentId: comment.id,
},
});
this.$notifier.success(this.$t("Comment deleted") as string);
} catch (error) {
console.error(error);
}
}
async updateReport(status: ReportStatusEnum): Promise<void> {
try {
await this.$apollo.mutate<{ updateReportStatus: IReport }>({
mutation: UPDATE_REPORT,
variables: {
reportId: this.report.id,
status,
},
update: (
store: ApolloCache<{ updateReportStatus: IReport }>,
{ data }: FetchResult
) => {
if (data == null) return;
const reportCachedData = store.readQuery<{ report: IReport }>({
query: REPORT,
variables: { id: this.report.id },
});
if (reportCachedData == null) return;
const { report } = reportCachedData;
if (report === null) {
console.error(
"Cannot update event notes cache, because of null value."
);
return;
}
const updatedReport = {
...report,
status: data.updateReportStatus.status,
};
store.writeQuery({
query: REPORT,
variables: { id: this.report.id },
data: { report: updatedReport },
});
},
});
await this.$router.push({ name: RouteName.REPORTS });
} catch (error) {
console.error(error);
}
}
}
const updateReport = async (status: ReportStatusEnum): Promise<void> => {
updateReportMutation({
reportId: report.value?.id,
status,
});
};
</script>
<style lang="scss" scoped>
tbody td img.image,

View File

@@ -14,35 +14,29 @@
/>
<section>
<div class="flex flex-wrap gap-2">
<b-field :label="$t('Report status')">
<b-radio-button
v-model="status"
:native-value="ReportStatusEnum.OPEN"
>{{ $t("Open") }}</b-radio-button
>
<b-radio-button
v-model="status"
:native-value="ReportStatusEnum.RESOLVED"
>{{ $t("Resolved") }}</b-radio-button
>
<b-radio-button
v-model="status"
:native-value="ReportStatusEnum.CLOSED"
>{{ $t("Closed") }}</b-radio-button
>
</b-field>
<b-field
<o-field :label="$t('Report status')">
<o-radio v-model="status" :native-value="ReportStatusEnum.OPEN">{{
$t("Open")
}}</o-radio>
<o-radio v-model="status" :native-value="ReportStatusEnum.RESOLVED">{{
$t("Resolved")
}}</o-radio>
<o-radio v-model="status" :native-value="ReportStatusEnum.CLOSED">{{
$t("Closed")
}}</o-radio>
</o-field>
<o-field
:label="$t('Domain')"
label-for="domain-filter"
class="flex-auto"
>
<b-input
<o-input
id="domain-filter"
:placeholder="$t('mobilizon-instance.tld')"
:value="filterDomain"
@input="debouncedUpdateDomainFilter"
/>
</b-field>
</o-field>
</div>
<ul v-if="reports.elements.length > 0">
<li v-for="report in reports.elements" :key="report.id">
@@ -76,7 +70,7 @@
{{ $t("No closed reports yet") }}
</empty-content>
</div>
<b-pagination
<o-pagination
:total="reports.total"
v-model="page"
:simple="true"
@@ -86,116 +80,65 @@
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</o-pagination>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
<script lang="ts" setup>
import { IReport } from "@/types/report.model";
import { REPORTS } from "@/graphql/report";
import ReportCard from "@/components/Report/ReportCard.vue";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import { ReportStatusEnum } from "@/types/enums";
import RouteName from "../../router/name";
import VueRouter from "vue-router";
import { Paginate } from "@/types/paginate";
import debounce from "lodash/debounce";
const { isNavigationFailure, NavigationFailureType } = VueRouter;
import { useQuery } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { useHead } from "@vueuse/head";
import { computed, ref } from "vue";
import {
enumTransformer,
integerTransformer,
useRouteQuery,
} from "vue-use-route-query";
const REPORT_PAGE_LIMIT = 10;
const page = useRouteQuery("page", 1, integerTransformer);
const filterDomain = useRouteQuery("filterDomain", "");
const status = useRouteQuery(
"status",
ReportStatusEnum.OPEN,
enumTransformer(ReportStatusEnum)
);
@Component({
components: {
ReportCard,
EmptyContent,
},
apollo: {
reports: {
fetchPolicy: "cache-and-network",
query: REPORTS,
variables() {
return {
page: this.page,
status: this.status,
limit: REPORT_PAGE_LIMIT,
domain: this.filterDomain,
};
},
pollInterval: 120000, // 2 minutes
},
},
metaInfo() {
return {
title: this.$t("Reports") as string,
};
},
})
export default class ReportList extends Vue {
reports?: Paginate<IReport> = { elements: [], total: 0 };
const { result: reportsResult } = useQuery<{ reports: Paginate<IReport> }>(
REPORTS,
() => ({
page: page.value,
status: status.value,
limit: REPORT_PAGE_LIMIT,
domain: filterDomain.value,
})
);
RouteName = RouteName;
const reports = computed(
() => reportsResult.value?.reports ?? { elements: [], total: 0 }
);
ReportStatusEnum = ReportStatusEnum;
const { t } = useI18n({ useScope: "global" });
filterReports: ReportStatusEnum = ReportStatusEnum.OPEN;
useHead({
title: computed(() => t("Reports")),
});
REPORT_PAGE_LIMIT = REPORT_PAGE_LIMIT;
const filterReports = ref<ReportStatusEnum>(ReportStatusEnum.OPEN);
data(): Record<string, unknown> {
return {
debouncedUpdateDomainFilter: debounce(this.updateDomainFilter, 500),
};
}
const updateDomainFilter = (event: InputEvent) => {
filterDomain.value = event.target?.value;
};
async updateDomainFilter(domain: string) {
this.filterDomain = domain;
}
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
set page(page: number) {
this.pushRouter({
page: page.toString(),
});
}
get status(): ReportStatusEnum {
const filter = (this.$route.query.status || "") as string;
if (filter in ReportStatusEnum) {
return filter as ReportStatusEnum;
}
return ReportStatusEnum.OPEN;
}
set status(status: ReportStatusEnum) {
this.pushRouter({ status });
}
get filterDomain(): string {
return (this.$route.query.domain as string) || "";
}
set filterDomain(domain: string) {
this.pushRouter({ domain });
}
protected async pushRouter(args: Record<string, string>): Promise<void> {
try {
await this.$router.push({
name: RouteName.REPORTS,
params: this.$route.params,
query: { ...this.$route.query, ...args },
});
} catch (e) {
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
throw Error(e.toString());
}
}
}
}
const debouncedUpdateDomainFilter = debounce(updateDomainFilter, 500);
</script>
<style lang="scss" scoped>