Added entity reporting.
This commit is contained in:
@@ -34,6 +34,13 @@ import Admin from '#/components/admin/admin.vue';
|
||||
class="link"
|
||||
>Entity Health</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/admin/reports"
|
||||
class="link"
|
||||
>Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Admin>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import Admin from '#/components/admin/admin.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import { patch, post } from '#/src/api.js';
|
||||
import reportReasons from '#/utils/report-reasons.js';
|
||||
|
||||
const { pageProps, user } = inject('pageContext');
|
||||
const { completed } = pageProps;
|
||||
|
||||
const reports = ref(pageProps.reports);
|
||||
|
||||
async function updateReport(report, update) {
|
||||
const updatedReport = await patch(`/reports/${report.id}`, update, {
|
||||
errorFeedback: 'Failed to update report',
|
||||
appendErrorMessage: true,
|
||||
}).catch(() => null); // feedback is shown by the api module
|
||||
|
||||
if (updatedReport) {
|
||||
reports.value = reports.value.map((listedReport) => (listedReport.id === report.id
|
||||
? {
|
||||
...listedReport,
|
||||
isRead: updatedReport.isRead,
|
||||
isCompleted: updatedReport.isCompleted,
|
||||
}
|
||||
: listedReport));
|
||||
}
|
||||
}
|
||||
|
||||
async function banReporter(report) {
|
||||
if (!confirm(`Ban ${report.user.username} from submitting reports?`)) { // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
|
||||
await post('/bans', {
|
||||
userId: report.user.id,
|
||||
banIp: true,
|
||||
scope: 'reports',
|
||||
}, {
|
||||
successFeedback: `Banned ${report.user.username} from submitting reports`,
|
||||
errorFeedback: `Failed to ban ${report.user.username}`,
|
||||
appendErrorMessage: true,
|
||||
}).catch(() => {}); // feedback is shown by the api module
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Admin class="page">
|
||||
<div class="header">
|
||||
<h2 class="heading">{{ completed ? 'Completed reports' : 'Open reports' }}</h2>
|
||||
|
||||
<a
|
||||
:href="completed ? '/admin/reports' : '/admin/reports?completed=true'"
|
||||
class="link toggle-completed"
|
||||
>{{ completed ? 'Show open reports' : 'Show completed reports' }}</a>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="reports.length === 0"
|
||||
class="empty"
|
||||
>No {{ completed ? 'completed' : 'open' }} reports.</p>
|
||||
|
||||
<table
|
||||
v-else
|
||||
class="table"
|
||||
>
|
||||
<thead class="table-header">
|
||||
<tr>
|
||||
<th class="table-heading">Site</th>
|
||||
<th class="table-heading">Reasons</th>
|
||||
<th class="table-heading">Comment</th>
|
||||
<th class="table-heading">Reporter</th>
|
||||
<th class="table-heading">Date</th>
|
||||
<th class="table-heading table-check">Read</th>
|
||||
<th class="table-heading table-check">Done</th>
|
||||
<th
|
||||
v-if="user.role === 'admin'"
|
||||
class="table-heading"
|
||||
>Ban</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="report in reports"
|
||||
:key="`report-${report.id}`"
|
||||
class="report"
|
||||
:class="{ unread: !report.isRead, completed: report.isCompleted !== completed }"
|
||||
>
|
||||
<td
|
||||
:title="report.entity?.id"
|
||||
class="table-cell table-name ellipsis"
|
||||
>
|
||||
<a
|
||||
v-if="report.entity"
|
||||
:href="`/${report.entity.type}/${report.entity.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ report.entity.name }}</a>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-reasons">
|
||||
<span
|
||||
v-for="reason in report.reasons"
|
||||
:key="`report-${report.id}-${reason}`"
|
||||
class="reason"
|
||||
:class="{ urgent: reason === 'inappropriate' }"
|
||||
>{{ reportReasons[reason] || reason }}</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-comment">{{ report.comment }}</td>
|
||||
|
||||
<td class="table-cell table-username ellipsis">
|
||||
<a
|
||||
v-if="report.user"
|
||||
:href="`/user/${report.user.username}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ report.user.username }}</a>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="deleted"
|
||||
>deleted user</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-date">{{ format(report.createdAt, 'yyyy-MM-dd HH:mm') }}</td>
|
||||
|
||||
<td class="table-cell table-check">
|
||||
<Checkbox
|
||||
:checked="report.isRead"
|
||||
@change="(checked) => updateReport(report, { isRead: checked })"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-check">
|
||||
<Checkbox
|
||||
:checked="report.isCompleted"
|
||||
@change="(checked) => updateReport(report, { isCompleted: checked })"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="user.role === 'admin'"
|
||||
class="table-cell table-actions"
|
||||
>
|
||||
<Icon
|
||||
v-if="report.user"
|
||||
v-tooltip="`Ban ${report.user.username} from submitting reports`"
|
||||
icon="blocked"
|
||||
class="ban"
|
||||
@click="banReporter(report)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Admin>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--glass);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.table-heading {
|
||||
text-align: left;
|
||||
padding: 0 .5rem .25rem .5rem;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: .25rem .5rem .25rem .5rem;
|
||||
border-collapse: collapse;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.table-name {
|
||||
max-width: 12rem;
|
||||
}
|
||||
|
||||
.table-comment {
|
||||
min-width: 15rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.table-date {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-check {
|
||||
width: 3rem;
|
||||
text-align: center;
|
||||
|
||||
:deep(.check-container) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.report {
|
||||
&:nth-child(2) {
|
||||
background: var(--glass-weak-50);
|
||||
}
|
||||
|
||||
&.unread .table-name .link {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* marked completed (or reopened) on this page, it moves to the other list on reload */
|
||||
&.completed {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.reason {
|
||||
display: inline-block;
|
||||
padding: .25rem .5rem;
|
||||
margin: 0 .25rem .25rem 0;
|
||||
border-radius: .25rem;
|
||||
background: var(--background-dark-20);
|
||||
font-size: .9rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&.urgent {
|
||||
background: var(--warn);
|
||||
color: var(--text-light);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toggle-completed {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.deleted {
|
||||
color: var(--glass);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ban {
|
||||
fill: var(--glass);
|
||||
|
||||
&:hover {
|
||||
fill: var(--warn);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { render } from 'vike/abort';
|
||||
|
||||
import { canReviewReports, fetchReports } from '#/src/reports.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
if (!canReviewReports(pageContext.user)) {
|
||||
throw render(404);
|
||||
}
|
||||
|
||||
const completed = pageContext.urlParsed.search.completed === 'true';
|
||||
const reports = await fetchReports({ completed }, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: 'Reports',
|
||||
pageProps: {
|
||||
reports,
|
||||
completed,
|
||||
},
|
||||
routeParams: {
|
||||
section: 'reports',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user