Add report notes

This commit is contained in:
Maxim Filippov 2019-12-08 11:26:42 +03:00
parent 4d7889d76a
commit 1ba4564b20
9 changed files with 240 additions and 11 deletions

View file

@ -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

View file

@ -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",

View file

@ -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()}` } : {}

View file

@ -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',

View file

@ -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
})
} }
} }
} }

View 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>

View file

@ -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;

View file

@ -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"

View file

@ -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"