Merge branch 'feature/combine-reports-with-one-topic' into 'master'

Combine reports with one topic and add pagination

See merge request pleroma/admin-fe!52
This commit is contained in:
Maxim Filippov 2019-11-28 11:04:05 +00:00
commit 253fa33235
26 changed files with 1414 additions and 782 deletions

View file

@ -11,18 +11,41 @@ const reports = [
{ created_at: '2019-05-18T13:01:33.000Z', account: { acct: 'nick', display_name: 'Nick Keys', tags: [] }, actor: { acct: 'admin' }, state: 'closed', id: '4', content: '', statuses: [] }
]
export async function fetchReports(limit, max_id, authHost, token) {
const paginatedReports = max_id.length > 0 ? reports.slice(5) : reports.slice(0, 5)
return Promise.resolve({ data: { reports: paginatedReports }})
const groupedReports = [
{ account: { avatar: 'http://localhost:4000/images/avi.png', confirmation_pending: false, deactivated: false, display_name: 'leo', id: '9oG0YghgBi94EATI9I', local: true, nickname: 'leo', roles: { admin: false, moderator: false }, tags: [] },
actors: [{ acct: 'admin', avatar: 'http://localhost:4000/images/avi.png', deactivated: false, display_name: 'admin', id: '9oFz4pTauG0cnJ581w', local: true, nickname: 'admin', roles: { admin: false, moderator: false }, tags: [], url: 'http://localhost:4000/users/admin', username: 'admin' }],
date: '2019-11-23T12:56:11.969772Z',
reports: [
{ created_at: '2019-05-21T21:35:33.000Z', account: { acct: 'benj', display_name: 'Benjamin Fame', tags: [] }, actor: { acct: 'admin' }, state: 'open', id: '2', content: 'This is a report', statuses: [] },
{ created_at: '2019-05-20T22:45:33.000Z', account: { acct: 'alice', display_name: 'Alice Pool', tags: [] }, actor: { acct: 'admin2' }, state: 'resolved', id: '7', content: 'Please block this user', statuses: [
{ account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'public', sensitive: false, id: '11', content: 'Hey!', url: '', created_at: '2019-05-10T21:35:33.000Z' },
{ account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'unlisted', sensitive: true, id: '10', content: 'Bye!', url: '', created_at: '2019-05-10T21:00:33.000Z' }
] }
],
status: {
account: { acct: 'leo' },
content: 'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis',
created_at: '2019-11-23T12:55:20.000Z',
id: '9pFoQO69piu7cUDnJg',
url: 'http://localhost:4000/notice/9pFoQO69piu7cUDnJg',
visibility: 'unlisted',
sensitive: true
},
status_deleted: false
}
]
export async function fetchReports(filter, page, pageSize, authHost, token) {
return filter.length > 0
? Promise.resolve({ data: { reports: reports.filter(report => report.state === filter) }})
: Promise.resolve({ data: { reports }})
}
export async function filterReports(filter, limit, max_id, authHost, token) {
const filteredReports = reports.filter(report => report.state === filter)
const paginatedReports = max_id.length > 0 ? filteredReports.slice(5) : filteredReports.slice(0, 5)
return Promise.resolve({ data: { reports: paginatedReports }})
export async function fetchGroupedReports(authHost, token) {
return Promise.resolve({ data: { reports: groupedReports }})
}
export async function changeState(state, id, authHost, token) {
export async function changeState(reportsData, authHost, token) {
return Promise.resolve({ data: '' })
}

View file

@ -0,0 +1,7 @@
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
return Promise.resolve()
}
export async function deleteStatus(id, authHost, token) {
return Promise.resolve()
}

View file

@ -2,48 +2,32 @@ import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function changeState(state, id, authHost, token) {
export async function changeState(reports, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/reports`,
method: 'patch',
headers: authHeaders(token),
data: { reports: [{ id, state }] }
data: { reports }
})
}
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
export async function fetchReports(filter, page, pageSize, authHost, token) {
const url = filter.length > 0
? `/api/pleroma/admin/reports?state=${filter}&page=${page}&page_size=${pageSize}`
: `/api/pleroma/admin/reports?page=${page}&page_size=${pageSize}`
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/statuses/${id}`,
method: 'put',
headers: authHeaders(token),
data: { sensitive, visibility }
})
}
export async function deleteStatus(id, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/statuses/${id}`,
method: 'delete',
headers: authHeaders(token)
})
}
export async function fetchReports(limit, max_id, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/reports?limit=${limit}&max_id=${max_id}`,
url,
method: 'get',
headers: authHeaders(token)
})
}
export async function filterReports(filter, limit, max_id, authHost, token) {
export async function fetchGroupedReports(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/reports?state=${filter}&limit=${limit}&max_id=${max_id}`,
url: `/api/pleroma/admin/grouped_reports`,
method: 'get',
headers: authHeaders(token)
})

24
src/api/status.js Normal file
View file

@ -0,0 +1,24 @@
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/statuses/${id}`,
method: 'put',
headers: authHeaders(token),
data: { sensitive, visibility }
})
}
export async function deleteStatus(id, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/statuses/${id}`,
method: 'delete',
headers: authHeaders(token)
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}

View file

@ -253,6 +253,7 @@ export default {
},
reports: {
reports: 'Reports',
groupedReports: 'Grouped reports',
reply: 'Reply',
from: 'From',
showNotes: 'Show notes',
@ -264,19 +265,32 @@ export default {
deleteCompleted: 'Delete comleted',
deleteCanceled: 'Delete canceled',
noNotes: 'No notes to display',
changeState: 'Change report state',
changeState: "Change report's state",
changeAllReports: 'Change all reports',
changeScope: 'Change scope',
moderateUser: 'Moderate user',
resolve: 'Resolve',
reopen: 'Reopen',
close: 'Close',
resolveAll: 'Resolve all',
reopenAll: 'Reopen all',
closeAll: 'Close all',
addSensitive: 'Add Sensitive flag',
removeSensitive: 'Remove Sensitive flag',
public: 'Make status public',
private: 'Make status private',
unlisted: 'Make status unlisted',
sensitive: 'Sensitive',
deleteStatus: 'Delete status'
deleteStatus: 'Delete status',
reportOn: 'Report on',
reportsOn: 'Reports on',
id: 'ID',
account: 'Account',
actor: 'Actor',
actors: 'Actors',
content: 'Content',
reportedStatus: 'Reported status',
statusDeleted: 'This status has been deleted'
},
reportsFilter: {
inputPlaceholder: 'Select filter',

View file

@ -8,6 +8,7 @@ import permission from './modules/permission'
import relays from './modules/relays'
import reports from './modules/reports'
import settings from './modules/settings'
import status from './modules/status'
import tagsView from './modules/tagsView'
import user from './modules/user'
import userProfile from './modules/userProfile'
@ -27,6 +28,7 @@ const store = new Vuex.Store({
relays,
reports,
settings,
status,
tagsView,
user,
userProfile,

View file

@ -1,10 +1,13 @@
import { changeState, changeStatusScope, deleteStatus, fetchReports, filterReports } from '@/api/reports'
import { changeState, fetchReports, fetchGroupedReports } from '@/api/reports'
const reports = {
state: {
fetchedReports: [],
idOfLastReport: '',
page_limit: 5,
fetchedGroupedReports: [],
totalReportsCount: 0,
currentPage: 1,
pageSize: 50,
groupReports: false,
stateFilter: '',
loading: true
},
@ -15,67 +18,67 @@ const reports = {
SET_LOADING: (state, status) => {
state.loading = status
},
SET_PAGE: (state, page) => {
state.currentPage = page
},
SET_REPORTS: (state, reports) => {
state.fetchedReports = reports
},
SET_GROUPED_REPORTS: (state, reports) => {
state.fetchedGroupedReports = reports
},
SET_REPORTS_COUNT: (state, total) => {
state.totalReportsCount = total
},
SET_REPORTS_FILTER: (state, filter) => {
state.stateFilter = filter
},
SET_REPORTS_GROUPING: (state) => {
state.groupReports = !state.groupReports
}
},
actions: {
async ChangeReportState({ commit, getters, state }, { reportState, reportId }) {
changeState(reportState, reportId, getters.authHost, getters.token)
async ChangeReportState({ commit, getters, state }, reportsData) {
changeState(reportsData, getters.authHost, getters.token)
const updatedReports = state.fetchedReports.map(report => {
return report.id === reportId ? { ...report, state: reportState } : report
const updatedReportsIds = reportsData.map(({ id }) => id)
return updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report
})
const updatedGroupedReports = state.fetchedGroupedReports.map(group => {
const updatedReportsIds = reportsData.map(({ id }) => id)
const updatedReports = group.reports.map(report => updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report)
return { ...group, reports: updatedReports }
})
commit('SET_REPORTS', updatedReports)
},
async ChangeStatusScope({ commit, getters, state }, { statusId, isSensitive, visibility, reportId }) {
const { data } = await changeStatusScope(statusId, isSensitive, visibility, getters.authHost, getters.token)
const updatedReports = state.fetchedReports.map(report => {
if (report.id === reportId) {
const statuses = report.statuses.map(status => status.id === statusId ? data : status)
return { ...report, statuses }
} else {
return report
}
})
commit('SET_REPORTS', updatedReports)
commit('SET_GROUPED_REPORTS', updatedGroupedReports)
},
ClearFetchedReports({ commit }) {
commit('SET_REPORTS', [])
commit('SET_LAST_REPORT_ID', '')
},
async DeleteStatus({ commit, getters, state }, { statusId, reportId }) {
deleteStatus(statusId, getters.authHost, getters.token)
const updatedReports = state.fetchedReports.map(report => {
if (report.id === reportId) {
const statuses = report.statuses.filter(status => status.id !== statusId)
return { ...report, statuses }
} else {
return report
}
})
commit('SET_REPORTS', updatedReports)
},
async FetchReports({ commit, getters, state }) {
async FetchReports({ commit, getters, state }, page) {
commit('SET_LOADING', true)
const { data } = await fetchReports(state.stateFilter, page, state.pageSize, getters.authHost, getters.token)
const response = state.stateFilter.length === 0
? await fetchReports(state.page_limit, state.idOfLastReport, getters.authHost, getters.token)
: await filterReports(state.stateFilter, state.page_limit, state.idOfLastReport, getters.authHost, getters.token)
commit('SET_REPORTS', data.reports)
commit('SET_REPORTS_COUNT', data.total)
commit('SET_PAGE', page)
commit('SET_LOADING', false)
},
async FetchGroupedReports({ commit, getters }) {
commit('SET_LOADING', true)
const { data } = await fetchGroupedReports(getters.authHost, getters.token)
const reports = state.fetchedReports.concat(response.data.reports)
const id = reports.length > 0 ? reports[reports.length - 1].id : state.idOfLastReport
commit('SET_REPORTS', reports)
commit('SET_LAST_REPORT_ID', id)
commit('SET_GROUPED_REPORTS', data.reports)
commit('SET_LOADING', false)
},
SetFilter({ commit }, filter) {
commit('SET_REPORTS_FILTER', filter)
},
ToggleReportsGrouping({ commit }) {
commit('SET_REPORTS_GROUPING')
}
}
}

View file

@ -0,0 +1,28 @@
import { changeStatusScope, deleteStatus } from '@/api/status'
const status = {
actions: {
async ChangeStatusScope({ dispatch, getters }, { statusId, isSensitive, visibility, reportCurrentPage, userId, godmode }) {
await changeStatusScope(statusId, isSensitive, visibility, getters.authHost, getters.token)
if (reportCurrentPage !== 0) { // called from Reports
dispatch('FetchReports', reportCurrentPage)
} else if (userId.length > 0) { // called from User profile
dispatch('FetchUserStatuses', { userId, godmode })
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async DeleteStatus({ dispatch, getters }, { statusId, reportCurrentPage, userId, godmode }) {
await deleteStatus(statusId, getters.authHost, getters.token)
if (reportCurrentPage !== 0) {
dispatch('FetchReports', reportCurrentPage)
} else if (userId.length > 0) {
dispatch('FetchUserStatuses', { userId, godmode })
} else {
dispatch('FetchGroupedReports')
}
}
}
}
export default status

View file

@ -2,33 +2,42 @@ import { fetchUser, fetchUserStatuses } from '@/api/users'
const userProfile = {
state: {
statuses: [],
statusesLoading: true,
user: {},
loading: true,
statuses: []
userProfileLoading: true
},
mutations: {
SET_STATUSES: (state, statuses) => {
state.statuses = statuses
},
SET_STATUSES_LOADING: (state, status) => {
state.statusesLoading = status
},
SET_USER: (state, user) => {
state.user = user
},
SET_LOADING: (state, status) => {
state.loading = status
},
SET_STATUSES: (state, statuses) => {
state.statuses = statuses
SET_USER_PROFILE_LOADING: (state, status) => {
state.userProfileLoading = status
}
},
actions: {
async FetchData({ commit, getters }, { id, godmode }) {
commit('SET_LOADING', true)
const [userResponse, statusesResponse] = await Promise.all([
fetchUser(id, getters.authHost, getters.token),
fetchUserStatuses(id, getters.authHost, godmode, getters.token)
])
async FetchUserProfile({ commit, dispatch, getters }, { userId, godmode }) {
commit('SET_USER_PROFILE_LOADING', true)
const userResponse = await fetchUser(userId, getters.authHost, getters.token)
commit('SET_USER', userResponse.data)
commit('SET_STATUSES', statusesResponse.data)
commit('SET_LOADING', false)
commit('SET_USER_PROFILE_LOADING', false)
dispatch('FetchUserStatuses', { userId, godmode })
},
async FetchUserStatuses({ commit, getters }, { userId, godmode }) {
commit('SET_STATUSES_LOADING', true)
const statuses = await fetchUserStatuses(userId, getters.authHost, godmode, getters.token)
commit('SET_STATUSES', statuses.data)
commit('SET_STATUSES_LOADING', false)
}
}
}

View file

@ -0,0 +1,143 @@
<template>
<el-timeline class="timeline">
<el-timeline-item
v-for="groupedReport in groupedReports"
:key="groupedReport.id"
:timestamp="parseTimestamp(groupedReport.date)"
placement="top"
class="timeline-item-container">
<el-card class="grouped-report">
<div class="header-container">
<div>
<h3 class="report-title">{{ $t('reports.reportsOn') }} {{ groupedReport.account.display_name }}</h3>
</div>
<div>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeAllReports') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeAllReports('resolved', groupedReport.reports)">{{ $t('reports.resolveAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('open', groupedReport.reports)">{{ $t('reports.reopenAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('closed', groupedReport.reports)">{{ $t('reports.closeAll') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<moderate-user-dropdown :account="groupedReport.account"/>
</div>
</div>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.account') }}:</span>
<img
:src="groupedReport.account.avatar"
alt="avatar"
class="avatar-img">
<a :href="groupedReport.account.url" target="_blank">
<span>{{ groupedReport.account.nickname }}</span>
</a>
</div>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.actors') }}:</span>
<span v-for="(actor, index) in groupedReport.actors" :key="actor.id">
<a :href="actor.url" target="_blank">
{{ actor.acct }}<span v-if="index < groupedReport.actors.length - 1">, </span>
</a>
</span>
</div>
<div v-if="groupedReport.status">
<div class="line"/>
<span class="report-row-key">{{ $t('reports.reportedStatus') }}:</span>
<status :status="groupedReport.status" class="reported-status"/>
</div>
<div v-if="groupedReport.reports">
<el-collapse>
<el-collapse-item :title="$t('reports.reports')">
<report-card :reports="groupedReport.reports"/>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</template>
<script>
import moment from 'moment'
import ModerateUserDropdown from './ModerateUserDropdown'
import ReportCard from './ReportCard'
import Status from '../../status/Status'
export default {
name: 'Report',
components: { ModerateUserDropdown, ReportCard, Status },
props: {
groupedReports: {
type: Array,
required: true
}
},
methods: {
changeAllReports(reportState, groupOfReports) {
const reportsData = groupOfReports.map(report => {
return { id: report.id, state: reportState }
})
this.$store.dispatch('ChangeReportState', reportsData)
},
parseTimestamp(timestamp) {
return moment(timestamp).format('L HH:mm')
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
a {
text-decoration: underline;
}
.avatar-img {
vertical-align: bottom;
width: 15px;
height: 15px;
margin-left: 5px;
}
.el-card__body {
padding: 17px;
}
.el-card__header {
background-color: #FAFAFA;
padding: 10px 20px;
}
.el-icon-arrow-right {
margin-right: 6px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: baseline;
height: 40px;
}
.line {
width: 100%;
height: 0;
border: 0.5px solid #EBEEF5;
margin: 15px 0 15px;
}
.report-title {
margin: 0;
}
.report-row-key {
font-size: 14px;
font-weight: 500;
}
.reported-status {
margin-top: 15px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.header-container {
display: flex;
flex-direction: column;
height: 80px;
}
}
</style>

View file

@ -0,0 +1,86 @@
<template>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-files">{{ $t('reports.moderateUser') }}
<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="showDeactivatedButton(account)"
@click.native="handleDeactivation(account)">
{{ account.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showDeactivatedButton(account.id)"
@click.native="handleDeletion(account.id)">
{{ $t('users.deleteAccount') }}
</el-dropdown-item>
<el-dropdown-item
:divided="true"
:class="{ 'active-tag': account.tags.includes('force_nsfw') }"
@click.native="toggleTag(account, 'force_nsfw')">
{{ $t('users.forceNsfw') }}
<i v-if="account.tags.includes('force_nsfw')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': account.tags.includes('strip_media') }"
@click.native="toggleTag(account, 'strip_media')">
{{ $t('users.stripMedia') }}
<i v-if="account.tags.includes('strip_media')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': account.tags.includes('force_unlisted') }"
@click.native="toggleTag(account, 'force_unlisted')">
{{ $t('users.forceUnlisted') }}
<i v-if="account.tags.includes('force_unlisted')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': account.tags.includes('sandbox') }"
@click.native="toggleTag(account, 'sandbox')">
{{ $t('users.sandbox') }}
<i v-if="account.tags.includes('sandbox')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="account.local"
:class="{ 'active-tag': account.tags.includes('disable_remote_subscription') }"
@click.native="toggleTag(account, 'disable_remote_subscription')">
{{ $t('users.disableRemoteSubscription') }}
<i v-if="account.tags.includes('disable_remote_subscription')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="account.local"
:class="{ 'active-tag': account.tags.includes('disable_any_subscription') }"
@click.native="toggleTag(account, 'disable_any_subscription')">
{{ $t('users.disableAnySubscription') }}
<i v-if="account.tags.includes('disable_any_subscription')" class="el-icon-check"/>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
name: 'ModerateUserDropdown',
props: {
account: {
type: Object,
required: true
}
},
methods: {
handleDeactivation({ nickname }) {
this.$store.dispatch('ToggleUserActivation', nickname)
},
handleDeletion(user) {
this.$store.dispatch('DeleteUser', user)
},
showDeactivatedButton(id) {
return this.$store.state.user.id !== id
},
toggleTag(user, tag) {
user.tags.includes(tag)
? this.$store.dispatch('RemoveTag', { users: [user], tag })
: this.$store.dispatch('AddTag', { users: [user], tag })
}
}
}
</script>

View file

@ -0,0 +1,250 @@
<template>
<div>
<el-timeline class="timeline">
<el-timeline-item
v-for="report in reports"
:timestamp="parseTimestamp(report.created_at)"
:key="report.id"
placement="top"
class="timeline-item-container">
<el-card>
<div class="header-container">
<div>
<h3 class="report-title">{{ $t('reports.reportOn') }} {{ report.account.display_name }}</h3>
<h5 class="id">{{ $t('reports.id') }}: {{ report.id }}</h5>
</div>
<div>
<el-tag :type="getStateType(report.state)" size="large">{{ capitalizeFirstLetter(report.state) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeState') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-if="report.state !== 'resolved'" @click.native="changeReportState('resolved', report.id)">{{ $t('reports.resolve') }}</el-dropdown-item>
<el-dropdown-item v-if="report.state !== 'open'" @click.native="changeReportState('open', report.id)">{{ $t('reports.reopen') }}</el-dropdown-item>
<el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<moderate-user-dropdown :account="report.account"/>
</div>
</div>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.account') }}:</span>
<img
:src="report.account.avatar"
alt="avatar"
class="avatar-img">
<a :href="report.account.url" target="_blank" class="account">
<span>{{ report.account.acct }}</span>
</a>
</div>
<div v-if="report.content.length > 0">
<div class="line"/>
<span class="report-row-key">{{ $t('reports.content') }}:
<span>{{ report.content }}</span>
</span>
</div>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.actor') }}:</span>
<img
:src="report.actor.avatar"
alt="avatar"
class="avatar-img">
<a :href="report.actor.url" target="_blank" class="account">
<span>{{ report.actor.acct }}</span>
</a>
</div>
<div v-if="report.statuses.length > 0" class="statuses">
<el-collapse>
<el-collapse-item :title="getStatusesTitle(report.statuses)">
<div v-for="status in report.statuses" :key="status.id">
<status :status="status" :page="currentPage"/>
</div>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
<div v-if="!loading" class="reports-pagination">
<el-pagination
:total="totalReportsCount"
:current-page="currentPage"
:page-size="pageSize"
background
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script>
import moment from 'moment'
import Status from '../../status/Status'
import ModerateUserDropdown from './ModerateUserDropdown'
export default {
name: 'Report',
components: { Status, ModerateUserDropdown },
props: {
reports: {
type: Array,
required: true
}
},
computed: {
loading() {
return this.$store.state.reports.loading
},
pageSize() {
return this.$store.state.reports.pageSize
},
totalReportsCount() {
return this.$store.state.reports.totalReportsCount
},
currentPage() {
return this.$store.state.reports.currentPage
}
},
methods: {
changeReportState(state, id) {
this.$store.dispatch('ChangeReportState', [{ state, id }])
},
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
getStateType(state) {
switch (state) {
case 'closed':
return 'info'
case 'resolved':
return 'success'
default:
return 'primary'
}
},
getStatusesTitle(statuses) {
return `Reported statuses: ${statuses.length} item(s)`
},
handlePageChange(page) {
this.$store.dispatch('FetchReports', page)
},
parseTimestamp(timestamp) {
return moment(timestamp).format('L HH:mm')
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.account {
text-decoration: underline;
}
.avatar-img {
vertical-align: bottom;
width: 15px;
height: 15px;
margin-left: 5px;
}
.el-card__body {
padding: 17px;
}
.el-card__header {
background-color: #FAFAFA;
padding: 10px 20px;
}
.el-collapse {
border-bottom: none;
}
.el-collapse-item__header {
height: 46px;
font-size: 14px;
}
.el-collapse-item__content {
padding-bottom: 7px;
}
.el-icon-arrow-right {
margin-right: 6px;
}
.el-icon-close {
padding: 10px 5px 10px 10px;
cursor: pointer;
}
h4 {
margin: 0;
height: 17px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: baseline;
height: 40px;
}
.id {
color: gray;
margin-top: 6px;
}
.line {
width: 100%;
height: 0;
border: 0.5px solid #EBEEF5;
margin: 15px 0 15px;
}
.new-note {
p {
font-size: 14px;
font-weight: 500;
height: 17px;
margin: 13px 0 7px;
}
}
.note {
box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
margin-bottom: 10px;
}
.no-notes {
font-style: italic;
color: gray;
}
.report-row-key {
font-size: 14px;
font-weight: 500;
}
.report-row-key {
font-size: 14px;
}
.report-title {
margin: 0;
}
.reports-pagination {
margin: 25px 0;
text-align: center;
}
.statuses {
margin-top: 15px;
}
.submit-button {
display: block;
margin: 7px 0 17px auto;
}
.timestamp {
margin: 0;
font-style: italic;
color: gray;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.timeline-item-container {
.header-container {
display: flex;
flex-direction: column;
height: 80px;
}
.id {
margin: 6px 0 0 0;
}
}
}
</style>

View file

@ -0,0 +1,130 @@
<template>
<div>
<el-card v-for="report in reports" :key="report.id" class="report-card">
<div slot="header">
<div class="report-header">
<div class="report-actor-container">
<div class="report-actor">
<img :src="report.actor.avatar" class="report-avatar-img">
<h3 class="report-actor-name">{{ report.actor.display_name }}</h3>
</div>
<a :href="report.actor.url" target="_blank">
@{{ report.actor.acct }}
</a>
</div>
<div>
<el-tag :type="getStateType(report.state)" size="large">{{ capitalizeFirstLetter(report.state) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeState') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-if="report.state !== 'resolved'" @click.native="changeReportState('resolved', report.id)">{{ $t('reports.resolve') }}</el-dropdown-item>
<el-dropdown-item v-if="report.state !== 'open'" @click.native="changeReportState('open', report.id)">{{ $t('reports.reopen') }}</el-dropdown-item>
<el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<div class="report-body">
<span class="report-content" v-html="report.content"/>
{{ parseTimestamp(report.created_at) }}
</div>
</el-card>
</div>
</template>
<script>
import moment from 'moment'
export default {
name: 'Statuses',
props: {
reports: {
type: Array,
required: true
}
},
methods: {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
changeReportState(state, id) {
this.$store.dispatch('ChangeReportState', [{ state, id }])
},
getStateType(state) {
switch (state) {
case 'closed':
return 'info'
case 'resolved':
return 'success'
default:
return 'primary'
}
},
parseTimestamp(timestamp) {
return moment(timestamp).format('YYYY-MM-DD HH:mm')
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
a {
text-decoration: underline;
}
.el-icon-arrow-right {
margin-right: 6px;
}
.report-header {
display: flex;
justify-content: space-between;
align-items: baseline;
height: 40px;
}
.report-actor {
display: flex;
align-items: center;
}
.report-actor-name {
margin: 0;
height: 22px;
}
.report-avatar-img {
width: 15px;
height: 15px;
margin-right: 5px;
}
.report-body {
display: flex;
flex-direction: column;
}
.report-card {
margin-bottom: 15px;
}
.report-content {
font-size: 15px;
}
.report-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;
}
.report-header {
display: flex;
flex-direction: column;
height: 80px;
}
.report-actor-container {
margin-bottom: 5px;
}
.report-header {
display: flex;
flex-direction: column;
}
}
</style>

View file

@ -44,7 +44,7 @@ export default {
toggleFilters() {
this.$store.dispatch('SetFilter', this.$data.filter)
this.$store.dispatch('ClearFetchedReports')
this.$store.dispatch('FetchReports')
this.$store.dispatch('FetchReports', 1)
}
}
}

View file

@ -1,176 +0,0 @@
<template>
<el-collapse-item :title="getStatusesTitle(report.statuses)">
<el-card v-for="status in report.statuses" :key="status.id" class="status-card">
<div slot="header">
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<img :src="status.account.avatar" class="status-avatar-img">
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
</div>
<a :href="status.account.url" target="_blank" class="account">
@{{ status.account.acct }}
</a>
</div>
<div class="status-actions">
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag>
<el-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button">
{{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="!status.sensitive"
@click.native="changeStatus(status.id, true, status.visibility, report.id)">
{{ $t('reports.addSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.sensitive"
@click.native="changeStatus(status.id, false, status.visibility, report.id)">
{{ $t('reports.removeSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'public'"
@click.native="changeStatus(status.id, status.sensitive, 'public', report.id)">
{{ $t('reports.public') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'private'"
@click.native="changeStatus(status.id, status.sensitive, 'private', report.id)">
{{ $t('reports.private') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'unlisted'"
@click.native="changeStatus(status.id, status.sensitive, 'unlisted', report.id)">
{{ $t('reports.unlisted') }}
</el-dropdown-item>
<el-dropdown-item
@click.native="deleteStatus(status.id, report.id)">
{{ $t('reports.deleteStatus') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<div class="status-body">
<span class="status-content" v-html="status.content"/>
<a :href="status.url" target="_blank" class="account">
{{ parseTimestamp(status.created_at) }}
</a>
</div>
</el-card>
</el-collapse-item>
</template>
<script>
import moment from 'moment'
export default {
name: 'Statuses',
props: {
report: {
type: Object,
required: true
}
},
methods: {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
changeStatus(statusId, isSensitive, visibility, reportId) {
this.$store.dispatch('ChangeStatusScope', { statusId, isSensitive, visibility, reportId })
},
deleteStatus(statusId, reportId) {
this.$confirm('Are you sure you want to delete this status?', 'Warning', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeleteStatus', { statusId, reportId })
this.$message({
type: 'success',
message: 'Delete completed'
})
}).catch(() => {
this.$message({
type: 'info',
message: 'Delete canceled'
})
})
},
getStatusesTitle(statuses) {
return `Reported statuses: ${statuses.length} item(s)`
},
parseTimestamp(timestamp) {
return moment(timestamp).format('YYYY-MM-DD HH:mm')
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.account {
text-decoration: underline;
}
.status-account {
display: flex;
align-items: center;
}
.status-avatar-img {
width: 15px;
height: 15px;
margin-right: 5px;
}
.status-account-name {
margin: 0;
height: 22px;
}
.status-body {
display: flex;
flex-direction: column;
}
.status-content {
font-size: 15px;
}
.status-card {
margin-bottom: 15px;
}
.status-header {
display: flex;
justify-content: space-between;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.el-message {
min-width: 80%;
}
.el-message-box {
width: 80%;
}
.status-card {
.el-card__header {
padding: 10px 17px
}
.el-tag {
margin: 3px 4px 3px 0;
}
.status-account-container {
margin-bottom: 5px;
}
.status-actions-button {
margin: 3px 0 3px;
}
.status-actions {
display: flex;
flex-wrap: wrap;
}
.status-header {
display: flex;
flex-direction: column;
}
}
}
</style>

View file

@ -1,271 +0,0 @@
<template>
<el-timeline-item :timestamp="parseTimestamp(report.created_at)" placement="top" class="timeline-item-container">
<el-card>
<div class="header-container">
<div>
<h3 class="report-title">Report on {{ report.account.display_name }}</h3>
<h5 class="id">ID: {{ report.id }}</h5>
</div>
<div>
<el-tag :type="getStateType(report.state)" size="large">{{ capitalizeFirstLetter(report.state) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeState') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-if="report.state !== 'resolved'" @click.native="changeReportState('resolved', report.id)">{{ $t('reports.resolve') }}</el-dropdown-item>
<el-dropdown-item v-if="report.state !== 'open'" @click.native="changeReportState('open', report.id)">{{ $t('reports.reopen') }}</el-dropdown-item>
<el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-files">{{ $t('reports.moderateUser') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="showDeactivatedButton(report.account)"
@click.native="toggleActivation(report.account)">
{{ report.account.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showDeactivatedButton(report.account.id)"
@click.native="handleDeletion(report.account.id)">
{{ $t('users.deleteAccount') }}
</el-dropdown-item>
<el-dropdown-item
:divided="true"
:class="{ 'active-tag': report.account.tags.includes('force_nsfw') }"
@click.native="toggleTag(report.account, 'force_nsfw')">
{{ $t('users.forceNsfw') }}
<i v-if="report.account.tags.includes('force_nsfw')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': report.account.tags.includes('strip_media') }"
@click.native="toggleTag(report.account, 'strip_media')">
{{ $t('users.stripMedia') }}
<i v-if="report.account.tags.includes('strip_media')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': report.account.tags.includes('force_unlisted') }"
@click.native="toggleTag(report.account, 'force_unlisted')">
{{ $t('users.forceUnlisted') }}
<i v-if="report.account.tags.includes('force_unlisted')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': report.account.tags.includes('sandbox') }"
@click.native="toggleTag(report.account, 'sandbox')">
{{ $t('users.sandbox') }}
<i v-if="report.account.tags.includes('sandbox')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="report.account.local"
:class="{ 'active-tag': report.account.tags.includes('disable_remote_subscription') }"
@click.native="toggleTag(report.account, 'disable_remote_subscription')">
{{ $t('users.disableRemoteSubscription') }}
<i v-if="report.account.tags.includes('disable_remote_subscription')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="report.account.local"
:class="{ 'active-tag': report.account.tags.includes('disable_any_subscription') }"
@click.native="toggleTag(report.account, 'disable_any_subscription')">
{{ $t('users.disableAnySubscription') }}
<i v-if="report.account.tags.includes('disable_any_subscription')" class="el-icon-check"/>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div>
<div class="line"/>
<span class="report-row-key">Account:</span>
<img
:src="report.account.avatar"
alt="avatar"
class="avatar-img">
<a :href="report.account.url" target="_blank" class="account">
<span class="report-row-value">{{ report.account.acct }}</span>
</a>
</div>
<div v-if="report.content.length > 0">
<div class="line"/>
<span class="report-row-key">Content:
<span class="report-row-value">{{ report.content }}</span>
</span>
</div>
<div>
<div class="line"/>
<span class="report-row-key">Actor:</span>
<img
:src="report.actor.avatar"
alt="avatar"
class="avatar-img">
<a :href="report.actor.url" target="_blank" class="account">
<span class="report-row-value">{{ report.actor.acct }}</span>
</a>
</div>
<div v-if="report.statuses.length > 0" class="statuses">
<el-collapse>
<statuses :report="report"/>
</el-collapse>
</div>
</el-card>
</el-timeline-item>
</template>
<script>
import moment from 'moment'
import Statuses from './Statuses'
export default {
name: 'TimelineItem',
components: { Statuses },
props: {
report: {
type: Object,
required: true
}
},
methods: {
changeReportState(reportState, reportId) {
this.$store.dispatch('ChangeReportState', { reportState, reportId })
},
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
getStateType(state) {
switch (state) {
case 'closed':
return 'info'
case 'resolved':
return 'success'
default:
return 'primary'
}
},
handleDeletion(user) {
this.$store.dispatch('DeleteUsers', [user])
},
parseTimestamp(timestamp) {
return moment(timestamp).format('L HH:mm')
},
showDeactivatedButton(id) {
return this.$store.state.user.id !== id
},
toggleActivation(user) {
user.deactivated
? this.$store.dispatch('ActivateUsers', [user])
: this.$store.dispatch('DeactivateUsers', [user])
},
toggleTag(user, tag) {
user.tags.includes(tag)
? this.$store.dispatch('RemoveTag', { users: [user], tag })
: this.$store.dispatch('AddTag', { users: [user], tag })
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.account {
text-decoration: underline;
}
.avatar-img {
vertical-align: bottom;
width: 15px;
height: 15px;
margin-left: 5px;
}
.el-card__body {
padding: 17px;
}
.el-card__header {
background-color: #FAFAFA;
padding: 10px 20px;
}
.el-collapse {
border-bottom: none;
}
.el-collapse-item__header {
height: 46px;
font-size: 14px;
}
.el-collapse-item__content {
padding-bottom: 7px;
}
.el-icon-arrow-right {
margin-right: 6px;
}
.el-icon-close {
padding: 10px 5px 10px 10px;
cursor: pointer;
}
h4 {
margin: 0;
height: 17px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: baseline;
height: 40px;
}
.id {
color: gray;
margin-top: 6px;
}
.line {
width: 100%;
height: 0;
border: 0.5px solid #EBEEF5;
margin: 15px 0 15px;
}
.new-note {
p {
font-size: 14px;
font-weight: 500;
height: 17px;
margin: 13px 0 7px;
}
}
.note {
box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
margin-bottom: 10px;
}
.no-notes {
font-style: italic;
color: gray;
}
.report-row-key {
font-size: 14px;
font-weight: 500;
}
.report-row-key {
font-size: 14px;
}
.report-title {
margin: 0;
}
.statuses {
margin-top: 15px;
}
.submit-button {
display: block;
margin: 7px 0 17px auto;
}
.timestamp {
margin: 0;
font-style: italic;
color: gray;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.timeline-item-container {
.header-container {
display: flex;
flex-direction: column;
height: 80px;
}
.id {
margin: 6px 0 0 0;
}
}
}
</style>

View file

@ -1,13 +1,22 @@
<template>
<div class="reports-container">
<h1>{{ $t('reports.reports') }}</h1>
<h1 v-if="groupReports">
{{ $t('reports.groupedReports') }}
<span class="report-count">({{ normalizedReportsCount }})</span>
</h1>
<h1 v-else>
{{ $t('reports.reports') }}
<span class="report-count">({{ normalizedReportsCount }})</span>
</h1>
<div class="filter-container">
<reports-filter/>
<reports-filter v-if="!groupReports"/>
<el-checkbox v-model="groupReports" class="group-reports-checkbox">
Group reports by statuses
</el-checkbox>
</div>
<div class="block">
<el-timeline class="timeline">
<timeline-item v-loading="loading" v-for="report in reports" :report="report" :key="report.id"/>
</el-timeline>
<grouped-report v-loading="loading" v-if="groupReports" :grouped-reports="groupedReports"/>
<report v-loading="loading" v-else :reports="reports"/>
<div v-if="reports.length === 0" class="no-reports-message">
<p>There are no reports to display</p>
</div>
@ -16,34 +25,44 @@
</template>
<script>
import TimelineItem from './components/TimelineItem'
import GroupedReport from './components/GroupedReport'
import numeral from 'numeral'
import Report from './components/Report'
import ReportsFilter from './components/ReportsFilter'
export default {
components: { TimelineItem, ReportsFilter },
components: { GroupedReport, Report, ReportsFilter },
computed: {
groupedReports() {
return this.$store.state.reports.fetchedGroupedReports
},
groupReports: {
get() {
return this.$store.state.reports.groupReports
},
set() {
this.toggleReportsGrouping()
}
},
loading() {
return this.$store.state.users.loading
return this.$store.state.reports.loading
},
normalizedReportsCount() {
return this.groupReports
? numeral(this.$store.state.reports.fetchedGroupedReports.length).format('0a')
: numeral(this.$store.state.reports.totalReportsCount).format('0a')
},
reports() {
return this.$store.state.reports.fetchedReports
}
},
mounted() {
this.$store.dispatch('FetchReports')
},
created() {
window.addEventListener('scroll', this.handleScroll)
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll)
this.$store.dispatch('FetchReports', 1)
this.$store.dispatch('FetchGroupedReports')
},
methods: {
handleScroll(reports) {
const bottomOfWindow = document.documentElement.scrollHeight - document.documentElement.scrollTop === document.documentElement.clientHeight
if (bottomOfWindow) {
this.$store.dispatch('FetchReports')
}
toggleReportsGrouping() {
this.$store.dispatch('ToggleReportsGrouping')
}
}
}
@ -56,16 +75,24 @@ export default {
padding: 0px;
}
.filter-container {
display: flex;
flex-direction: column;
margin: 22px 15px 22px 15px;
padding-bottom: 0
}
.group-reports-checkbox {
margin-top: 10px;
}
h1 {
margin: 22px 0 0 15px;
}
.no-reports-message {
color: gray;
margin-left: 19px
}
.report-count {
color: gray;
font-size: 28px;
}
}
@media
@ -78,9 +105,9 @@ only screen and (max-width: 760px),
.filter-container {
margin: 0 10px
}
.timeline {
margin: 20px 20px 20px 18px
}
#app > div > div.main-container > section > div > div.block > ul {
margin: 45px 45px 5px 19px;
}
}
</style>

269
src/views/status/Status.vue Normal file
View file

@ -0,0 +1,269 @@
<template>
<div>
<el-card v-if="!status.deleted" class="status-card">
<div slot="header">
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<img :src="status.account.avatar" class="status-avatar-img">
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
</div>
<a :href="status.account.url" target="_blank" class="account">
@{{ status.account.acct }}
</a>
</div>
<div class="status-actions">
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag>
<el-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button">
{{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="!status.sensitive"
@click.native="changeStatus(status.id, true, status.visibility)">
{{ $t('reports.addSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.sensitive"
@click.native="changeStatus(status.id, false, status.visibility)">
{{ $t('reports.removeSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'public'"
@click.native="changeStatus(status.id, status.sensitive, 'public')">
{{ $t('reports.public') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'private'"
@click.native="changeStatus(status.id, status.sensitive, 'private')">
{{ $t('reports.private') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'unlisted'"
@click.native="changeStatus(status.id, status.sensitive, 'unlisted')">
{{ $t('reports.unlisted') }}
</el-dropdown-item>
<el-dropdown-item
@click.native="deleteStatus(status.id)">
{{ $t('reports.deleteStatus') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<div class="status-body">
<div v-if="status.spoiler_text">
<strong>{{ status.spoiler_text }}</strong>
<el-button v-if="!showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = true">Show more</el-button>
<el-button v-if="showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = false">Show less</el-button>
<div v-if="showHiddenStatus">
<span class="status-content" v-html="status.content"/>
<div v-if="status.poll" class="poll">
<ul>
<li v-for="(option, index) in status.poll.options" :key="index">
{{ option.title }}
<el-progress :percentage="optionPercent(status.poll, option)" />
</li>
</ul>
</div>
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
<img :src="attachment.preview_url">
</div>
</div>
</div>
<div v-if="!status.spoiler_text">
<span class="status-content" v-html="status.content"/>
<div v-if="status.poll" class="poll">
<ul>
<li v-for="(option, index) in status.poll.options" :key="index">
{{ option.title }}
<el-progress :percentage="optionPercent(status.poll, option)" />
</li>
</ul>
</div>
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
<img :src="attachment.preview_url">
</div>
</div>
<a :href="status.url" target="_blank" class="account">
{{ parseTimestamp(status.created_at) }}
</a>
</div>
</el-card>
<el-card v-else class="status-card">
<div slot="header">
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<h4 class="status-deleted">{{ $t('reports.statusDeleted') }}</h4>
</div>
</div>
</div>
</div>
<div class="status-body">
<span v-if="status.content" class="status-content" v-html="status.content"/>
<span v-else class="status-without-content">no content</span>
</div>
<a v-if="status.created_at" :href="status.url" target="_blank" class="account">
{{ parseTimestamp(status.created_at) }}
</a>
</el-card>
</div>
</template>
<script>
import moment from 'moment'
export default {
name: 'Status',
props: {
status: {
type: Object,
required: true
},
page: {
type: Number,
required: false,
default: 0
},
userId: {
type: String,
required: false,
default: ''
},
godmode: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
showHiddenStatus: false
}
},
methods: {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
changeStatus(statusId, isSensitive, visibility) {
this.$store.dispatch('ChangeStatusScope', { statusId, isSensitive, visibility, reportCurrentPage: this.page, userId: this.userId, godmode: this.godmode })
},
deleteStatus(statusId) {
this.$confirm('Are you sure you want to delete this status?', 'Warning', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeleteStatus', { statusId, reportCurrentPage: this.page, userId: this.userId, godmode: this.godmode })
this.$message({
type: 'success',
message: 'Delete completed'
})
}).catch(() => {
this.$message({
type: 'info',
message: 'Delete canceled'
})
})
},
optionPercent(poll, pollOption) {
const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0)
if (allVotes === 0) {
return 0
}
return +(pollOption.votes_count / allVotes * 100).toFixed(1)
},
parseTimestamp(timestamp) {
return moment(timestamp).format('YYYY-MM-DD HH:mm')
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.account {
text-decoration: underline;
line-height: 26px;
font-size: 13px;
}
.image {
width: 20%;
img {
width: 100%;
}
}
.show-more-button {
margin-left: 5px;
}
.status-account {
display: flex;
align-items: center;
}
.status-avatar-img {
width: 15px;
height: 15px;
margin-right: 5px;
}
.status-account-name {
margin: 0;
height: 22px;
}
.status-body {
display: flex;
flex-direction: column;
}
.status-content {
font-size: 15px;
line-height: 26px;
}
.status-card {
margin-bottom: 15px;
}
.status-deleted {
font-style: italic;
margin-top: 3px;
}
.status-header {
display: flex;
justify-content: space-between;
}
.status-without-content {
font-style: italic;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.el-message {
min-width: 80%;
}
.el-message-box {
width: 80%;
}
.status-card {
.el-card__header {
padding: 10px 17px;
}
.el-tag {
margin: 3px 4px 3px 0;
}
.status-account-container {
margin-bottom: 5px;
}
.status-actions-button {
margin: 3px 0 3px;
}
.status-actions {
display: flex;
flex-wrap: wrap;
}
.status-header {
display: flex;
flex-direction: column;
}
}
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<main v-if="!loading">
<main v-if="!userProfileLoading">
<header>
<el-avatar :src="user.avatar" size="large" />
<h1>{{ user.display_name }}</h1>
@ -71,23 +71,9 @@
</el-col>
</el-row>
<el-col :span="18">
<el-timeline class="statuses">
<el-timeline-item v-for="status in statuses" :timestamp="createdAtLocaleString(status.created_at)" :key="status.id">
<el-card>
<strong v-if="status.spoiler_text">{{ status.spoiler_text }}</strong>
<p v-if="status.content" v-html="status.content" />
<div v-if="status.poll" class="poll">
<ul>
<li v-for="(option, index) in status.poll.options" :key="index">
{{ option.title }}
<el-progress :percentage="optionPercent(status.poll, option)" />
</li>
</ul>
</div>
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
<img :src="attachment.preview_url">
</div>
</el-card>
<el-timeline v-if="!statusesLoading" class="statuses">
<el-timeline-item v-for="status in statuses" :key="status.id">
<status :status="status" :user-id="user.id" :godmode="showPrivate"/>
</el-timeline-item>
</el-timeline>
</el-col>
@ -96,45 +82,36 @@
</template>
<script>
import Status from '../status/Status'
export default {
name: 'UsersShow',
components: { Status },
data() {
return {
showPrivate: false
}
},
computed: {
loading() {
return this.$store.state.userProfile.loading
statuses() {
return this.$store.state.userProfile.statuses
},
statusesLoading() {
return this.$store.state.userProfile.statusesLoading
},
user() {
return this.$store.state.userProfile.user
},
statuses() {
return this.$store.state.userProfile.statuses
userProfileLoading() {
return this.$store.state.userProfile.userProfileLoading
}
},
mounted: function() {
this.$store.dispatch('FetchData', { id: this.$route.params.id, godmode: false })
this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: false })
},
methods: {
optionPercent(poll, pollOption) {
const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0)
if (allVotes === 0) {
return 0
}
return +(pollOption.votes_count / allVotes * 100).toFixed(1)
},
createdAtLocaleString(createdAt) {
const date = new Date(createdAt)
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
},
onTogglePrivate() {
console.log(this.showPrivate)
this.$store.dispatch('FetchData', { id: this.$route.params.id, godmode: this.showPrivate })
this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: this.showPrivate })
}
}
}

View file

@ -0,0 +1,47 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import GroupedReport from '@/views/reports/components/GroupedReport'
import storeConfig from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'
config.mocks["$t"] = () => {}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Element)
jest.mock('@/api/reports')
describe('Grouped report', () => {
let store
beforeEach(async() => {
store = new Vuex.Store(cloneDeep(storeConfig))
store.dispatch('FetchGroupedReports')
await flushPromises()
})
it('changes state of all reports in a group', async (done) => {
const groupedReports = store.state.reports.fetchedGroupedReports
const wrapper = mount(GroupedReport, {
store,
localVue,
propsData: {
groupedReports
}
})
expect(groupedReports[0].reports[0].state).toBe('open')
expect(groupedReports[0].reports[1].state).toBe('resolved')
const button = wrapper.find(`.grouped-report .el-dropdown-menu__item:nth-child(3)`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedGroupedReports[0].reports[0].state).toBe('closed')
expect(store.state.reports.fetchedGroupedReports[0].reports[1].state).toBe('closed')
done()
})
})

View file

@ -31,22 +31,7 @@ describe('Reports', () => {
await flushPromises()
const initialReports = store.state.reports.fetchedReports.length
expect(initialReports).toEqual(5)
done()
})
it('loads more reports on scroll', async (done) => {
const wrapper = mount(Reports, {
store,
localVue
})
await flushPromises()
expect(store.state.reports.fetchedReports.length).toEqual(5)
window.dispatchEvent(new CustomEvent('scroll', { detail: 2000 }))
await flushPromises()
expect(store.state.reports.fetchedReports.length).toEqual(7)
expect(initialReports).toEqual(7)
done()
})
})

View file

@ -0,0 +1,75 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import Report from '@/views/reports/components/Report'
import storeConfig from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'
config.mocks["$t"] = () => {}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Element)
jest.mock('@/api/reports')
describe('Report in a timeline', () => {
let store
beforeEach(async() => {
store = new Vuex.Store(cloneDeep(storeConfig))
store.dispatch('FetchReports')
await flushPromises()
})
it('changes report state from open to resolved', async (done) => {
const reports = store.state.reports.fetchedReports
const wrapper = mount(Report, {
store,
localVue,
propsData: {
reports
}
})
expect(reports[0].state).toBe('open')
const button = wrapper.find(`li.el-timeline-item:nth-child(1) li.el-dropdown-menu__item:nth-child(1)`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[0].state).toBe('resolved')
done()
})
it('changes report state from open to closed', async (done) => {
const reports = store.state.reports.fetchedReports
const wrapper = mount(Report, {
store,
localVue,
propsData: {
reports
}
})
expect(reports[3].state).toBe('open')
const button = wrapper.find(`li.el-timeline-item:nth-child(4) li.el-dropdown-menu__item:nth-child(2)`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[3].state).toBe('closed')
done()
})
it('shows statuses', () => {
const reports = store.state.reports.fetchedReports
const wrapper = mount(Report, {
store,
localVue,
propsData: {
reports
}
})
const statuses = wrapper.findAll(`.status-card`)
expect(statuses.length).toEqual(2)
})
})

View file

@ -24,11 +24,11 @@ describe('Reports filter', () => {
})
it('shows open reports when "Open" filter is applied', async (done) => {
expect(store.state.reports.fetchedReports.length).toEqual(5)
expect(store.state.reports.fetchedReports.length).toEqual(7)
store.dispatch('SetFilter', 'open')
store.dispatch('ClearFetchedReports')
store.dispatch('FetchReports')
store.dispatch('FetchReports', 1)
await flushPromises()
expect(store.state.reports.fetchedReports.length).toEqual(2)
@ -36,7 +36,7 @@ describe('Reports filter', () => {
})
it('shows resolved reports when "Resolved" filter is applied', async (done) => {
expect(store.state.reports.fetchedReports.length).toEqual(5)
expect(store.state.reports.fetchedReports.length).toEqual(7)
store.dispatch('SetFilter', 'resolved')
store.dispatch('ClearFetchedReports')
@ -48,7 +48,7 @@ describe('Reports filter', () => {
})
it('shows closed reports when "Closed" filter is applied', async (done) => {
expect(store.state.reports.fetchedReports.length).toEqual(5)
expect(store.state.reports.fetchedReports.length).toEqual(7)
store.dispatch('SetFilter', 'closed')
store.dispatch('ClearFetchedReports')
@ -60,7 +60,7 @@ describe('Reports filter', () => {
})
it('shows all users after removing filters', async (done) => {
expect(store.state.reports.fetchedReports.length).toEqual(5)
expect(store.state.reports.fetchedReports.length).toEqual(7)
store.dispatch('SetFilter', 'open')
store.dispatch('ClearFetchedReports')
@ -72,7 +72,7 @@ describe('Reports filter', () => {
store.dispatch('ClearFetchedReports')
store.dispatch('FetchReports')
await flushPromises()
expect(store.state.reports.fetchedReports.length).toEqual(5)
expect(store.state.reports.fetchedReports.length).toEqual(7)
done()
})

View file

@ -0,0 +1,151 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import Status from '@/views/status/Status'
import storeConfig from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'
config.mocks["$t"] = () => {}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Element)
jest.mock('@/api/reports')
jest.mock('@/api/status')
describe('Status in reports', () => {
let store
beforeEach(async() => {
store = new Vuex.Store(cloneDeep(storeConfig))
store.dispatch('FetchReports', 1)
await flushPromises()
})
it('adds sensitive flag to a status', async (done) => {
const status = store.state.reports.fetchedReports[4].statuses[0]
const wrapper = mount(Status, {
store,
localVue,
propsData: {
status,
page: 1,
userId: '7',
godmode: false
}
})
await flushPromises()
const changeStatusStub = jest.fn()
wrapper.setMethods({ changeStatus: changeStatusStub })
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(1)`)
button.trigger('click')
expect(wrapper.vm.changeStatus).toHaveBeenCalled()
expect(wrapper.vm.changeStatus).toHaveBeenCalledWith('11', true, 'public')
done()
})
it('removes sensitive flag to a status', async (done) => {
const status = store.state.reports.fetchedReports[4].statuses[1]
const wrapper = mount(Status, {
store,
localVue,
propsData: {
status,
page: 1,
userId: '7',
godmode: false
}
})
await flushPromises()
const changeStatusStub = jest.fn()
wrapper.setMethods({ changeStatus: changeStatusStub })
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(1)`)
button.trigger('click')
expect(wrapper.vm.changeStatus).toHaveBeenCalled()
expect(wrapper.vm.changeStatus).toHaveBeenCalledWith('10', false, 'unlisted')
done()
})
it('changes status visibility from public to unlisted', async (done) => {
const status = store.state.reports.fetchedReports[4].statuses[0]
const wrapper = mount(Status, {
store,
localVue,
propsData: {
status,
page: 1,
userId: '7',
godmode: false
}
})
await flushPromises()
const changeStatusStub = jest.fn()
wrapper.setMethods({ changeStatus: changeStatusStub })
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(3)`)
button.trigger('click')
expect(wrapper.vm.changeStatus).toHaveBeenCalled()
expect(wrapper.vm.changeStatus).toHaveBeenCalledWith('11', false, 'unlisted')
done()
})
it('changes status visibility from unlisted to private', async (done) => {
const status = store.state.reports.fetchedReports[4].statuses[1]
const wrapper = mount(Status, {
store,
localVue,
propsData: {
status,
page: 1,
userId: '7',
godmode: false
}
})
await flushPromises()
const changeStatusStub = jest.fn()
wrapper.setMethods({ changeStatus: changeStatusStub })
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(3)`)
button.trigger('click')
expect(wrapper.vm.changeStatus).toHaveBeenCalled()
expect(wrapper.vm.changeStatus).toHaveBeenCalledWith('10', true, 'private')
done()
})
it('deletes a status', async (done) => {
const status = store.state.reports.fetchedReports[4].statuses[1]
const wrapper = mount(Status, {
store,
localVue,
propsData: {
status,
page: 1,
userId: '7',
godmode: false
}
})
await flushPromises()
const deleteStatusStub = jest.fn()
wrapper.setMethods({ deleteStatus: deleteStatusStub })
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(4)`)
button.trigger('click')
expect(wrapper.vm.deleteStatus).toHaveBeenCalled()
expect(wrapper.vm.deleteStatus).toHaveBeenCalledWith('10')
done()
})
})

