Add report notes
This commit is contained in:
parent
4d7889d76a
commit
1ba4564b20
9 changed files with 240 additions and 11 deletions
|
@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Ability to fetch all statuses from a given instance
|
- Ability to fetch all statuses from a given instance
|
||||||
- Grouped reports: now you can view reports, which are grouped by status (pagination is not implemented yet, though)
|
- Grouped reports: now you can view reports, which are grouped by status (pagination is not implemented yet, though)
|
||||||
- Ability to confirm users' emails and resend confirmation emails
|
- Ability to confirm users' emails and resend confirmation emails
|
||||||
|
- Report notes
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
"driver.js": "0.8.1",
|
"driver.js": "0.8.1",
|
||||||
"dropzone": "5.2.0",
|
"dropzone": "5.2.0",
|
||||||
"echarts": "4.1.0",
|
"echarts": "4.1.0",
|
||||||
"element-ui": "^2.10.0",
|
"element-ui": "^2.13.0",
|
||||||
"file-saver": "1.3.8",
|
"file-saver": "1.3.8",
|
||||||
"fuse.js": "3.4.2",
|
"fuse.js": "3.4.2",
|
||||||
"js-cookie": "2.2.0",
|
"js-cookie": "2.2.0",
|
||||||
|
|
|
@ -33,4 +33,23 @@ export async function fetchGroupedReports(authHost, token) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createNote(content, reportID, authHost, token) {
|
||||||
|
return await request({
|
||||||
|
baseURL: baseName(authHost),
|
||||||
|
url: `/api/pleroma/admin/reports/${reportID}/notes`,
|
||||||
|
method: `post`,
|
||||||
|
headers: authHeaders(token),
|
||||||
|
data: { content }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteNote(noteID, reportID, authHost, token) {
|
||||||
|
return await request({
|
||||||
|
baseURL: baseName(authHost),
|
||||||
|
url: `/api/pleroma/admin/reports/${reportID}/notes/${noteID}`,
|
||||||
|
method: `delete`,
|
||||||
|
headers: authHeaders(token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
||||||
|
|
|
@ -240,9 +240,10 @@ export default {
|
||||||
resendConfirmation: 'Resend confirmation email'
|
resendConfirmation: 'Resend confirmation email'
|
||||||
},
|
},
|
||||||
statuses: {
|
statuses: {
|
||||||
statuses: 'Statuses',
|
statuses: 'Statuses by instance',
|
||||||
instanceFilter: 'Instance filter',
|
instanceFilter: 'Instance filter',
|
||||||
loadMore: 'Load more'
|
loadMore: 'Load more',
|
||||||
|
noInstances: 'No other instances found'
|
||||||
},
|
},
|
||||||
userProfile: {
|
userProfile: {
|
||||||
tags: 'Tags',
|
tags: 'Tags',
|
||||||
|
@ -308,7 +309,10 @@ export default {
|
||||||
actors: 'Actors',
|
actors: 'Actors',
|
||||||
content: 'Content',
|
content: 'Content',
|
||||||
reportedStatus: 'Reported status',
|
reportedStatus: 'Reported status',
|
||||||
statusDeleted: 'This status has been deleted'
|
statusDeleted: 'This status has been deleted',
|
||||||
|
leaveNote: 'Leave a note',
|
||||||
|
postNote: 'Send',
|
||||||
|
deleteNote: 'Delete'
|
||||||
},
|
},
|
||||||
reportsFilter: {
|
reportsFilter: {
|
||||||
inputPlaceholder: 'Select filter',
|
inputPlaceholder: 'Select filter',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { changeState, fetchReports, fetchGroupedReports } from '@/api/reports'
|
import { Message } from 'element-ui'
|
||||||
|
import { changeState, fetchReports, fetchGroupedReports, createNote, deleteNote } from '@/api/reports'
|
||||||
|
|
||||||
const reports = {
|
const reports = {
|
||||||
state: {
|
state: {
|
||||||
|
@ -79,6 +80,50 @@ const reports = {
|
||||||
},
|
},
|
||||||
ToggleReportsGrouping({ commit }) {
|
ToggleReportsGrouping({ commit }) {
|
||||||
commit('SET_REPORTS_GROUPING')
|
commit('SET_REPORTS_GROUPING')
|
||||||
|
},
|
||||||
|
CreateReportNote({ commit, getters, state, rootState }, { content, reportID }) {
|
||||||
|
createNote(content, reportID, getters.authHost, getters.token)
|
||||||
|
|
||||||
|
const optimisticNote = {
|
||||||
|
user: {
|
||||||
|
avatar: rootState.user.avatar,
|
||||||
|
display_name: rootState.user.name,
|
||||||
|
url: `${rootState.user.authHost}/${rootState.user.name}`,
|
||||||
|
acct: rootState.user.name
|
||||||
|
},
|
||||||
|
content: content,
|
||||||
|
created_at: new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedReports = state.fetchedReports.map(report => {
|
||||||
|
if (report.id === reportID) {
|
||||||
|
report.notes = [...report.notes, optimisticNote]
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
})
|
||||||
|
|
||||||
|
commit('SET_REPORTS', updatedReports)
|
||||||
|
},
|
||||||
|
DeleteReportNote({ commit, getters, state }, { noteID, reportID }) {
|
||||||
|
deleteNote(noteID, reportID, getters.authHost, getters.token)
|
||||||
|
|
||||||
|
const updatedReports = state.fetchedReports.map(report => {
|
||||||
|
if (report.id === reportID) {
|
||||||
|
report.notes = report.notes.filter(note => note.id !== noteID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
})
|
||||||
|
|
||||||
|
commit('SET_REPORTS', updatedReports)
|
||||||
|
},
|
||||||
|
SuccessMessage(text) {
|
||||||
|
return Message({
|
||||||
|
message: text,
|
||||||
|
type: 'success',
|
||||||
|
duration: 5 * 1000
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
119
src/views/reports/components/NoteCard.vue
Normal file
119
src/views/reports/components/NoteCard.vue
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
<template>
|
||||||
|
<el-card class="note-card">
|
||||||
|
<div slot="header">
|
||||||
|
<div class="note-header">
|
||||||
|
<div class="note-actor-container">
|
||||||
|
<div class="note-actor">
|
||||||
|
<img :src="note.user.avatar" class="note-avatar-img">
|
||||||
|
<h3 class="note-actor-name">{{ note.user.display_name }}</h3>
|
||||||
|
</div>
|
||||||
|
<a :href="note.user.url" target="_blank">
|
||||||
|
@{{ note.user.acct }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-popconfirm
|
||||||
|
title="Are you sure to delete this?"
|
||||||
|
confirm-button-text="Yes"
|
||||||
|
cancel-button-text="No"
|
||||||
|
@onConfirm="handleNoteDeletion(note.id, report.id)">
|
||||||
|
<el-button slot="reference" size="mini">
|
||||||
|
{{ $t('reports.deleteNote') }}
|
||||||
|
</el-button>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="note-body">
|
||||||
|
<span class="note-content" v-html="note.content"/>
|
||||||
|
{{ parseTimestamp(note.created_at) }}
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NoteCard',
|
||||||
|
props: {
|
||||||
|
report: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
parseTimestamp(timestamp) {
|
||||||
|
return moment(timestamp).format('YYYY-MM-DD HH:mm')
|
||||||
|
},
|
||||||
|
handleNoteDeletion(noteID, reportID) {
|
||||||
|
this.$store.dispatch('DeleteReportNote', { noteID, reportID })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style rel='stylesheet/scss' lang='scss'>
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.el-icon-arrow-right {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.note-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.note-actor {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.note-actor-name {
|
||||||
|
margin: 0;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
.note-avatar-img {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.note-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.note-card {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.note-content {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.note-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
@media
|
||||||
|
only screen and (max-width: 760px),
|
||||||
|
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||||
|
.el-card__header {
|
||||||
|
padding: 10px 17px;
|
||||||
|
}
|
||||||
|
.note-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
.note-actor-container {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.note-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -63,6 +63,23 @@
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="report-notes">
|
||||||
|
<el-collapse>
|
||||||
|
<el-collapse-item :title="getNotesTitle(report.notes)">
|
||||||
|
<note-card v-for="(note, index) in report.notes" :key="index" :note="note" :report="report"/>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
<div class="report-note-form">
|
||||||
|
<el-input
|
||||||
|
v-model="notes[report.id]"
|
||||||
|
:placeholder="$t('reports.leaveNote')"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"/>
|
||||||
|
<div class="report-post-note">
|
||||||
|
<el-button @click="handleNewNote(report.id)">{{ $t('reports.postNote') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-timeline-item>
|
</el-timeline-item>
|
||||||
</el-timeline>
|
</el-timeline>
|
||||||
|
@ -81,18 +98,24 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import NoteCard from './NoteCard'
|
||||||
import Status from '@/components/Status'
|
import Status from '@/components/Status'
|
||||||
import ModerateUserDropdown from './ModerateUserDropdown'
|
import ModerateUserDropdown from './ModerateUserDropdown'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Report',
|
name: 'Report',
|
||||||
components: { Status, ModerateUserDropdown },
|
components: { Status, ModerateUserDropdown, NoteCard },
|
||||||
props: {
|
props: {
|
||||||
reports: {
|
reports: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
notes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
loading() {
|
loading() {
|
||||||
return this.$store.state.reports.loading
|
return this.$store.state.reports.loading
|
||||||
|
@ -127,11 +150,18 @@ export default {
|
||||||
getStatusesTitle(statuses) {
|
getStatusesTitle(statuses) {
|
||||||
return `Reported statuses: ${statuses.length} item(s)`
|
return `Reported statuses: ${statuses.length} item(s)`
|
||||||
},
|
},
|
||||||
|
getNotesTitle(notes) {
|
||||||
|
return `Notes: ${notes.length} item(s)`
|
||||||
|
},
|
||||||
handlePageChange(page) {
|
handlePageChange(page) {
|
||||||
this.$store.dispatch('FetchReports', page)
|
this.$store.dispatch('FetchReports', page)
|
||||||
},
|
},
|
||||||
parseTimestamp(timestamp) {
|
parseTimestamp(timestamp) {
|
||||||
return moment(timestamp).format('L HH:mm')
|
return moment(timestamp).format('L HH:mm')
|
||||||
|
},
|
||||||
|
handleNewNote(reportID) {
|
||||||
|
this.$store.dispatch('CreateReportNote', { content: this.notes[reportID], reportID })
|
||||||
|
this.notes[reportID] = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,6 +247,13 @@ export default {
|
||||||
.report-title {
|
.report-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.report-note-form {
|
||||||
|
margin: 15px 0 0 0;
|
||||||
|
}
|
||||||
|
.report-post-note {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
.reports-pagination {
|
.reports-pagination {
|
||||||
margin: 25px 0;
|
margin: 25px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
{{ $t('statuses.statuses') }}
|
{{ $t('statuses.statuses') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-select v-model="selectedInstance" :placeholder="$t('statuses.instanceFilter')" @change="handleFilterChange">
|
<el-select
|
||||||
|
v-model="selectedInstance"
|
||||||
|
:placeholder="$t('statuses.instanceFilter')"
|
||||||
|
:no-data-text="$t('statuses.noInstances')"
|
||||||
|
@change="handleFilterChange">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="(instance,index) in instances"
|
v-for="(instance,index) in instances"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|
|
@ -3456,10 +3456,10 @@ elegant-spinner@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
|
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
|
||||||
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
|
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
|
||||||
|
|
||||||
element-ui@^2.10.0:
|
element-ui@^2.13.0:
|
||||||
version "2.10.0"
|
version "2.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/element-ui/-/element-ui-2.10.0.tgz#e6129f6b6d6ffe0dbad125a4a8d17d447a5f639c"
|
resolved "https://registry.yarnpkg.com/element-ui/-/element-ui-2.13.0.tgz#f6bb04e5b0a76ea5f62466044b774407ba4ebd2d"
|
||||||
integrity sha512-uthsnJ1CIdQvLWphr67uwFSfSYoRBjxFcEhXhy+2/EwKNsqO7MRN+mYqroNLz5WJuLqVy1aOpJ8Lv4B32qKthQ==
|
integrity sha512-KYsHWsBXYbLELS8cdfvgJTOMSUby3UEjvsPV1V1VmgJ/DdkOAS4z3MiOrPxrT9w2Cc5lZ4eVSQiGhYFR5NVChw==
|
||||||
dependencies:
|
dependencies:
|
||||||
async-validator "~1.8.1"
|
async-validator "~1.8.1"
|
||||||
babel-helper-vue-jsx-merge-props "^2.0.0"
|
babel-helper-vue-jsx-merge-props "^2.0.0"
|
||||||
|
|
Loading…
Reference in a new issue