diff --git a/src/api/__mocks__/reports.js b/src/api/__mocks__/reports.js index e277d8df9be863e97e99087c20228c841a379f7e..ba4e412e5dd3464245801532e458936a21b36e13 100644 --- a/src/api/__mocks__/reports.js +++ b/src/api/__mocks__/reports.js @@ -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: '' }) } diff --git a/src/api/__mocks__/status.js b/src/api/__mocks__/status.js new file mode 100644 index 0000000000000000000000000000000000000000..0e7bc9fb6c90715daa4fe4a4dc30e85cd02425b6 --- /dev/null +++ b/src/api/__mocks__/status.js @@ -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() +} diff --git a/src/api/reports.js b/src/api/reports.js index 79833e40f71952bcf9633fe8157844d28b358c27..373e5bd5bd6f89b959e431e4ff47d9243c8889b6 100644 --- a/src/api/reports.js +++ b/src/api/reports.js @@ -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) }) diff --git a/src/api/status.js b/src/api/status.js new file mode 100644 index 0000000000000000000000000000000000000000..7d931ae04c73f3c150883035d56fb07c0e8c974a --- /dev/null +++ b/src/api/status.js @@ -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()}` } : {} diff --git a/src/lang/en.js b/src/lang/en.js index fee486d06f47a02b4ef8b12f4bf396efc1a9e1a3..d48934556dd6de016a586ab3b8a737959f20b231 100644 --- a/src/lang/en.js +++ b/src/lang/en.js @@ -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', diff --git a/src/store/index.js b/src/store/index.js index c71fca877b61e34f3bca306d5c28c4fe3b947875..87301b69ce68223eaa7c1c579cce037b5a5cc224 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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, diff --git a/src/store/modules/reports.js b/src/store/modules/reports.js index e44664f437ac467ecaef7985e84fc66d7828c108..d62f1b55e2e3faffd7869738a0f1c85c993c1166 100644 --- a/src/store/modules/reports.js +++ b/src/store/modules/reports.js @@ -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 }) - 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 - } + 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) + 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) - - const reports = state.fetchedReports.concat(response.data.reports) - const id = reports.length > 0 ? reports[reports.length - 1].id : state.idOfLastReport + 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) - 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') } } } diff --git a/src/store/modules/status.js b/src/store/modules/status.js new file mode 100644 index 0000000000000000000000000000000000000000..7d19d895ecd43fb44ad54f5df5cdbddcb0abe58c --- /dev/null +++ b/src/store/modules/status.js @@ -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 diff --git a/src/store/modules/userProfile.js b/src/store/modules/userProfile.js index 2dd656e688569514b4192a10d6c7b044059748fd..5a7e4394cd84e6019256c5b3b63ae38ad556c510 100644 --- a/src/store/modules/userProfile.js +++ b/src/store/modules/userProfile.js @@ -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) } } } diff --git a/src/views/reports/components/GroupedReport.vue b/src/views/reports/components/GroupedReport.vue new file mode 100644 index 0000000000000000000000000000000000000000..e11ba313882bdc76380c77f95c423a901b3cfc8f --- /dev/null +++ b/src/views/reports/components/GroupedReport.vue @@ -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> diff --git a/src/views/reports/components/ModerateUserDropdown.vue b/src/views/reports/components/ModerateUserDropdown.vue new file mode 100644 index 0000000000000000000000000000000000000000..2d3689991e97590f1e4eff62553337d9033e4a98 --- /dev/null +++ b/src/views/reports/components/ModerateUserDropdown.vue @@ -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> diff --git a/src/views/reports/components/Report.vue b/src/views/reports/components/Report.vue new file mode 100644 index 0000000000000000000000000000000000000000..7fc5d0519ac2a03f249dc85438c3f7de2c74029e --- /dev/null +++ b/src/views/reports/components/Report.vue @@ -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> diff --git a/src/views/reports/components/ReportCard.vue b/src/views/reports/components/ReportCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..67b0b01ceecfd4cdb5bdba60af87d80e666ab33e --- /dev/null +++ b/src/views/reports/components/ReportCard.vue @@ -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> diff --git a/src/views/reports/components/ReportsFilter.vue b/src/views/reports/components/ReportsFilter.vue index 40c232165fba4fb8b8be9b1678bfe00cf02ab147..da1fd7820813c045bbec9c149ec32873e7c0571e 100644 --- a/src/views/reports/components/ReportsFilter.vue +++ b/src/views/reports/components/ReportsFilter.vue @@ -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) } } } diff --git a/src/views/reports/components/Statuses.vue b/src/views/reports/components/Statuses.vue deleted file mode 100644 index a385a700af4e98f004221911ee76bdfedd259e73..0000000000000000000000000000000000000000 --- a/src/views/reports/components/Statuses.vue +++ /dev/null @@ -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> diff --git a/src/views/reports/components/TimelineItem.vue b/src/views/reports/components/TimelineItem.vue deleted file mode 100644 index e7e41e7ba91f1f93302ab898c477337b057db831..0000000000000000000000000000000000000000 --- a/src/views/reports/components/TimelineItem.vue +++ /dev/null @@ -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> diff --git a/src/views/reports/index.vue b/src/views/reports/index.vue index 63a830ae9352aacdc87c44fb8419f3e4d63e3b15..3c87d3e52138e70238d84da904a12290319472c9 100644 --- a/src/views/reports/index.vue +++ b/src/views/reports/index.vue @@ -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> diff --git a/src/views/status/Status.vue b/src/views/status/Status.vue new file mode 100644 index 0000000000000000000000000000000000000000..b33744b1db2f60e7ae764c45214c796b3bda7b90 --- /dev/null +++ b/src/views/status/Status.vue @@ -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> diff --git a/src/views/users/show.vue b/src/views/users/show.vue index 7c733d13a1511aa3ede225d6645fe16bf9078371..f721a7f5aeab06616d9cea7d0fe6f4cfb1f27325 100644 --- a/src/views/users/show.vue +++ b/src/views/users/show.vue @@ -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 }) } } } diff --git a/test/views/reports/groupedReport.test.js b/test/views/reports/groupedReport.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b24d05e45ffaeec29447517c8170f8c9531f7d05 --- /dev/null +++ b/test/views/reports/groupedReport.test.js @@ -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() + }) +}) diff --git a/test/views/reports/index.test.js b/test/views/reports/index.test.js index cb1f10dad5ab8ac82066279e97c965b69a11ee3d..eab73c81ca196919f67fd8ee0970b1007165fe9b 100644 --- a/test/views/reports/index.test.js +++ b/test/views/reports/index.test.js @@ -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() }) }) diff --git a/test/views/reports/report.test.js b/test/views/reports/report.test.js new file mode 100644 index 0000000000000000000000000000000000000000..cc91c6ca5f3ee656e657fffd93a50597bf5e9a80 --- /dev/null +++ b/test/views/reports/report.test.js @@ -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) + }) +}) diff --git a/test/views/reports/reportsFilter.test.js b/test/views/reports/reportsFilter.test.js index 60e9147adf21e018ce11c6845d34c13cc9e44f09..0701262cbb11319043313f2b5635eea532e27fd5 100644 --- a/test/views/reports/reportsFilter.test.js +++ b/test/views/reports/reportsFilter.test.js @@ -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() }) diff --git a/test/views/reports/status.test.js b/test/views/reports/status.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2414fa8e375266fb458b22f046e84af0a28ea39b --- /dev/null +++ b/test/views/reports/status.test.js @@ -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() + }) +}) diff --git a/test/views/reports/store.conf.js b/test/views/reports/store.conf.js index a961ccca32ba18cb87f2869e632929214269bf08..72117897b11c69ffdb33af4572dea3008f2b48d1 100644 --- a/test/views/reports/store.conf.js +++ b/test/views/reports/store.conf.js @@ -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 } diff --git a/test/views/reports/timelineItem.test.js b/test/views/reports/timelineItem.test.js deleted file mode 100644 index 6e3b9f01ce3e6aba1d90a618c8b90470f67e5d5e..0000000000000000000000000000000000000000 --- a/test/views/reports/timelineItem.test.js +++ /dev/null @@ -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() - }) -})