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:
commit
12bac96c9d
19 changed files with 757 additions and 191 deletions
|
@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Create `/statuses/:id` route that shows single status
|
||||
|
||||
### Changed
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
||||
|
|
|
@ -6,6 +6,29 @@ export async function deleteStatus(id, authHost, token) {
|
|||
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 }) {
|
||||
let data
|
||||
if (pageSize === 1) {
|
||||
|
|
|
@ -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 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 filters = str.split(',').filter(item => item.length > 0)
|
||||
|
|
|
@ -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 }) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
|
|
|
@ -1,86 +1,74 @@
|
|||
<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">
|
||||
<el-checkbox v-if="showCheckbox" class="status-checkbox" @change="handleStatusSelection(account)"/>
|
||||
<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 class="status-account-name">{{ account.nickname }}</span>
|
||||
</a>
|
||||
<span v-else>
|
||||
<span v-if="propertyExists(account, 'nickname')" class="status-account-name">
|
||||
{{ account.nickname }}
|
||||
<el-card v-if="!status.deleted" class="status-card" @click.native="handleRouteChange()">
|
||||
<div slot="header">
|
||||
<div class="status-header">
|
||||
<div class="status-account-container">
|
||||
<div class="status-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">
|
||||
<span v-if="propertyExists(account, 'nickname')" class="status-account-name">{{ account.nickname }}</span>
|
||||
<span v-else>
|
||||
<span v-if="propertyExists(account, 'nickname')" class="status-account-name">
|
||||
{{ account.nickname }}
|
||||
</span>
|
||||
<span v-else class="status-account-name deactivated">({{ $t('users.invalidNickname') }})</span>
|
||||
</span>
|
||||
<span v-else class="status-account-name deactivated">({{ $t('users.invalidNickname') }})</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
</div>
|
||||
<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 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>
|
||||
<el-dropdown trigger="click" @click.native.stop>
|
||||
<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 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">
|
||||
</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>
|
||||
|
@ -94,30 +82,52 @@
|
|||
<img :src="attachment.preview_url">
|
||||
</div>
|
||||
</div>
|
||||
<a :href="status.url" target="_blank" class="account">
|
||||
{{ parseTimestamp(status.created_at) }}
|
||||
</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>
|
||||
<div class="status-footer">
|
||||
<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>
|
||||
</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>
|
||||
</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 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) }}
|
||||
</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>
|
||||
<div class="status-footer">
|
||||
<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>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -208,6 +218,9 @@ export default {
|
|||
handleStatusSelection(account) {
|
||||
this.$emit('status-selection', account)
|
||||
},
|
||||
handleRouteChange() {
|
||||
this.$router.push({ name: 'StatusShow', params: { id: this.status.id }})
|
||||
},
|
||||
optionPercent(poll, pollOption) {
|
||||
const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0)
|
||||
if (allVotes === 0) {
|
||||
|
@ -231,10 +244,14 @@ export default {
|
|||
<style rel='stylesheet/scss' lang='scss'>
|
||||
.status-card {
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
.account {
|
||||
text-decoration: underline;
|
||||
line-height: 26px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.account:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.deactivated {
|
||||
color: gray;
|
||||
|
@ -271,6 +288,10 @@ export default {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.status-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.status-checkbox {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
@ -278,13 +299,26 @@ export default {
|
|||
font-size: 15px;
|
||||
line-height: 26px;
|
||||
}
|
||||
.status-created-at {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.status-deleted {
|
||||
font-style: italic;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.status-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.status-tags {
|
||||
display: inline;
|
||||
}
|
||||
.status-without-content {
|
||||
font-style: italic;
|
||||
|
@ -303,7 +337,7 @@ export default {
|
|||
padding: 10px 17px;
|
||||
}
|
||||
.el-tag {
|
||||
margin: 3px 4px 3px 0;
|
||||
margin: 3px 0;
|
||||
}
|
||||
.status-account-container {
|
||||
margin-bottom: 5px;
|
||||
|
@ -312,12 +346,20 @@ export default {
|
|||
margin: 3px 0 3px;
|
||||
}
|
||||
.status-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.status-footer {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,16 +171,16 @@ export default {
|
|||
id: 'ID',
|
||||
name: 'Name',
|
||||
status: 'Status',
|
||||
local: 'local',
|
||||
external: 'external',
|
||||
deactivated: 'deactivated',
|
||||
active: 'active',
|
||||
unconfirmed: 'unconfirmed',
|
||||
local: 'Local',
|
||||
external: 'External',
|
||||
deactivated: 'Deactivated',
|
||||
active: 'Active',
|
||||
unconfirmed: 'Unconfirmed',
|
||||
actions: 'Actions',
|
||||
activate: 'Activate',
|
||||
deactivate: 'Deactivate',
|
||||
admin: 'admin',
|
||||
moderator: 'moderator',
|
||||
admin: 'Admin',
|
||||
moderator: 'Moderator',
|
||||
moderation: 'Moderation',
|
||||
revokeAdmin: 'Revoke Admin',
|
||||
grantAdmin: 'Grant Admin',
|
||||
|
@ -205,8 +205,8 @@ export default {
|
|||
moderateUser: 'Moderate user',
|
||||
moderateUsers: 'Moderate multiple users',
|
||||
createAccount: 'Create new account',
|
||||
apply: 'apply',
|
||||
remove: 'remove',
|
||||
apply: 'Apply',
|
||||
remove: 'Remove',
|
||||
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?',
|
||||
activateMultipleUsersConfirmation: 'Are you sure you want to activate accounts of all selected users?',
|
||||
|
@ -258,15 +258,15 @@ export default {
|
|||
tags: 'Tags',
|
||||
moderator: 'Moderator',
|
||||
admin: 'Admin',
|
||||
local: 'local',
|
||||
external: 'external',
|
||||
localUppercase: 'Local',
|
||||
local: 'Local',
|
||||
external: 'External',
|
||||
accountType: 'Account type',
|
||||
nickname: 'Nickname',
|
||||
recentStatuses: 'Recent Statuses',
|
||||
roles: 'Roles',
|
||||
activeUppercase: 'Active',
|
||||
active: 'active',
|
||||
deactivated: 'deactivated',
|
||||
active: 'Active',
|
||||
status: 'Status',
|
||||
deactivated: 'Deactivated',
|
||||
noStatuses: 'No statuses to show',
|
||||
securitySettings: {
|
||||
email: 'Email',
|
||||
|
@ -286,7 +286,7 @@ export default {
|
|||
},
|
||||
usersFilter: {
|
||||
inputPlaceholder: 'Select filter',
|
||||
byUserType: 'By user type',
|
||||
byAccountType: 'By account type',
|
||||
local: 'Local',
|
||||
external: 'External',
|
||||
byStatus: 'By status',
|
||||
|
|
|
@ -172,5 +172,17 @@ export const asyncRouterMap = [
|
|||
],
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/statuses/:id',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'StatusShow',
|
||||
component: () => import('@/views/statuses/show')
|
||||
}
|
||||
],
|
||||
hidden: true
|
||||
},
|
||||
{ path: '*', redirect: '/404', hidden: true }
|
||||
]
|
||||
|
|
|
@ -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 = {
|
||||
state: {
|
||||
fetchedStatus: {},
|
||||
fetchedStatuses: [],
|
||||
loading: false,
|
||||
statusAuthor: {},
|
||||
statusesByInstance: {
|
||||
selectedInstance: '',
|
||||
showLocal: false,
|
||||
|
@ -28,6 +30,9 @@ const status = {
|
|||
CHANGE_SELECTED_INSTANCE: (state, instance) => {
|
||||
state.statusesByInstance.selectedInstance = instance
|
||||
},
|
||||
SET_STATUS: (state, status) => {
|
||||
state.fetchedStatus = status
|
||||
},
|
||||
SET_STATUSES_BY_INSTANCE: (state, statuses) => {
|
||||
state.fetchedStatuses = statuses
|
||||
},
|
||||
|
@ -45,6 +50,9 @@ const status = {
|
|||
},
|
||||
SET_STATUS_VISIBILITY: (state, visibility) => {
|
||||
state.statusVisibility = visibility
|
||||
},
|
||||
SET_STATUS_AUTHOR: (state, user) => {
|
||||
state.statusAuthor = user
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -56,6 +64,8 @@ const status = {
|
|||
dispatch('FetchUserStatuses', { userId, godmode })
|
||||
} else if (fetchStatusesByInstance) { // called from Statuses by Instance
|
||||
dispatch('FetchStatusesByInstance')
|
||||
} else { // called from Status show page
|
||||
dispatch('FetchStatusAfterUserModeration', statusId)
|
||||
}
|
||||
},
|
||||
ClearState({ commit }) {
|
||||
|
@ -76,6 +86,21 @@ const status = {
|
|||
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) {
|
||||
commit('SET_LOADING', true)
|
||||
const { data } = await fetchStatusesCount(instance, getters.authHost, getters.token)
|
||||
|
@ -159,6 +184,10 @@ const status = {
|
|||
},
|
||||
HandlePageChange({ commit }, page) {
|
||||
commit('CHANGE_PAGE', page)
|
||||
},
|
||||
SetStatus({ commit }, status) {
|
||||
commit('SET_STATUS', status)
|
||||
commit('SET_STATUS_AUTHOR', status.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,18 +35,21 @@ const userProfile = {
|
|||
|
||||
dispatch('FetchUserStatuses', { userId, godmode })
|
||||
},
|
||||
async FetchUserStatuses({ commit, getters }, { userId, godmode }) {
|
||||
FetchUserStatuses({ commit, dispatch, getters }, { userId, godmode }) {
|
||||
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)
|
||||
},
|
||||
async FetchUserCredentials({ commit, getters }, { nickname }) {
|
||||
const userResponse = await fetchUserCredentials(nickname, getters.authHost, getters.token)
|
||||
commit('SET_USER_CREDENTIALS', userResponse.data)
|
||||
},
|
||||
SetStatuses({ commit }, statuses) {
|
||||
commit('SET_STATUSES', statuses)
|
||||
},
|
||||
async UpdateUserCredentials({ dispatch, getters }, { nickname, credentials }) {
|
||||
await updateUserCredentials(nickname, credentials, getters.authHost, getters.token)
|
||||
dispatch('FetchUserCredentials', { nickname })
|
||||
|
|
|
@ -24,6 +24,7 @@ const users = {
|
|||
searchQuery: '',
|
||||
totalUsersCount: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
filters: {
|
||||
local: false,
|
||||
external: false,
|
||||
|
@ -75,9 +76,6 @@ const users = {
|
|||
},
|
||||
SET_USERS_FILTERS: (state, filters) => {
|
||||
state.filters = filters
|
||||
},
|
||||
SET_USER_PROFILE: (state, user) => {
|
||||
state.userProfile = user
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -90,7 +88,7 @@ const users = {
|
|||
|
||||
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)
|
||||
|
||||
try {
|
||||
|
@ -100,29 +98,30 @@ const users = {
|
|||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
if (statusId) {
|
||||
dispatch('FetchStatusAfterUserModeration', statusId)
|
||||
} else if (userId) {
|
||||
dispatch('FetchUserProfile', { userId, godmode: false })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async AddRight({ dispatch, getters }, { users, right, _userId }) {
|
||||
async AddRight({ dispatch, getters }, { users, right, _userId, _statusId }) {
|
||||
const updatedUsers = users.map(user => {
|
||||
return user.local ? { ...user, roles: { ...user.roles, [right]: true }} : user
|
||||
})
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
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 => {
|
||||
return { ...user, tags: [...user.tags, tag] }
|
||||
})
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
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 }) {
|
||||
commit('SET_SEARCH_QUERY', '')
|
||||
|
@ -151,14 +150,14 @@ const users = {
|
|||
|
||||
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
|
||||
},
|
||||
async ConfirmUsersEmail({ dispatch, getters }, { users, _userId }) {
|
||||
async ConfirmUsersEmail({ dispatch, getters }, { users, _userId, _statusId }) {
|
||||
const updatedUsers = users.map(user => {
|
||||
return { ...user, confirmation_pending: false }
|
||||
})
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
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) {
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
|
@ -169,14 +168,14 @@ const users = {
|
|||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async DeleteRight({ dispatch, getters }, { users, right, _userId }) {
|
||||
async DeleteRight({ dispatch, getters }, { users, right, _userId, _statusId }) {
|
||||
const updatedUsers = users.map(user => {
|
||||
return user.local ? { ...user, roles: { ...user.roles, [right]: false }} : user
|
||||
})
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
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 }) {
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
|
@ -206,14 +205,14 @@ const users = {
|
|||
RemovePasswordToken({ commit }) {
|
||||
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 => {
|
||||
return { ...user, tags: user.tags.filter(userTag => userTag !== tag) }
|
||||
})
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
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) {
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
|
|
273
src/views/statuses/show.vue
Normal file
273
src/views/statuses/show.vue
Normal 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>
|
|
@ -1,11 +1,11 @@
|
|||
<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>
|
||||
<el-button v-if="page === 'users'" type="text" class="el-dropdown-link">
|
||||
{{ $t('users.moderation') }}
|
||||
<i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/>
|
||||
</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>
|
||||
<i class="el-icon-edit" />
|
||||
|
@ -27,13 +27,13 @@
|
|||
{{ user.roles.moderator ? $t('users.revokeModerator') : $t('users.grantModerator') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="showDeactivatedButton(user.id)"
|
||||
v-if="showDeactivatedButton(user.id) && page !== 'statusPage'"
|
||||
:divided="showAdminAction(user)"
|
||||
@click.native="toggleActivation(user)">
|
||||
{{ user.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="showDeactivatedButton(user.id)"
|
||||
v-if="showDeactivatedButton(user.id) && page !== 'statusPage'"
|
||||
@click.native="handleDeletion(user)">
|
||||
{{ $t('users.deleteAccount') }}
|
||||
</el-dropdown-item>
|
||||
|
@ -115,6 +115,10 @@ export default {
|
|||
page: {
|
||||
type: String,
|
||||
default: 'users'
|
||||
},
|
||||
statusId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -134,7 +138,7 @@ export default {
|
|||
this.$store.dispatch('DeleteUsers', { users: [user], _userId: user.id })
|
||||
},
|
||||
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) {
|
||||
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
|
||||
|
@ -157,13 +161,13 @@ export default {
|
|||
},
|
||||
toggleTag(user, tag) {
|
||||
user.tags.includes(tag)
|
||||
? this.$store.dispatch('RemoveTag', { users: [user], tag, _userId: user.id })
|
||||
: this.$store.dispatch('AddTag', { 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, _statusId: this.statusId })
|
||||
},
|
||||
toggleUserRight(user, right) {
|
||||
user.roles[right]
|
||||
? this.$store.dispatch('DeleteRight', { users: [user], right, _userId: user.id })
|
||||
: this.$store.dispatch('AddRight', { 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, _statusId: this.statusId })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
47
src/views/users/components/ResetPasswordDialog.vue
Normal file
47
src/views/users/components/ResetPasswordDialog.vue
Normal 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>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
multiple
|
||||
class="select-field"
|
||||
@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="external">{{ $t('usersFilter.external') }}</el-option>
|
||||
</el-option-group>
|
||||
|
|
|
@ -87,19 +87,9 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog
|
||||
v-loading="loading"
|
||||
:visible.sync="resetPasswordDialogOpen"
|
||||
: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>
|
||||
<reset-password-dialog
|
||||
:reset-password-dialog-open="resetPasswordDialogOpen"
|
||||
@close-reset-token-dialog="closeResetPasswordDialog"/>
|
||||
<div v-if="!loading" class="pagination">
|
||||
<el-pagination
|
||||
:total="usersCount"
|
||||
|
@ -121,6 +111,7 @@ import MultipleUsersMenu from './components/MultipleUsersMenu'
|
|||
import NewAccountDialog from './components/NewAccountDialog'
|
||||
import ModerationDropdown from './components/ModerationDropdown'
|
||||
import RebootButton from '@/components/RebootButton'
|
||||
import ResetPasswordDialog from './components/ResetPasswordDialog'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
|
@ -129,6 +120,7 @@ export default {
|
|||
ModerationDropdown,
|
||||
MultipleUsersMenu,
|
||||
RebootButton,
|
||||
ResetPasswordDialog,
|
||||
UsersFilter
|
||||
},
|
||||
data() {
|
||||
|
@ -149,12 +141,6 @@ export default {
|
|||
pageSize() {
|
||||
return this.$store.state.users.pageSize
|
||||
},
|
||||
passwordResetLink() {
|
||||
return this.$store.state.users.passwordResetToken.link
|
||||
},
|
||||
passwordResetToken() {
|
||||
return this.$store.state.users.passwordResetToken.token
|
||||
},
|
||||
currentPage() {
|
||||
return this.$store.state.users.currentPage
|
||||
},
|
||||
|
@ -265,11 +251,6 @@ export default {
|
|||
.create-account > .el-icon-plus {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.users-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.password-reset-token {
|
||||
margin: 0 0 14px 0;
|
||||
}
|
||||
|
@ -279,6 +260,11 @@ export default {
|
|||
.reset-password-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.users-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.users-container {
|
||||
h1 {
|
||||
margin: 10px 0 0 15px;
|
||||
|
|
|
@ -30,19 +30,9 @@
|
|||
:page="'userPage'"
|
||||
@open-reset-token-dialog="openResetPasswordDialog"/>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-loading="loading"
|
||||
:visible.sync="resetPasswordDialogOpen"
|
||||
: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>
|
||||
<reset-password-dialog
|
||||
:reset-password-dialog-open="resetPasswordDialogOpen"
|
||||
@close-reset-token-dialog="closeResetPasswordDialog"/>
|
||||
<div class="user-profile-container">
|
||||
<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">
|
||||
|
@ -77,14 +67,14 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr class="el-table__row">
|
||||
<td>{{ $t('userProfile.localUppercase') }}</td>
|
||||
<td>{{ $t('userProfile.accountType') }}</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.external') }}</el-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="el-table__row">
|
||||
<td>{{ $t('userProfile.activeUppercase') }}</td>
|
||||
<td>{{ $t('userProfile.status') }}</td>
|
||||
<td>
|
||||
<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>
|
||||
|
@ -123,10 +113,11 @@ import Status from '@/components/Status'
|
|||
import ModerationDropdown from './components/ModerationDropdown'
|
||||
import SecuritySettingsModal from './components/SecuritySettingsModal'
|
||||
import RebootButton from '@/components/RebootButton'
|
||||
import ResetPasswordDialog from './components/ResetPasswordDialog'
|
||||
|
||||
export default {
|
||||
name: 'UsersShow',
|
||||
components: { ModerationDropdown, RebootButton, Status, SecuritySettingsModal },
|
||||
components: { ModerationDropdown, RebootButton, ResetPasswordDialog, Status, SecuritySettingsModal },
|
||||
data() {
|
||||
return {
|
||||
showPrivate: false,
|
||||
|
@ -147,12 +138,6 @@ export default {
|
|||
loading() {
|
||||
return this.$store.state.users.loading
|
||||
},
|
||||
passwordResetLink() {
|
||||
return this.$store.state.users.passwordResetToken.link
|
||||
},
|
||||
passwordResetToken() {
|
||||
return this.$store.state.users.passwordResetToken.token
|
||||
},
|
||||
statuses() {
|
||||
return this.$store.state.userProfile.statuses
|
||||
},
|
||||
|
@ -179,6 +164,17 @@ export default {
|
|||
this.resetPasswordDialogOpen = false
|
||||
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() {
|
||||
this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: this.showPrivate })
|
||||
},
|
||||
|
@ -192,7 +188,7 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss' scoped>
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -241,6 +237,12 @@ table {
|
|||
margin-left: 28px;
|
||||
color: #606266;
|
||||
}
|
||||
.password-reset-token {
|
||||
margin: 0 0 14px 0;
|
||||
}
|
||||
.password-reset-token-dialog {
|
||||
width: 50%
|
||||
}
|
||||
.poll ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
|
@ -258,6 +260,9 @@ table {
|
|||
.recent-statuses-header {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.reset-password-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.security-setting-button {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
|
@ -307,16 +312,29 @@ table {
|
|||
.avatar-name-container {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.el-timeline-item__wrapper {
|
||||
padding-left: 18px;
|
||||
}
|
||||
.password-reset-token-dialog {
|
||||
width: 85%
|
||||
}
|
||||
.recent-statuses {
|
||||
margin: 20px 10px 15px 10px;
|
||||
}
|
||||
.recent-statuses-container {
|
||||
width: 100%;
|
||||
margin: 0 10px;
|
||||
margin: 0;
|
||||
}
|
||||
.show-private-statuses {
|
||||
margin: 0 10px 20px 10px;
|
||||
}
|
||||
.status-container {
|
||||
margin: 0 10px;
|
||||
}
|
||||
.statuses {
|
||||
padding-right: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.user-page-header {
|
||||
padding: 0;
|
||||
margin: 7px 15px 15px 10px;
|
||||
|
|
91
test/views/statuses/show.test.js
Normal file
91
test/views/statuses/show.test.js
Normal 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()
|
||||
})
|
||||
})
|
21
test/views/statuses/statusShowStore.conf.js
Normal file
21
test/views/statuses/statusShowStore.conf.js
Normal 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
|
||||
}
|
|
@ -21,7 +21,7 @@ const $route = {
|
|||
jest.mock('@/api/nodeInfo')
|
||||
jest.mock('@/api/users')
|
||||
|
||||
describe('Search and filter users', () => {
|
||||
describe('User profile', () => {
|
||||
let store
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
Loading…
Reference in a new issue