Merge branch 'fix/statuses-by-instance' into 'develop'

Improvements for statuses by instance

Closes #76

See merge request pleroma/admin-fe!88
This commit is contained in:
Angelina Filippova 2020-02-04 22:15:42 +00:00
commit d4be7d2643
10 changed files with 175 additions and 71 deletions

View file

@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Actions in users module (ActivateUsers, AddRight, DeactivateUsers, DeleteRight, DeleteUsers) now accept an array of users instead of one user
- Leave dropdown menu open after clicking an action
- Move current try/catch error handling from view files to module, add it where necessary
- Display checkboxes in status card and fetch statuses only when status card was rendered from Statuses by instance page
- Move statuses by instance state from local state to store state
- Pass user's ID to actions that moderate users when action is called from user's profile page
### Added
@ -32,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Show checkmarks when tag is applied
- Reports update (also, now it's optimistic)
- Remove duplicated success message
- Fix styles for Statuses by instance page
## [1.2.0] - 2019-09-27

View file

@ -21,7 +21,7 @@ export async function deleteStatus(id, authHost, token) {
})
}
export async function fetchStatusesByInstance(instance, authHost, token, pageSize, page = 1) {
export async function fetchStatusesByInstance({ instance, authHost, token, pageSize, page }) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/instances/${instance}/statuses?page=${page}&page_size=${pageSize}`,

View file

@ -5,10 +5,9 @@
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<el-checkbox @change="handleStatusSelection(status.account)">
<img :src="status.account.avatar" class="status-avatar-img">
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
</el-checkbox>
<el-checkbox v-if="showCheckbox" class="status-checkbox" @change="handleStatusSelection(status.account)"/>
<img :src="status.account.avatar" class="status-avatar-img">
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
</div>
<a :href="status.account.url" target="_blank" class="account">
@{{ status.account.acct }}
@ -122,6 +121,16 @@ import moment from 'moment'
export default {
name: 'Status',
props: {
fetchStatusesByInstance: {
type: Boolean,
required: false,
default: false
},
showCheckbox: {
type: Boolean,
required: true,
default: false
},
status: {
type: Object,
required: true
@ -152,7 +161,15 @@ export default {
return str.charAt(0).toUpperCase() + str.slice(1)
},
changeStatus(statusId, isSensitive, visibility) {
this.$store.dispatch('ChangeStatusScope', { statusId, isSensitive, visibility, reportCurrentPage: this.page, userId: this.userId, godmode: this.godmode })
this.$store.dispatch('ChangeStatusScope', {
statusId,
isSensitive,
visibility,
reportCurrentPage: this.page,
userId: this.userId,
godmode: this.godmode,
fetchStatusesByInstance: this.fetchStatusesByInstance
})
},
deleteStatus(statusId) {
this.$confirm('Are you sure you want to delete this status?', 'Warning', {
@ -160,7 +177,13 @@ export default {
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeleteStatus', { statusId, reportCurrentPage: this.page, userId: this.userId, godmode: this.godmode })
this.$store.dispatch('DeleteStatus', {
statusId,
reportCurrentPage: this.page,
userId: this.userId,
godmode: this.godmode,
fetchStatusesByInstance: this.fetchStatusesByInstance
})
this.$message({
type: 'success',
message: 'Delete completed'
@ -224,6 +247,9 @@ export default {
display: flex;
flex-direction: column;
}
.status-checkbox {
margin-right: 7px;
}
.status-content {
font-size: 15px;
line-height: 26px;

View file

@ -3,10 +3,21 @@ import { changeStatusScope, deleteStatus, fetchStatusesByInstance } from '@/api/
const status = {
state: {
fetchedStatuses: [],
loading: false
loading: false,
statusesByInstance: {
selectedInstance: '',
page: 1,
pageSize: 30
}
},
mutations: {
SET_STATUSES: (state, statuses) => {
CHANGE_PAGE: (state, page) => {
state.statusesByInstance.page = page
},
CHANGE_SELECTED_INSTANCE: (state, instance) => {
state.statusesByInstance.selectedInstance = instance
},
SET_STATUSES_BY_INSTANCE: (state, statuses) => {
state.fetchedStatuses = statuses
},
PUSH_STATUSES: (state, statuses) => {
@ -17,39 +28,65 @@ const status = {
}
},
actions: {
async ChangeStatusScope({ dispatch, getters }, { statusId, isSensitive, visibility, reportCurrentPage, userId, godmode }) {
async ChangeStatusScope({ dispatch, getters }, { statusId, isSensitive, visibility, reportCurrentPage, userId, godmode, fetchStatusesByInstance }) {
await changeStatusScope(statusId, isSensitive, visibility, getters.authHost, getters.token)
if (reportCurrentPage !== 0) { // called from Reports
dispatch('FetchReports', reportCurrentPage)
} else if (userId.length > 0) { // called from User profile
dispatch('FetchUserStatuses', { userId, godmode })
} else if (fetchStatusesByInstance) { // called from Statuses by Instance
dispatch('FetchStatusesByInstance')
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async DeleteStatus({ dispatch, getters }, { statusId, reportCurrentPage, userId, godmode }) {
async DeleteStatus({ dispatch, getters }, { statusId, reportCurrentPage, userId, godmode, fetchStatusesByInstance }) {
await deleteStatus(statusId, getters.authHost, getters.token)
if (reportCurrentPage !== 0) { // called from Reports
dispatch('FetchReports', reportCurrentPage)
} else if (userId.length > 0) { // called from User profile
dispatch('FetchUserStatuses', { userId, godmode })
} else if (fetchStatusesByInstance) { // called from Statuses by Instance
dispatch('FetchStatusesByInstance')
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async FetchStatusesByInstance({ commit, getters }, { instance, page, pageSize }) {
async FetchStatusesByInstance({ commit, getters, state }) {
commit('SET_LOADING', true)
const statuses = await fetchStatusesByInstance(instance, getters.authHost, getters.token, pageSize, page)
const statuses = state.statusesByInstance.selectedInstance === ''
? { data: [] }
: await fetchStatusesByInstance(
{
instance: state.statusesByInstance.selectedInstance,
authHost: getters.authHost,
token: getters.token,
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
commit('SET_STATUSES', statuses.data)
commit('SET_STATUSES_BY_INSTANCE', statuses.data)
commit('SET_LOADING', false)
},
async FetchStatusesPageByInstance({ commit, getters }, { instance, page, pageSize }) {
async FetchStatusesPageByInstance({ commit, getters, state }) {
commit('SET_LOADING', true)
const statuses = await fetchStatusesByInstance(instance, getters.authHost, getters.token, pageSize, page)
const statuses = await fetchStatusesByInstance(
{
instance: state.statusesByInstance.selectedInstance,
authHost: getters.authHost,
token: getters.token,
pageSize: state.statusesByInstance.pageSize,
page: state.statusesByInstance.page
})
commit('PUSH_STATUSES', statuses.data)
commit('SET_LOADING', false)
},
HandleFilterChange({ commit }, instance) {
commit('CHANGE_SELECTED_INSTANCE', instance)
},
HandlePageChange({ commit }, page) {
commit('CHANGE_PAGE', page)
}
}
}

View file

@ -79,14 +79,14 @@ const users = {
}
},
actions: {
async ActivateUsers({ dispatch, getters }, users) {
async ActivateUsers({ dispatch, getters }, { users, _userId }) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: false }
})
const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await activateUsers(nicknames, getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async ApplyChanges({ commit, dispatch, state }, { updatedUsers, callApiFn, userId }) {
commit('SWAP_USERS', updatedUsers)
@ -99,26 +99,28 @@ const users = {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('FetchUserProfile', { userId, godmode: false })
if (userId) {
dispatch('FetchUserProfile', { userId, godmode: false })
}
dispatch('SuccessMessage')
},
async AddRight({ dispatch, getters }, { users, right }) {
async AddRight({ dispatch, getters }, { users, right, _userId }) {
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: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async AddTag({ dispatch, getters }, { users, tag }) {
async AddTag({ dispatch, getters }, { users, tag, _userId }) {
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: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async ClearFilters({ commit, dispatch, state }) {
commit('CLEAR_USERS_FILTERS')
@ -134,23 +136,23 @@ const users = {
}
dispatch('SuccessMessage')
},
async DeactivateUsers({ dispatch, getters }, users) {
async DeactivateUsers({ dispatch, getters }, { users, _userId }) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: true }
})
const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await deactivateUsers(nicknames, getters.authHost, getters.token)
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async ConfirmUsersEmail({ dispatch, getters }, users) {
async ConfirmUsersEmail({ dispatch, getters }, { users, _userId }) {
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: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async ResendConfirmationEmail({ dispatch, getters }, users) {
const usersNicknames = users.map(user => user.nickname)
@ -161,16 +163,16 @@ const users = {
}
dispatch('SuccessMessage')
},
async DeleteRight({ dispatch, getters }, { users, right }) {
async DeleteRight({ dispatch, getters }, { users, right, _userId }) {
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: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async DeleteUsers({ commit, dispatch, getters, state }, users) {
async DeleteUsers({ commit, dispatch, getters, state }, { users, _userId }) {
const usersNicknames = users.map(user => user.nickname)
try {
await deleteUsers(usersNicknames, getters.authHost, getters.token)
@ -181,7 +183,7 @@ const users = {
const updatedUsers = state.fetchedUsers.filter(user => !deletedUsersIds.includes(user.id))
commit('SET_USERS', updatedUsers)
dispatch('FetchUserProfile', { userId: users[0].id, godmode: false })
dispatch('FetchUserProfile', { userId: _userId, godmode: false })
dispatch('SuccessMessage')
},
async FetchUsers({ commit, dispatch, getters, state }, { page }) {
@ -198,14 +200,14 @@ const users = {
RemovePasswordToken({ commit }) {
commit('SET_PASSWORD_RESET_TOKEN', { link: '', token: '' })
},
async RemoveTag({ dispatch, getters }, { users, tag }) {
async RemoveTag({ dispatch, getters }, { users, tag, _userId }) {
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: users[0].id })
dispatch('ApplyChanges', { updatedUsers, callApiFn, userId: _userId })
},
async RequirePasswordReset({ dispatch, getters }, users) {
const nicknames = users.map(user => user.nickname)

View file

@ -58,7 +58,7 @@
<el-collapse>
<el-collapse-item :title="getStatusesTitle(report.statuses)">
<div v-for="status in report.statuses" :key="status.id">
<status :status="status" :page="currentPage"/>
<status :status="status" :show-checkbox="false" :page="currentPage"/>
</div>
</el-collapse-item>
</el-collapse>

View file

@ -9,6 +9,8 @@
:placeholder="$t('statuses.instanceFilter')"
:no-data-text="$t('statuses.noInstances')"
filterable
clearable
class="select-instance"
@change="handleFilterChange">
<el-option
v-for="(instance,index) in instances"
@ -21,7 +23,11 @@
@apply-action="clearSelection"/>
</div>
<div v-for="status in statuses" :key="status.id" class="status-container">
<status :status="status" @status-selection="handleStatusSelection" />
<status
:status="status"
:show-checkbox="isDesktop"
:fetch-statuses-by-instance="true"
@status-selection="handleStatusSelection" />
</div>
<div v-if="statuses.length > 0" class="statuses-pagination">
<el-button @click="handleLoadMore">{{ $t('statuses.loadMore') }}</el-button>
@ -42,43 +48,50 @@ export default {
},
data() {
return {
selectedInstance: '',
selectedUsers: [],
page: 1,
pageSize: 30
selectedUsers: []
}
},
computed: {
loadingPeers() {
return this.$store.state.peers.loading
},
...mapGetters([
'instances',
'statuses'
])
},
created() {
]),
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
loadingPeers() {
return this.$store.state.peers.loading
},
page() {
return this.$store.state.status.statusesByInstance.page
},
pageSize() {
return this.$store.state.status.statusesByInstance.pageSize
},
selectedInstance: {
get() {
return this.$store.state.status.statusesByInstance.selectedInstance
},
set(instance) {
this.$store.dispatch('HandleFilterChange', instance)
}
}
},
mounted() {
this.$store.dispatch('FetchPeers')
},
methods: {
handleFilterChange(instance) {
this.page = 1
this.$store.dispatch('FetchStatusesByInstance', { instance, page: this.page, pageSize: this.pageSize })
handleFilterChange() {
this.$store.dispatch('HandlePageChange', 1)
this.$store.dispatch('FetchStatusesByInstance')
},
handleLoadMore() {
this.page = this.page + 1
this.$store.dispatch('HandlePageChange', this.page + 1)
this.$store.dispatch('FetchStatusesPageByInstance', {
instance: this.selectedInstance,
page: this.page,
pageSize: this.pageSize
})
this.$store.dispatch('FetchStatusesPageByInstance')
},
clearSelection() {
// TODO
this.selectedUsers = []
},
handleStatusSelection(user) {
if (this.selectedUsers.find(selectedUser => user.id === selectedUser.id) !== undefined) {
@ -99,7 +112,14 @@ export default {
}
}
.filter-container {
margin: 22px 15px 15px 0;
display: flex;
height: 36px;
justify-content: space-between;
align-items: center;
margin: 22px 0 15px 0;
}
.select-instance {
width: 350px;
}
.statuses-pagination {
padding: 15px 0;
@ -108,4 +128,20 @@ export default {
h1 {
margin: 22px 0 0 0;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
.filter-container {
display: flex;
height: 36px;
flex-direction: column;
margin: 10px 10px
}
.select-field {
width: 100%;
margin-bottom: 5px;
}
}
</style>

View file

@ -131,10 +131,10 @@ export default {
this.$store.dispatch('ResendConfirmationEmail', [user])
},
handleDeletion(user) {
this.$store.dispatch('DeleteUsers', [user])
this.$store.dispatch('DeleteUsers', { users: [user], _userId: user.id })
},
handleEmailConfirmation(user) {
this.$store.dispatch('ConfirmUsersEmail', [user])
this.$store.dispatch('ConfirmUsersEmail', { users: [user], _userId: user.id })
},
requirePasswordReset(user) {
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
@ -152,18 +152,18 @@ export default {
},
toggleActivation(user) {
user.deactivated
? this.$store.dispatch('ActivateUsers', [user])
: this.$store.dispatch('DeactivateUsers', [user])
? this.$store.dispatch('ActivateUsers', { users: [user], _userId: user.id })
: this.$store.dispatch('DeactivateUsers', { users: [user], _userId: user.id })
},
toggleTag(user, tag) {
user.tags.includes(tag)
? this.$store.dispatch('RemoveTag', { users: [user], tag })
: this.$store.dispatch('AddTag', { users: [user], tag })
? this.$store.dispatch('RemoveTag', { users: [user], tag, _userId: user.id })
: this.$store.dispatch('AddTag', { users: [user], tag, _userId: user.id })
},
toggleUserRight(user, right) {
user.roles[right]
? this.$store.dispatch('DeleteRight', { users: [user], right })
: this.$store.dispatch('AddRight', { users: [user], right })
? this.$store.dispatch('DeleteRight', { users: [user], right, _userId: user.id })
: this.$store.dispatch('AddRight', { users: [user], right, _userId: user.id })
}
}
}

View file

@ -180,19 +180,19 @@ export default {
},
activate: () => {
const filtered = this.selectedUsers.filter(user => user.deactivated && this.$store.state.user.id !== user.id)
const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', users)
const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', { users })
applyAction(filtered, activateUsersFn)
},
deactivate: () => {
const filtered = this.selectedUsers.filter(user => !user.deactivated && this.$store.state.user.id !== user.id)
const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', users)
const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', { users })
applyAction(filtered, deactivateUsersFn)
},
remove: () => {
const filtered = this.selectedUsers.filter(user => this.$store.state.user.id !== user.id)
const deleteAccountFn = async(users) => await this.$store.dispatch('DeleteUsers', users)
const deleteAccountFn = async(users) => await this.$store.dispatch('DeleteUsers', { users })
applyAction(filtered, deleteAccountFn)
},
@ -202,7 +202,6 @@ export default {
? user.local && !user.tags.includes(tag)
: !user.tags.includes(tag))
const addTagFn = async(users) => await this.$store.dispatch('AddTag', { users, tag })
applyAction(filtered, addTagFn)
},
removeTag: (tag) => async() => {
@ -222,7 +221,7 @@ export default {
},
confirmAccounts: () => {
const filtered = this.selectedUsers.filter(user => user.local && user.confirmation_pending)
const confirmAccountFn = async(users) => await this.$store.dispatch('ConfirmUsersEmail', users)
const confirmAccountFn = async(users) => await this.$store.dispatch('ConfirmUsersEmail', { users })
applyAction(filtered, confirmAccountFn)
},

View file

@ -92,7 +92,7 @@
<el-col :span="16">
<el-timeline v-if="!statusesLoading" class="statuses">
<el-timeline-item v-for="status in statuses" :key="status.id">
<status :status="status" :user-id="user.id" :godmode="showPrivate"/>
<status :status="status" :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>