Merge branch 'develop' into 'feature/do-not-show-users-with-null-nicknames'

# Conflicts:
#   src/components/Status/index.vue
#   src/views/users/components/ModerationDropdown.vue
#   src/views/users/show.vue
This commit is contained in:
Angelina Filippova 2020-06-06 21:10:12 +00:00
commit 12bac96c9d
19 changed files with 757 additions and 191 deletions

View file

@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added
- Create `/statuses/:id` route that shows single status
### Changed ### Changed
- Statuses count changes when an instance is selected and shows the amount of statuses from an originating instance - Statuses count changes when an instance is selected and shows the amount of statuses from an originating instance
@ -18,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Send `true` and `false` as booleans if they are values of single selects on the Settings page - Send `true` and `false` as booleans if they are values of single selects on the Settings page
- Fix sorting users on Users page if there is an acount with missing nickname or ID
## [2.0.3] - 2020-04-29 ## [2.0.3] - 2020-04-29

View file

@ -6,6 +6,29 @@ export async function deleteStatus(id, authHost, token) {
return Promise.resolve() return Promise.resolve()
} }
export async function fetchStatus(id, authHost, token) {
const data = {
account: {
id: '9n1bySks25olxWrku0',
avatar: 'http://localhost:4000/images/avi.png',
display_name: 'dolin',
tags: ['strip_media', 'sandbox', 'disable_any_subscription', 'force_nsfw'],
url: 'http://localhost:4000/users/dolin'
},
content: 'pizza makes everything better',
created_at: '2020-05-22T17:34:34.000Z',
id: '9vJOO3iFPyjNaEhJ5s',
media_attachments: [],
poll: null,
sensitive: false,
spoiler_text: '',
visibility: 'public',
url: 'http://localhost:4000/notice/9vJOO3iFPyjNaEhJ5s'
}
return Promise.resolve({ data })
}
export async function fetchStatusesByInstance({ instance, authHost, token, pageSize, page }) { export async function fetchStatusesByInstance({ instance, authHost, token, pageSize, page }) {
let data let data
if (pageSize === 1) { if (pageSize === 1) {

View file

@ -6,7 +6,11 @@ export let users = [
const userProfile = { avatar: 'avatar.jpg', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false } const userProfile = { avatar: 'avatar.jpg', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false }
const userStatuses = [] const userStatuses = [
{ account: { id: '9n1bySks25olxWrku0', display_name: 'dolin' }, content: 'pizza makes everything better', id: '9vJOO3iFPyjNaEhJ5s', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' },
{ account: { id: '9n1bySks25olxWrku0', display_name: 'dolin' }, content: 'pizza time', id: '9vJPD5XKOdzQ0bvGLY', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' },
{ account: { id: '9n1bySks25olxWrku0', display_name: 'dolin' }, content: 'what is yout favorite pizza?', id: '9jop82OBXeFPYulVjM', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' }
]
const filterUsers = (str) => { const filterUsers = (str) => {
const filters = str.split(',').filter(item => item.length > 0) const filters = str.split(',').filter(item => item.length > 0)

View file

@ -21,6 +21,15 @@ export async function deleteStatus(id, authHost, token) {
}) })
} }
export async function fetchStatus(id, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/statuses/${id}`,
method: 'get',
headers: authHeaders(token)
})
}
export async function fetchStatuses({ godmode, localOnly, authHost, token, pageSize, page }) { export async function fetchStatuses({ godmode, localOnly, authHost, token, pageSize, page }) {
return await request({ return await request({
baseURL: baseName(authHost), baseURL: baseName(authHost),

View file

@ -1,15 +1,14 @@
<template> <template>
<div> <el-card v-if="!status.deleted" class="status-card" @click.native="handleRouteChange()">
<el-card v-if="!status.deleted" class="status-card">
<div slot="header"> <div slot="header">
<div class="status-header"> <div class="status-header">
<div class="status-account-container"> <div class="status-account-container">
<div class="status-account"> <div class="status-account">
<el-checkbox v-if="showCheckbox" class="status-checkbox" @change="handleStatusSelection(account)"/> <el-checkbox v-if="showCheckbox" class="status-checkbox" @change="handleStatusSelection(account)"/>
<router-link v-if="propertyExists(account, 'id')" :to="{ name: 'UsersShow', params: { id: account.id }}" @click.native.stop>
<div class="status-card-header">
<img v-if="propertyExists(account, 'avatar')" :src="account.avatar" class="status-avatar-img"> <img v-if="propertyExists(account, 'avatar')" :src="account.avatar" class="status-avatar-img">
<a v-if="propertyExists(account, 'url', 'nickname')" :href="account.url" target="_blank" class="account"> <span v-if="propertyExists(account, 'nickname')" class="status-account-name">{{ account.nickname }}</span>
<span class="status-account-name">{{ account.nickname }}</span>
</a>
<span v-else> <span v-else>
<span v-if="propertyExists(account, 'nickname')" class="status-account-name"> <span v-if="propertyExists(account, 'nickname')" class="status-account-name">
{{ account.nickname }} {{ account.nickname }}
@ -17,11 +16,15 @@
<span v-else class="status-account-name deactivated">({{ $t('users.invalidNickname') }})</span> <span v-else class="status-account-name deactivated">({{ $t('users.invalidNickname') }})</span>
</span> </span>
</div> </div>
</router-link>
</div>
</div> </div>
<div class="status-actions"> <div class="status-actions">
<div class="status-tags">
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag> <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-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
<el-dropdown trigger="click"> </div>
<el-dropdown trigger="click" @click.native.stop>
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button"> <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"/> {{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
</el-button> </el-button>
@ -94,10 +97,14 @@
<img :src="attachment.preview_url"> <img :src="attachment.preview_url">
</div> </div>
</div> </div>
<a :href="status.url" target="_blank" class="account"> <div class="status-footer">
{{ parseTimestamp(status.created_at) }} <span class="status-created-at">{{ parseTimestamp(status.created_at) }}</span>
<a v-if="status.url" :href="status.url" target="_blank" class="account" @click.stop>
Open status in instance
<i class="el-icon-top-right"/>
</a> </a>
</div> </div>
</div>
</el-card> </el-card>
<el-card v-else class="status-card"> <el-card v-else class="status-card">
<div slot="header"> <div slot="header">
@ -113,11 +120,14 @@
<span v-if="status.content" class="status-content" v-html="status.content"/> <span v-if="status.content" class="status-content" v-html="status.content"/>
<span v-else class="status-without-content">no content</span> <span v-else class="status-without-content">no content</span>
</div> </div>
<a v-if="status.created_at" :href="status.url" target="_blank" class="account"> <div class="status-footer">
{{ parseTimestamp(status.created_at) }} <span v-if="status.created_at" class="status-created-at">{{ parseTimestamp(status.created_at) }}</span>
<a v-if="status.url" :href="status.url" target="_blank" class="account" @click.stop>
Open status in instance
<i class="el-icon-top-right"/>
</a> </a>
</el-card>
</div> </div>
</el-card>
</template> </template>
<script> <script>
@ -208,6 +218,9 @@ export default {
handleStatusSelection(account) { handleStatusSelection(account) {
this.$emit('status-selection', account) this.$emit('status-selection', account)
}, },
handleRouteChange() {
this.$router.push({ name: 'StatusShow', params: { id: this.status.id }})
},
optionPercent(poll, pollOption) { optionPercent(poll, pollOption) {
const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0) const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0)
if (allVotes === 0) { if (allVotes === 0) {
@ -231,10 +244,14 @@ export default {
<style rel='stylesheet/scss' lang='scss'> <style rel='stylesheet/scss' lang='scss'>
.status-card { .status-card {
margin-bottom: 10px; margin-bottom: 10px;
cursor: pointer;
.account { .account {
text-decoration: underline;
line-height: 26px; line-height: 26px;
font-size: 13px; font-size: 13px;
color: #606266;
}
.account:hover {
text-decoration: underline;
} }
.deactivated { .deactivated {
color: gray; color: gray;
@ -271,6 +288,10 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.status-card-header {
display: flex;
align-items: center;
}
.status-checkbox { .status-checkbox {
margin-right: 7px; margin-right: 7px;
} }
@ -278,13 +299,26 @@ export default {
font-size: 15px; font-size: 15px;
line-height: 26px; line-height: 26px;
} }
.status-created-at {
font-size: 13px;
color: #606266;
}
.status-deleted { .status-deleted {
font-style: italic; font-style: italic;
margin-top: 3px; margin-top: 3px;
} }
.status-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-header { .status-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
}
.status-tags {
display: inline;
} }
.status-without-content { .status-without-content {
font-style: italic; font-style: italic;
@ -303,7 +337,7 @@ export default {
padding: 10px 17px; padding: 10px 17px;
} }
.el-tag { .el-tag {
margin: 3px 4px 3px 0; margin: 3px 0;
} }
.status-account-container { .status-account-container {
margin-bottom: 5px; margin-bottom: 5px;
@ -312,12 +346,20 @@ export default {
margin: 3px 0 3px; margin: 3px 0 3px;
} }
.status-actions { .status-actions {
width: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between;
}
.status-footer {
flex-direction: column;
align-items: flex-start;
margin-top: 10px;
} }
.status-header { .status-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start;
} }
} }
} }

View file

@ -171,16 +171,16 @@ export default {
id: 'ID', id: 'ID',
name: 'Name', name: 'Name',
status: 'Status', status: 'Status',
local: 'local', local: 'Local',
external: 'external', external: 'External',
deactivated: 'deactivated', deactivated: 'Deactivated',
active: 'active', active: 'Active',
unconfirmed: 'unconfirmed', unconfirmed: 'Unconfirmed',
actions: 'Actions', actions: 'Actions',
activate: 'Activate', activate: 'Activate',
deactivate: 'Deactivate', deactivate: 'Deactivate',
admin: 'admin', admin: 'Admin',
moderator: 'moderator', moderator: 'Moderator',
moderation: 'Moderation', moderation: 'Moderation',
revokeAdmin: 'Revoke Admin', revokeAdmin: 'Revoke Admin',
grantAdmin: 'Grant Admin', grantAdmin: 'Grant Admin',
@ -205,8 +205,8 @@ export default {
moderateUser: 'Moderate user', moderateUser: 'Moderate user',
moderateUsers: 'Moderate multiple users', moderateUsers: 'Moderate multiple users',
createAccount: 'Create new account', createAccount: 'Create new account',
apply: 'apply', apply: 'Apply',
remove: 'remove', remove: 'Remove',
grantRightConfirmation: 'Are you sure you want to grant {right} rights to all selected users?', grantRightConfirmation: 'Are you sure you want to grant {right} rights to all selected users?',
revokeRightConfirmation: 'Are you sure you want to revoke {right} rights from all selected users?', revokeRightConfirmation: 'Are you sure you want to revoke {right} rights from all selected users?',
activateMultipleUsersConfirmation: 'Are you sure you want to activate accounts of all selected users?', activateMultipleUsersConfirmation: 'Are you sure you want to activate accounts of all selected users?',
@ -258,15 +258,15 @@ export default {
tags: 'Tags', tags: 'Tags',
moderator: 'Moderator', moderator: 'Moderator',
admin: 'Admin', admin: 'Admin',
local: 'local', local: 'Local',
external: 'external', external: 'External',
localUppercase: 'Local', accountType: 'Account type',
nickname: 'Nickname', nickname: 'Nickname',
recentStatuses: 'Recent Statuses', recentStatuses: 'Recent Statuses',
roles: 'Roles', roles: 'Roles',
activeUppercase: 'Active', active: 'Active',
active: 'active', status: 'Status',
deactivated: 'deactivated', deactivated: 'Deactivated',
noStatuses: 'No statuses to show', noStatuses: 'No statuses to show',
securitySettings: { securitySettings: {
email: 'Email', email: 'Email',
@ -286,7 +286,7 @@ export default {
}, },
usersFilter: { usersFilter: {
inputPlaceholder: 'Select filter', inputPlaceholder: 'Select filter',
byUserType: 'By user type', byAccountType: 'By account type',
local: 'Local', local: 'Local',
external: 'External', external: 'External',
byStatus: 'By status', byStatus: 'By status',

View file

@ -172,5 +172,17 @@ export const asyncRouterMap = [
], ],
hidden: true hidden: true
}, },
{
path: '/statuses/:id',
component: Layout,
children: [
{
path: '',
name: 'StatusShow',
component: () => import('@/views/statuses/show')
}
],
hidden: true
},
{ path: '*', redirect: '/404', hidden: true } { path: '*', redirect: '/404', hidden: true }
] ]

View file

@ -1,9 +1,11 @@
import { changeStatusScope, deleteStatus, fetchStatuses, fetchStatusesCount, fetchStatusesByInstance } from '@/api/status' import { changeStatusScope, deleteStatus, fetchStatus, fetchStatuses, fetchStatusesCount, fetchStatusesByInstance } from '@/api/status'
const status = { const status = {
state: { state: {
fetchedStatus: {},
fetchedStatuses: [], fetchedStatuses: [],
loading: false, loading: false,
statusAuthor: {},
statusesByInstance: { statusesByInstance: {
selectedInstance: '', selectedInstance: '',
showLocal: false, showLocal: false,
@ -28,6 +30,9 @@ const status = {
CHANGE_SELECTED_INSTANCE: (state, instance) => { CHANGE_SELECTED_INSTANCE: (state, instance) => {
state.statusesByInstance.selectedInstance = instance state.statusesByInstance.selectedInstance = instance
}, },
SET_STATUS: (state, status) => {
state.fetchedStatus = status
},
SET_STATUSES_BY_INSTANCE: (state, statuses) => { SET_STATUSES_BY_INSTANCE: (state, statuses) => {
state.fetchedStatuses = statuses state.fetchedStatuses = statuses
}, },
@ -45,6 +50,9 @@ const status = {
}, },
SET_STATUS_VISIBILITY: (state, visibility) => { SET_STATUS_VISIBILITY: (state, visibility) => {
state.statusVisibility = visibility state.statusVisibility = visibility
},
SET_STATUS_AUTHOR: (state, user) => {
state.statusAuthor = user
} }
}, },
actions: { actions: {
@ -56,6 +64,8 @@ const status = {
dispatch('FetchUserStatuses', { userId, godmode }) dispatch('FetchUserStatuses', { userId, godmode })
} else if (fetchStatusesByInstance) { // called from Statuses by Instance } else if (fetchStatusesByInstance) { // called from Statuses by Instance
dispatch('FetchStatusesByInstance') dispatch('FetchStatusesByInstance')
} else { // called from Status show page
dispatch('FetchStatusAfterUserModeration', statusId)
} }
}, },
ClearState({ commit }) { ClearState({ commit }) {
@ -76,6 +86,21 @@ const status = {
dispatch('FetchStatusesByInstance') dispatch('FetchStatusesByInstance')
} }
}, },
async FetchStatus({ commit, dispatch, getters, state }, id) {
commit('SET_LOADING', true)
const status = await fetchStatus(id, getters.authHost, getters.token)
commit('SET_STATUS', status.data)
commit('SET_STATUS_AUTHOR', status.data.account)
commit('SET_LOADING', false)
dispatch('FetchUserStatuses', { userId: state.fetchedStatus.account.id, godmode: false })
},
FetchStatusAfterUserModeration({ commit, dispatch, getters, state }, id) {
commit('SET_LOADING', true)
fetchStatus(id, getters.authHost, getters.token)
.then(status => dispatch('SetStatus', status.data))
commit('SET_LOADING', false)
},
async FetchStatusesCount({ commit, getters }, instance) { async FetchStatusesCount({ commit, getters }, instance) {
commit('SET_LOADING', true) commit('SET_LOADING', true)
const { data } = await fetchStatusesCount(instance, getters.authHost, getters.token) const { data } = await fetchStatusesCount(instance, getters.authHost, getters.token)
@ -159,6 +184,10 @@ const status = {
}, },
HandlePageChange({ commit }, page) { HandlePageChange({ commit }, page) {
commit('CHANGE_PAGE', page) commit('CHANGE_PAGE', page)
},
SetStatus({ commit }, status) {
commit('SET_STATUS', status)
commit('SET_STATUS_AUTHOR', status.account)
} }
} }
} }

View file

@ -35,18 +35,21 @@ const userProfile = {
dispatch('FetchUserStatuses', { userId, godmode }) dispatch('FetchUserStatuses', { userId, godmode })
}, },
async FetchUserStatuses({ commit, getters }, { userId, godmode }) { FetchUserStatuses({ commit, dispatch, getters }, { userId, godmode }) {
commit('SET_STATUSES_LOADING', true) commit('SET_STATUSES_LOADING', true)
const statuses = await fetchUserStatuses(userId, getters.authHost, godmode, getters.token) fetchUserStatuses(userId, getters.authHost, godmode, getters.token)
.then(statuses => dispatch('SetStatuses', statuses.data))
commit('SET_STATUSES', statuses.data)
commit('SET_STATUSES_LOADING', false) commit('SET_STATUSES_LOADING', false)
}, },
async FetchUserCredentials({ commit, getters }, { nickname }) { async FetchUserCredentials({ commit, getters }, { nickname }) {
const userResponse = await fetchUserCredentials(nickname, getters.authHost, getters.token) const userResponse = await fetchUserCredentials(nickname, getters.authHost, getters.token)
commit('SET_USER_CREDENTIALS', userResponse.data) commit('SET_USER_CREDENTIALS', userResponse.data)
}, },
SetStatuses({ commit }, statuses) {
commit('SET_STATUSES', statuses)
},
async UpdateUserCredentials({ dispatch, getters }, { nickname, credentials }) { async UpdateUserCredentials({ dispatch, getters }, { nickname, credentials }) {
await updateUserCredentials(nickname, credentials, getters.authHost, getters.token) await updateUserCredentials(nickname, credentials, getters.authHost, getters.token)
dispatch('FetchUserCredentials', { nickname }) dispatch('FetchUserCredentials', { nickname })

View file

@ -24,6 +24,7 @@ const users = {
searchQuery: '', searchQuery: '',
totalUsersCount: 0, totalUsersCount: 0,
currentPage: 1, currentPage: 1,
pageSize: 50,
filters: { filters: {
local: false, local: false,
external: false, external: false,
@ -75,9 +76,6 @@ const users = {
}, },
SET_USERS_FILTERS: (state, filters) => { SET_USERS_FILTERS: (state, filters) => {
state.filters = filters state.filters = filters
},
SET_USER_PROFILE: (state, user) => {
state.userProfile = user
} }
}, },
actions: { actions: {
@ -90,7 +88,7 @@ const users = {
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
}, },
async ApplyChanges({ commit, dispatch, state }, { updatedUsers, callApiFn, userId }) { async ApplyChanges({ commit, dispatch, state }, { updatedUsers, callApiFn, userId, statusId }) {
commit('SWAP_USERS', updatedUsers) commit('SWAP_USERS', updatedUsers)
try { try {
@ -100,29 +98,30 @@ const users = {
} finally { } finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage }) dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
} }
if (statusId) {
if (userId) { dispatch('FetchStatusAfterUserModeration', statusId)
} else if (userId) {
dispatch('FetchUserProfile', { userId, godmode: false }) dispatch('FetchUserProfile', { userId, godmode: false })
} }
dispatch('SuccessMessage') dispatch('SuccessMessage')
}, },
async AddRight({ dispatch, getters }, { users, right, _userId }) { async AddRight({ dispatch, getters }, { users, right, _userId, _statusId }) {
const updatedUsers = users.map(user => { const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: true }} : user return user.local ? { ...user, roles: { ...user.roles, [right]: true }} : user
}) })
const nicknames = users.map(user => user.nickname) const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await addRight(nicknames, right, getters.authHost, getters.token) const callApiFn = async() => await addRight(nicknames, right, getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId, statusId: _statusId })
}, },
async AddTag({ dispatch, getters }, { users, tag, _userId }) { async AddTag({ dispatch, getters }, { users, tag, _userId, _statusId }) {
const updatedUsers = users.map(user => { const updatedUsers = users.map(user => {
return { ...user, tags: [...user.tags, tag] } return { ...user, tags: [...user.tags, tag] }
}) })
const nicknames = users.map(user => user.nickname) const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await tagUser(nicknames, [tag], getters.authHost, getters.token) const callApiFn = async() => await tagUser(nicknames, [tag], getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId, statusId: _statusId })
}, },
ClearUsersState({ commit }) { ClearUsersState({ commit }) {
commit('SET_SEARCH_QUERY', '') commit('SET_SEARCH_QUERY', '')
@ -151,14 +150,14 @@ const users = {
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
}, },
async ConfirmUsersEmail({ dispatch, getters }, { users, _userId }) { async ConfirmUsersEmail({ dispatch, getters }, { users, _userId, _statusId }) {
const updatedUsers = users.map(user => { const updatedUsers = users.map(user => {
return { ...user, confirmation_pending: false } return { ...user, confirmation_pending: false }
}) })
const nicknames = users.map(user => user.nickname) const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await confirmUserEmail(nicknames, getters.authHost, getters.token) const callApiFn = async() => await confirmUserEmail(nicknames, getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId, statusId: _statusId })
}, },
async ResendConfirmationEmail({ dispatch, getters }, users) { async ResendConfirmationEmail({ dispatch, getters }, users) {
const usersNicknames = users.map(user => user.nickname) const usersNicknames = users.map(user => user.nickname)
@ -169,14 +168,14 @@ const users = {
} }
dispatch('SuccessMessage') dispatch('SuccessMessage')
}, },
async DeleteRight({ dispatch, getters }, { users, right, _userId }) { async DeleteRight({ dispatch, getters }, { users, right, _userId, _statusId }) {
const updatedUsers = users.map(user => { const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: false }} : user return user.local ? { ...user, roles: { ...user.roles, [right]: false }} : user
}) })
const nicknames = users.map(user => user.nickname) const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await deleteRight(nicknames, right, getters.authHost, getters.token) const callApiFn = async() => await deleteRight(nicknames, right, getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId, statusId: _statusId })
}, },
async DeleteUsers({ commit, dispatch, getters, state }, { users, _userId }) { async DeleteUsers({ commit, dispatch, getters, state }, { users, _userId }) {
const usersNicknames = users.map(user => user.nickname) const usersNicknames = users.map(user => user.nickname)
@ -206,14 +205,14 @@ const users = {
RemovePasswordToken({ commit }) { RemovePasswordToken({ commit }) {
commit('SET_PASSWORD_RESET_TOKEN', { link: '', token: '' }) commit('SET_PASSWORD_RESET_TOKEN', { link: '', token: '' })
}, },
async RemoveTag({ dispatch, getters }, { users, tag, _userId }) { async RemoveTag({ dispatch, getters }, { users, tag, _userId, _statusId }) {
const updatedUsers = users.map(user => { const updatedUsers = users.map(user => {
return { ...user, tags: user.tags.filter(userTag => userTag !== tag) } return { ...user, tags: user.tags.filter(userTag => userTag !== tag) }
}) })
const nicknames = users.map(user => user.nickname) const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await untagUser(nicknames, [tag], getters.authHost, getters.token) const callApiFn = async() => await untagUser(nicknames, [tag], getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId }) dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId, statusId: _statusId })
}, },
async RequirePasswordReset({ dispatch, getters }, users) { async RequirePasswordReset({ dispatch, getters }, users) {
const nicknames = users.map(user => user.nickname) const nicknames = users.map(user => user.nickname)

273
src/views/statuses/show.vue Normal file
View file

@ -0,0 +1,273 @@
<template>
<div v-if="!loading" class="status-show-container">
<header v-if="isDesktop || isTablet" class="user-page-header">
<div class="avatar-name-container">
<router-link :to="{ name: 'UsersShow', params: { id: user.id }}">
<div class="avatar-name-header">
<el-avatar v-if="accountExists(user, 'avatar')" :src="user.avatar" size="large" />
<h1 v-if="accountExists(user, 'display_name')">{{ user.display_name }}</h1>
</div>
</router-link>
<a v-if="accountExists(user, 'url')" :href="user.url" target="_blank" class="account">
<i class="el-icon-top-right" title="Open user in instance"/>
</a>
</div>
<div class="left-header-container">
<moderation-dropdown
:user="user"
:page="'statusPage'"
:status-id="status.id"
@open-reset-token-dialog="openResetPasswordDialog"/>
<reboot-button/>
</div>
</header>
<div v-if="isMobile" class="status-page-header-container">
<header class="user-page-header">
<div class="avatar-name-container">
<el-avatar v-if="accountExists(user, 'avatar')" :src="user.avatar" size="large" />
<h1 v-if="accountExists(user, 'display_name')">{{ user.display_name }}</h1>
</div>
<reboot-button/>
</header>
<moderation-dropdown
:user="user"
:page="'userPage'"
@open-reset-token-dialog="openResetPasswordDialog"/>
</div>
<reset-password-dialog
:reset-password-dialog-open="resetPasswordDialogOpen"
@close-reset-token-dialog="closeResetPasswordDialog"/>
<div class="status-container">
<status :status="status" :account="user" :show-checkbox="false" :godmode="showPrivate"/>
</div>
<div class="recent-statuses-container-show">
<h2 class="recent-statuses">{{ $t('userProfile.recentStatuses') }} by {{ user.display_name }}</h2>
<el-checkbox v-model="showPrivate" class="show-private-statuses" @change="onTogglePrivate">
{{ $t('statuses.showPrivateStatuses') }}
</el-checkbox>
<el-timeline v-if="!statusesLoading" class="statuses">
<el-timeline-item v-for="status in statuses" :key="status.id">
<status :status="status" :account="status.account" :show-checkbox="false" :user-id="user.id" :godmode="showPrivate"/>
</el-timeline-item>
<p v-if="statuses.length === 0" class="no-statuses">{{ $t('userProfile.noStatuses') }}</p>
</el-timeline>
</div>
</div>
</template>
<script>
import Status from '@/components/Status'
import ModerationDropdown from '../users/components/ModerationDropdown'
import RebootButton from '@/components/RebootButton'
import ResetPasswordDialog from '@/views/users/components/ResetPasswordDialog'
export default {
name: 'StatusShow',
components: { ModerationDropdown, RebootButton, ResetPasswordDialog, Status },
data() {
return {
showPrivate: false,
resetPasswordDialogOpen: false
}
},
computed: {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
isTablet() {
return this.$store.state.app.device === 'tablet'
},
loading() {
return this.$store.state.status.loading
},
status() {
return this.$store.state.status.fetchedStatus
},
statuses() {
return this.$store.state.userProfile.statuses
},
statusesLoading() {
return this.$store.state.userProfile.statusesLoading
},
user() {
return this.$store.state.status.statusAuthor
}
},
beforeMount: function() {
this.$store.dispatch('NeedReboot')
this.$store.dispatch('GetNodeInfo')
this.$store.dispatch('FetchStatus', this.$route.params.id)
},
methods: {
accountExists(account, key) {
return account[key]
},
closeResetPasswordDialog() {
this.resetPasswordDialogOpen = false
this.$store.dispatch('RemovePasswordToken')
},
onTogglePrivate() {
this.$store.dispatch('FetchUserStatuses', { userId: this.user.id, godmode: this.showPrivate })
},
openResetPasswordDialog() {
this.resetPasswordDialogOpen = true
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.avatar-name-container {
display: flex;
align-items: center;
.el-icon-top-right {
font-size: 2em;
line-height: 36px;
color: #606266;
}
}
.avatar-name-header {
display: flex;
height: 40px;
align-items: center;
}
.no-statuses {
margin-left: 28px;
color: #606266;
}
.password-reset-token {
margin: 0 0 14px 0;
}
.password-reset-token-dialog {
width: 50%
}
.reboot-button {
padding: 10px;
margin-left: 6px;
}
.recent-statuses-container-show {
display: flex;
flex-direction: column;
.el-timeline-item {
margin-left: 20px;
}
.recent-statuses {
margin-left: 20px;
}
.show-private-statuses {
margin-left: 20px;
margin-bottom: 20px;
}
}
.reset-password-link {
text-decoration: underline;
}
.status-container {
margin: 0 15px 0 20px;
}
.statuses {
padding: 0 20px 0 0;
}
.user-page-header {
display: flex;
justify-content: space-between;
margin: 22px 15px 22px 20px;
align-items: center;
h1 {
display: inline;
margin: 0 0 0 10px;
}
}
@media only screen and (min-width: 1824px) {
.status-show-container {
max-width: 1824px;
margin: auto;
}
}
@media only screen and (max-width:480px) {
.avatar-name-container {
margin-bottom: 10px;
}
.el-timeline-item__wrapper {
padding-left: 18px;
}
.left-header-container {
align-items: center;
display: flex;
justify-content: space-between;
}
.password-reset-token-dialog {
width: 85%
}
.recent-statuses {
margin: 20px 10px 15px 10px;
}
.recent-statuses-container-show {
width: 100%;
margin: 0 0 0 10px;
.el-timeline-item {
margin-left: 0;
}
.recent-statuses {
margin-left: 0;
}
.show-private-statuses {
margin: 0 10px 20px 0;
}
}
.status-card {
.el-card__body {
padding: 15px;
}
}
.status-container {
margin: 0 10px;
}
.statuses {
padding-right: 10px;
margin-left: 0;
.el-timeline-item__wrapper {
margin-right: 10px;
}
}
.user-page-header {
padding: 0;
margin: 7px 15px 5px 10px;
}
.status-page-header-container {
width: 100%;
.el-dropdown {
width: stretch;
margin: 0 10px 15px 10px;
}
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.recent-statuses-container-show {
width: 97%;
margin: 0 20px;
.el-timeline-item {
margin-left: 2px;
}
.recent-statuses {
margin: 20px 10px 15px 0;
}
.show-private-statuses {
margin: 0 10px 20px 0;
}
}
.show-private-statuses {
margin: 0 10px 20px 0;
}
.user-page-header {
padding: 0;
margin: 7px 15px 20px 20px;
}
}
</style>

View file

@ -1,11 +1,11 @@
<template> <template>
<el-dropdown :hide-on-click="false" size="small" trigger="click" @click.native.stop> <el-dropdown :hide-on-click="false" size="small" trigger="click" placement="top-start" @click.native.stop>
<div> <div>
<el-button v-if="page === 'users'" type="text" class="el-dropdown-link"> <el-button v-if="page === 'users'" type="text" class="el-dropdown-link">
{{ $t('users.moderation') }} {{ $t('users.moderation') }}
<i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/> <i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/>
</el-button> </el-button>
<el-button v-if="page === 'userPage'" class="moderate-user-button"> <el-button v-if="page === 'userPage' || page === 'statusPage'" class="moderate-user-button">
<span class="moderate-user-button-container"> <span class="moderate-user-button-container">
<span> <span>
<i class="el-icon-edit" /> <i class="el-icon-edit" />
@ -27,13 +27,13 @@
{{ user.roles.moderator ? $t('users.revokeModerator') : $t('users.grantModerator') }} {{ user.roles.moderator ? $t('users.revokeModerator') : $t('users.grantModerator') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="showDeactivatedButton(user.id)" v-if="showDeactivatedButton(user.id) && page !== 'statusPage'"
:divided="showAdminAction(user)" :divided="showAdminAction(user)"
@click.native="toggleActivation(user)"> @click.native="toggleActivation(user)">
{{ user.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }} {{ user.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="showDeactivatedButton(user.id)" v-if="showDeactivatedButton(user.id) && page !== 'statusPage'"
@click.native="handleDeletion(user)"> @click.native="handleDeletion(user)">
{{ $t('users.deleteAccount') }} {{ $t('users.deleteAccount') }}
</el-dropdown-item> </el-dropdown-item>
@ -115,6 +115,10 @@ export default {
page: { page: {
type: String, type: String,
default: 'users' default: 'users'
},
statusId: {
type: String,
default: ''
} }
}, },
computed: { computed: {
@ -134,7 +138,7 @@ export default {
this.$store.dispatch('DeleteUsers', { users: [user], _userId: user.id }) this.$store.dispatch('DeleteUsers', { users: [user], _userId: user.id })
}, },
handleEmailConfirmation(user) { handleEmailConfirmation(user) {
this.$store.dispatch('ConfirmUsersEmail', { users: [user], _userId: user.id }) this.$store.dispatch('ConfirmUsersEmail', { users: [user], _userId: user.id, _statusId: this.statusId })
}, },
requirePasswordReset(user) { requirePasswordReset(user) {
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
@ -157,13 +161,13 @@ export default {
}, },
toggleTag(user, tag) { toggleTag(user, tag) {
user.tags.includes(tag) user.tags.includes(tag)
? this.$store.dispatch('RemoveTag', { users: [user], tag, _userId: user.id }) ? this.$store.dispatch('RemoveTag', { users: [user], tag, _userId: user.id, _statusId: this.statusId })
: this.$store.dispatch('AddTag', { users: [user], tag, _userId: user.id }) : this.$store.dispatch('AddTag', { users: [user], tag, _userId: user.id, _statusId: this.statusId })
}, },
toggleUserRight(user, right) { toggleUserRight(user, right) {
user.roles[right] user.roles[right]
? this.$store.dispatch('DeleteRight', { users: [user], right, _userId: user.id }) ? this.$store.dispatch('DeleteRight', { users: [user], right, _userId: user.id, _statusId: this.statusId })
: this.$store.dispatch('AddRight', { users: [user], right, _userId: user.id }) : this.$store.dispatch('AddRight', { users: [user], right, _userId: user.id, _statusId: this.statusId })
} }
} }
} }

View file

@ -0,0 +1,47 @@
<template>
<el-dialog
v-loading="loading"
:visible="dialogOpen"
:title="$t('users.passwordResetTokenCreated')"
custom-class="password-reset-token-dialog"
@close="closeResetPasswordDialog">
<div>
<p class="password-reset-token">Password reset token was generated: {{ passwordResetToken }}</p>
<p>You can also use this link to reset password:
<a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a>
</p>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ResetPasswordDialog',
props: {
resetPasswordDialogOpen: {
type: Boolean,
default: false
}
},
computed: {
dialogOpen() {
return this.resetPasswordDialogOpen
},
loading() {
return this.$store.state.users.loading
},
passwordResetLink() {
return this.$store.state.users.passwordResetToken.link
},
passwordResetToken() {
return this.$store.state.users.passwordResetToken.token
}
},
methods: {
closeResetPasswordDialog() {
this.$emit('close-reset-token-dialog')
}
}
}
</script>

View file

@ -6,7 +6,7 @@
multiple multiple
class="select-field" class="select-field"
@change="toggleFilters"> @change="toggleFilters">
<el-option-group :label="$t('usersFilter.byUserType')"> <el-option-group :label="$t('usersFilter.byAccountType')">
<el-option value="local">{{ $t('usersFilter.local') }}</el-option> <el-option value="local">{{ $t('usersFilter.local') }}</el-option>
<el-option value="external">{{ $t('usersFilter.external') }}</el-option> <el-option value="external">{{ $t('usersFilter.external') }}</el-option>
</el-option-group> </el-option-group>

View file

@ -87,19 +87,9 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-dialog <reset-password-dialog
v-loading="loading" :reset-password-dialog-open="resetPasswordDialogOpen"
:visible.sync="resetPasswordDialogOpen" @close-reset-token-dialog="closeResetPasswordDialog"/>
:title="$t('users.passwordResetTokenCreated')"
custom-class="password-reset-token-dialog"
@close="closeResetPasswordDialog">
<div>
<p class="password-reset-token">Password reset token was generated: {{ passwordResetToken }}</p>
<p>You can also use this link to reset password:
<a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a>
</p>
</div>
</el-dialog>
<div v-if="!loading" class="pagination"> <div v-if="!loading" class="pagination">
<el-pagination <el-pagination
:total="usersCount" :total="usersCount"
@ -121,6 +111,7 @@ import MultipleUsersMenu from './components/MultipleUsersMenu'
import NewAccountDialog from './components/NewAccountDialog' import NewAccountDialog from './components/NewAccountDialog'
import ModerationDropdown from './components/ModerationDropdown' import ModerationDropdown from './components/ModerationDropdown'
import RebootButton from '@/components/RebootButton' import RebootButton from '@/components/RebootButton'
import ResetPasswordDialog from './components/ResetPasswordDialog'
export default { export default {
name: 'Users', name: 'Users',
@ -129,6 +120,7 @@ export default {
ModerationDropdown, ModerationDropdown,
MultipleUsersMenu, MultipleUsersMenu,
RebootButton, RebootButton,
ResetPasswordDialog,
UsersFilter UsersFilter
}, },
data() { data() {
@ -149,12 +141,6 @@ export default {
pageSize() { pageSize() {
return this.$store.state.users.pageSize return this.$store.state.users.pageSize
}, },
passwordResetLink() {
return this.$store.state.users.passwordResetToken.link
},
passwordResetToken() {
return this.$store.state.users.passwordResetToken.token
},
currentPage() { currentPage() {
return this.$store.state.users.currentPage return this.$store.state.users.currentPage
}, },
@ -265,11 +251,6 @@ export default {
.create-account > .el-icon-plus { .create-account > .el-icon-plus {
margin-right: 5px; margin-right: 5px;
} }
.users-header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.password-reset-token { .password-reset-token {
margin: 0 0 14px 0; margin: 0 0 14px 0;
} }
@ -279,6 +260,11 @@ export default {
.reset-password-link { .reset-password-link {
text-decoration: underline; text-decoration: underline;
} }
.users-header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.users-container { .users-container {
h1 { h1 {
margin: 10px 0 0 15px; margin: 10px 0 0 15px;

View file

@ -30,19 +30,9 @@
:page="'userPage'" :page="'userPage'"
@open-reset-token-dialog="openResetPasswordDialog"/> @open-reset-token-dialog="openResetPasswordDialog"/>
</div> </div>
<el-dialog <reset-password-dialog
v-loading="loading" :reset-password-dialog-open="resetPasswordDialogOpen"
:visible.sync="resetPasswordDialogOpen" @close-reset-token-dialog="closeResetPasswordDialog"/>
:title="$t('users.passwordResetTokenCreated')"
custom-class="password-reset-token-dialog"
@close="closeResetPasswordDialog">
<div>
<p class="password-reset-token">Password reset token was generated: {{ passwordResetToken }}</p>
<p>You can also use this link to reset password:
<a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a>
</p>
</div>
</el-dialog>
<div class="user-profile-container"> <div class="user-profile-container">
<el-card class="user-profile-card"> <el-card class="user-profile-card">
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium"> <div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium">
@ -77,14 +67,14 @@
</td> </td>
</tr> </tr>
<tr class="el-table__row"> <tr class="el-table__row">
<td>{{ $t('userProfile.localUppercase') }}</td> <td>{{ $t('userProfile.accountType') }}</td>
<td> <td>
<el-tag v-if="user.local" type="info">{{ $t('userProfile.local') }}</el-tag> <el-tag v-if="user.local" type="info">{{ $t('userProfile.local') }}</el-tag>
<el-tag v-if="!user.local" type="info">{{ $t('userProfile.external') }}</el-tag> <el-tag v-if="!user.local" type="info">{{ $t('userProfile.external') }}</el-tag>
</td> </td>
</tr> </tr>
<tr class="el-table__row"> <tr class="el-table__row">
<td>{{ $t('userProfile.activeUppercase') }}</td> <td>{{ $t('userProfile.status') }}</td>
<td> <td>
<el-tag v-if="!user.deactivated" type="success">{{ $t('userProfile.active') }}</el-tag> <el-tag v-if="!user.deactivated" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="user.deactivated" type="danger">{{ $t('userProfile.deactivated') }}</el-tag> <el-tag v-if="user.deactivated" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
@ -123,10 +113,11 @@ import Status from '@/components/Status'
import ModerationDropdown from './components/ModerationDropdown' import ModerationDropdown from './components/ModerationDropdown'
import SecuritySettingsModal from './components/SecuritySettingsModal' import SecuritySettingsModal from './components/SecuritySettingsModal'
import RebootButton from '@/components/RebootButton' import RebootButton from '@/components/RebootButton'
import ResetPasswordDialog from './components/ResetPasswordDialog'
export default { export default {
name: 'UsersShow', name: 'UsersShow',
components: { ModerationDropdown, RebootButton, Status, SecuritySettingsModal }, components: { ModerationDropdown, RebootButton, ResetPasswordDialog, Status, SecuritySettingsModal },
data() { data() {
return { return {
showPrivate: false, showPrivate: false,
@ -147,12 +138,6 @@ export default {
loading() { loading() {
return this.$store.state.users.loading return this.$store.state.users.loading
}, },
passwordResetLink() {
return this.$store.state.users.passwordResetToken.link
},
passwordResetToken() {
return this.$store.state.users.passwordResetToken.token
},
statuses() { statuses() {
return this.$store.state.userProfile.statuses return this.$store.state.userProfile.statuses
}, },
@ -179,6 +164,17 @@ export default {
this.resetPasswordDialogOpen = false this.resetPasswordDialogOpen = false
this.$store.dispatch('RemovePasswordToken') this.$store.dispatch('RemovePasswordToken')
}, },
humanizeTag(tag) {
const mapTags = {
'force_nsfw': 'Force NSFW',
'strip_media': 'Strip Media',
'force_unlisted': 'Force Unlisted',
'sandbox': 'Sandbox',
'disable_remote_subscription': 'Disable remote subscription',
'disable_any_subscription': 'Disable any subscription'
}
return mapTags[tag]
},
onTogglePrivate() { onTogglePrivate() {
this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: this.showPrivate }) this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: this.showPrivate })
}, },
@ -192,7 +188,7 @@ export default {
} }
</script> </script>
<style rel='stylesheet/scss' lang='scss' scoped> <style rel='stylesheet/scss' lang='scss'>
header { header {
align-items: center; align-items: center;
display: flex; display: flex;
@ -241,6 +237,12 @@ table {
margin-left: 28px; margin-left: 28px;
color: #606266; color: #606266;
} }
.password-reset-token {
margin: 0 0 14px 0;
}
.password-reset-token-dialog {
width: 50%
}
.poll ul { .poll ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
@ -258,6 +260,9 @@ table {
.recent-statuses-header { .recent-statuses-header {
margin-top: 10px; margin-top: 10px;
} }
.reset-password-link {
text-decoration: underline;
}
.security-setting-button { .security-setting-button {
margin-top: 20px; margin-top: 20px;
width: 100%; width: 100%;
@ -307,16 +312,29 @@ table {
.avatar-name-container { .avatar-name-container {
margin-bottom: 10px; margin-bottom: 10px;
} }
.el-timeline-item__wrapper {
padding-left: 18px;
}
.password-reset-token-dialog {
width: 85%
}
.recent-statuses { .recent-statuses {
margin: 20px 10px 15px 10px; margin: 20px 10px 15px 10px;
} }
.recent-statuses-container { .recent-statuses-container {
width: 100%; width: 100%;
margin: 0 10px; margin: 0;
} }
.show-private-statuses { .show-private-statuses {
margin: 0 10px 20px 10px; margin: 0 10px 20px 10px;
} }
.status-container {
margin: 0 10px;
}
.statuses {
padding-right: 10px;
margin-left: 8px;
}
.user-page-header { .user-page-header {
padding: 0; padding: 0;
margin: 7px 15px 15px 10px; margin: 7px 15px 15px 10px;

View file

@ -0,0 +1,91 @@
import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Element from 'element-ui'
import StatusShow from '@/views/statuses/show'
import storeConfig from './statusShowStore.conf'
import { cloneDeep } from 'lodash'
config.mocks["$t"] = () => {}
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Element)
const $route = {
params: {
id: '9vJOO3iFPyjNaEhJ5s'
}
}
jest.mock('@/api/app')
jest.mock('@/api/status')
jest.mock('@/api/peers')
jest.mock('@/api/nodeInfo')
jest.mock('@/api/users')
describe('Status show page', () => {
let store
beforeEach(() => {
store = new Vuex.Store(cloneDeep(storeConfig))
})
it(`fetches single status and user's statuses`, async (done) => {
const wrapper = mount(StatusShow, {
store,
localVue,
sync: false,
stubs: ['router-link'],
mocks: {
$route
}
})
await flushPromises()
expect(wrapper.find('.status-container').isVisible()).toBe(true)
expect(store.state.status.fetchedStatus.id).toBe('9vJOO3iFPyjNaEhJ5s')
expect(store.state.status.fetchedStatus.account.display_name).toBe('dolin')
expect(store.state.userProfile.statuses.length).toEqual(3)
done()
})
it(`renders links and user's moderation menu`, async (done) => {
const wrapper = mount(StatusShow, {
store,
localVue,
sync: false,
stubs: ['router-link'],
mocks: {
$route
}
})
await flushPromises()
expect(wrapper.find('router-link-stub h1').text()).toBe('dolin')
expect(wrapper.find('button.moderate-user-button').exists()).toBe(true)
expect(wrapper.find('.el-dropdown-menu').exists()).toBe(true)
done()
})
it(`renders status card`, async (done) => {
const wrapper = mount(StatusShow, {
store,
localVue,
sync: false,
stubs: ['router-link'],
mocks: {
$route
}
})
await flushPromises()
expect(wrapper.find('.status-card').exists()).toBe(true)
expect(wrapper.find('router-link-stub h3').text()).toBe('dolin')
expect(wrapper.find('span.el-tag').text()).not.toBe('Sensitive')
expect(wrapper.find('span.el-tag').text()).toBe('Public')
expect(wrapper.find('button.status-actions-button').exists()).toBe(true)
expect(wrapper.find('.status-body .status-content').text()).toBe('pizza makes everything better')
done()
})
})

View file

@ -0,0 +1,21 @@
import app from '@/store/modules/app'
import peers from '@/store/modules/peers'
import user from '@/store/modules/user'
import userProfile from '@/store/modules/userProfile'
import users from '@/store/modules/users'
import settings from '@/store/modules/settings'
import status from '@/store/modules/status'
import getters from '@/store/getters'
export default {
modules: {
app,
peers,
settings,
status,
user,
userProfile,
users
},
getters
}

View file

@ -21,7 +21,7 @@ const $route = {
jest.mock('@/api/nodeInfo') jest.mock('@/api/nodeInfo')
jest.mock('@/api/users') jest.mock('@/api/users')
describe('Search and filter users', () => { describe('User profile', () => {
let store let store
beforeEach(() => { beforeEach(() => {