View file

@ -2,6 +2,7 @@ import app from '@/store/modules/app'
import user from '@/store/modules/user'
import users from '@/store/modules/users'
import reports from '@/store/modules/reports'
import status from '@/store/modules/status'
import getters from '@/store/getters'
export default {
@ -9,7 +10,8 @@ export default {
app,
user,
users,
reports
reports,
status
},
getters
}

View file

@ -1,157 +0,0 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import TimelineItem from '@/views/reports/components/TimelineItem'
import storeConfig from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'
config.mocks["$t"] = () => {}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Element)
jest.mock('@/api/reports')
describe('Report in a timeline', () => {
let store
beforeEach(async() => {
store = new Vuex.Store(cloneDeep(storeConfig))
store.dispatch('FetchReports')
await flushPromises()
})
it('changes report state from open to resolved', async (done) => {
const report = store.state.reports.fetchedReports[0]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
expect(report.state).toBe('open')
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(${1})`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[0].state).toBe('resolved')
done()
})
it('changes report state from open to closed', async (done) => {
const report = store.state.reports.fetchedReports[3]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
expect(report.state).toBe('open')
const button = wrapper.find(`li.el-dropdown-menu__item:nth-child(${2})`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[3].state).toBe('closed')
done()
})
it('shows statuses', () => {
const report = store.state.reports.fetchedReports[4]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
const statuses = wrapper.findAll(`.status-card`)
expect(statuses.length).toEqual(2)
})
it('adds sensitive flag to a status', async (done) => {
const report = store.state.reports.fetchedReports[4]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
expect(report.statuses[0].sensitive).toBe(false)
const button = wrapper.find(`.status-card li.el-dropdown-menu__item`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[4].statuses[0].sensitive).toEqual(true)
done()
})
it('removes sensitive flag to a status', async (done) => {
const report = store.state.reports.fetchedReports[4]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
expect(report.statuses[1].sensitive).toBe(true)
const button = wrapper.find(`.status-card:nth-child(${2}) li.el-dropdown-menu__item`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[4].statuses[1].sensitive).toEqual(false)
done()
})
it('changes status visibility from public to unlisted', async (done) => {
const report = store.state.reports.fetchedReports[4]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
expect(report.statuses[0].visibility).toBe('public')
const button = wrapper.find(`.status-card li.el-dropdown-menu__item:nth-child(${3})`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[4].statuses[0].visibility).toEqual('unlisted')
done()
})
it('changes status visibility from unlisted to private', async (done) => {
const report = store.state.reports.fetchedReports[4]
const wrapper = mount(TimelineItem, {
store,
localVue,
propsData: {
report: report
}
})
expect(report.statuses[1].visibility).toBe('unlisted')
const button = wrapper.find(`.status-card:nth-child(${2}) li.el-dropdown-menu__item:nth-child(${3})`)
button.trigger('click')
await flushPromises()
expect(store.state.reports.fetchedReports[4].statuses[1].visibility).toEqual('private')
done()
})
it('deletes a status', async (done) => {
const report = store.state.reports.fetchedReports[4]
expect(report.statuses.length).toEqual(2)
store.dispatch('DeleteStatus', { statusId: '11', reportId: '7'})
await flushPromises()
expect(store.state.reports.fetchedReports[4].statuses.length).toEqual(1)
done()
})
})