Merge branch 'feature/update-multiple-users-actions' into 'master'

User actions moderate multiple users at a time

See merge request pleroma/admin-fe!55
This commit is contained in:
Angelina Filippova 2019-10-20 16:29:07 +00:00
commit 81a9e69ec7
10 changed files with 201 additions and 162 deletions

View file

@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- `mailerEnabled` must be set to `true` in order to require password reset (password reset currently only works via email)
- Actions in users module (ActivateUsers, AddRight, DeactivateUsers, DeleteRight, DeleteUsers) now accept an array of users instead of one user
### Added
- Optimistic update for actions in users module and fetching users after api function finished its execution
## [1.2.0] - 2019-09-27

View file

@ -33,11 +33,6 @@ export async function getPasswordResetToken(nickname, authHost, token) {
return Promise.resolve({ data: { token: 'g05lxnBJQnL', link: 'http://url/api/pleroma/password_reset/g05lxnBJQnL' }})
}
export async function toggleUserActivation(nickname, authHost, token) {
const response = users.find(user => user.nickname === nickname)
return Promise.resolve({ data: { ...response, deactivated: !response.deactivated }})
}
export async function searchUsers(query, filters, authHost, token, page = 1) {
const filteredUsers = filterUsers(filters)
const response = filteredUsers.filter(user => user.nickname === query)
@ -48,21 +43,37 @@ export async function searchUsers(query, filters, authHost, token, page = 1) {
}})
}
export async function addRight(nickname, right, authHost, token) {
export async function activateUsers(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, deactivated: false }
})
return Promise.resolve({ data: response })
}
export async function addRight(nicknames, right, authHost, token) {
return Promise.resolve({ data:
{ [`is_${right}`]: true }
})
}
export async function deactivateUsers(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, deactivated: true }
})
return Promise.resolve({ data: response })
}
export async function deleteRight(nickname, right, authHost, token) {
return Promise.resolve({ data:
{ [`is_${right}`]: false }
})
}
export async function deleteUser(nickname, authHost, token) {
export async function deleteUsers(nicknames, authHost, token) {
return Promise.resolve({ data:
nickname
nicknames
})
}

View file

@ -2,12 +2,23 @@ import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function addRight(nickname, right, authHost, token) {
export async function activateUsers(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/permission_group/${right}`,
url: `/api/pleroma/admin/users/activate`,
method: 'patch',
headers: authHeaders(token),
data: { nicknames }
})
}
export async function addRight(nicknames, right, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/permission_group/${right}`,
method: 'post',
headers: authHeaders(token)
headers: authHeaders(token),
data: { nicknames }
})
}
@ -21,21 +32,33 @@ export async function createNewAccount(nickname, email, password, authHost, toke
})
}
export async function deleteRight(nickname, right, authHost, token) {
export async function deactivateUsers(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/permission_group/${right}`,
method: 'delete',
headers: authHeaders(token)
url: `/api/pleroma/admin/users/deactivate`,
method: 'patch',
headers: authHeaders(token),
data: { nicknames }
})
}
export async function deleteUser(nickname, authHost, token) {
export async function deleteRight(nicknames, right, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users?nickname=${nickname}`,
url: `/api/pleroma/admin/users/permission_group/${right}`,
method: 'delete',
headers: authHeaders(token)
headers: authHeaders(token),
data: { nicknames }
})
}
export async function deleteUsers(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users`,
method: 'delete',
headers: authHeaders(token),
data: { nicknames }
})
}
@ -94,15 +117,6 @@ export async function tagUser(nicknames, tags, authHost, token) {
})
}
export async function toggleUserActivation(nickname, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/toggle_activation`,
method: 'patch',
headers: authHeaders(token)
})
}
export async function untagUser(nicknames, tags, authHost, token) {
return await request({
baseURL: baseName(authHost),

View file

@ -1,13 +1,14 @@
import {
activateUsers,
addRight,
createNewAccount,
deactivateUsers,
deleteRight,
deleteUser,
deleteUsers,
fetchUsers,
getPasswordResetToken,
searchUsers,
tagUser,
toggleUserActivation,
untagUser,
requirePasswordReset
} from '@/api/users'
@ -37,12 +38,6 @@ const users = {
SET_LOADING: (state, status) => {
state.loading = status
},
SWAP_USER: (state, updatedUser) => {
const updated = state.fetchedUsers.map(user => user.id === updatedUser.id ? updatedUser : user)
state.fetchedUsers = updated
.map(user => user.nickname ? user : { ...user, nickname: '' })
.sort((a, b) => a.nickname.localeCompare(b.nickname))
},
SWAP_USERS: (state, users) => {
const usersWithoutSwapped = users.reduce((acc, user) => {
return acc.filter(u => u.id !== user.id)
@ -76,11 +71,35 @@ const users = {
}
},
actions: {
async AddTag({ commit, getters }, { users, tag }) {
async ActivateUsers({ commit, dispatch, getters, state }, users) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: false }
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
await activateUsers(usersNicknames, getters.authHost, getters.token)
dispatch('FetchUsers', { page: state.currentPage })
},
async AddRight({ commit, dispatch, getters, state }, { users, right }) {
const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: true }} : user
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
await addRight(usersNicknames, right, getters.authHost, getters.token)
dispatch('FetchUsers', { page: state.currentPage })
},
async AddTag({ commit, dispatch, getters, state }, { users, tag }) {
const updatedUsers = users.map(user => {
return { ...user, tags: [...user.tags, tag] }
})
commit('SWAP_USERS', updatedUsers)
const nicknames = users.map(user => user.nickname)
await tagUser(nicknames, [tag], getters.authHost, getters.token)
commit('SWAP_USERS', users.map(user => ({ ...user, tags: [...user.tags, tag] })))
dispatch('FetchUsers', { page: state.currentPage })
},
async ClearFilters({ commit, dispatch, state }) {
commit('CLEAR_USERS_FILTERS')
@ -90,33 +109,60 @@ const users = {
await createNewAccount(nickname, email, password, getters.authHost, getters.token)
dispatch('FetchUsers', { page: state.currentPage })
},
async DeleteUser({ commit, getters, state }, user) {
const { data } = await deleteUser(user.nickname, getters.authHost, getters.token)
const users = state.fetchedUsers.filter(user => user.nickname !== data)
commit('SET_USERS', users)
async DeactivateUsers({ commit, dispatch, getters, state }, users) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: true }
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
await deactivateUsers(usersNicknames, getters.authHost, getters.token)
dispatch('FetchUsers', { page: state.currentPage })
},
async RequirePasswordReset({ commit, getters, state }, user) {
async DeleteRight({ commit, dispatch, getters, state }, { users, right }) {
const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: false }} : user
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
await deleteRight(usersNicknames, right, getters.authHost, getters.token)
dispatch('FetchUsers', { page: state.currentPage })
},
async DeleteUsers({ commit, getters, state }, users) {
const deletedUsersIds = users.map(deletedUser => deletedUser.id)
const updatedUsers = state.fetchedUsers.filter(user => !deletedUsersIds.includes(user.id))
commit('SET_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
await deleteUsers(usersNicknames, getters.authHost, getters.token)
},
async RequirePasswordReset({ getters }, user) {
await requirePasswordReset(user.nickname, getters.authHost, getters.token)
},
async FetchUsers({ commit, state, getters, dispatch }, { page }) {
async FetchUsers({ commit, dispatch, getters, state }, { page }) {
commit('SET_LOADING', true)
const filters = Object.keys(state.filters).filter(filter => state.filters[filter]).join()
const response = await fetchUsers(filters, getters.authHost, getters.token, page)
await dispatch('GetNodeInfo')
loadUsers(commit, page, response.data)
},
async GetPasswordResetToken({ commit, state, getters }, nickname) {
async GetPasswordResetToken({ commit, getters }, nickname) {
const { data } = await getPasswordResetToken(nickname, getters.authHost, getters.token)
commit('SET_PASSWORD_RESET_TOKEN', data)
},
RemovePasswordToken({ commit }) {
commit('SET_PASSWORD_RESET_TOKEN', { link: '', token: '' })
},
async RemoveTag({ commit, getters }, { users, tag }) {
async RemoveTag({ commit, dispatch, getters, state }, { users, tag }) {
const updatedUsers = users.map(user => {
return { ...user, tags: user.tags.filter(userTag => userTag !== tag) }
})
commit('SWAP_USERS', updatedUsers)
const nicknames = users.map(user => user.nickname)
await untagUser(nicknames, [tag], getters.authHost, getters.token)
commit('SWAP_USERS', users.map(user => ({ ...user, tags: user.tags.filter(userTag => userTag !== tag) })))
dispatch('FetchUsers', { page: state.currentPage })
},
async SearchUsers({ commit, dispatch, state, getters }, { query, page }) {
if (query.length === 0) {
@ -132,10 +178,6 @@ const users = {
loadUsers(commit, page, response.data)
}
},
async ToggleUserActivation({ commit, getters }, nickname) {
const { data } = await toggleUserActivation(nickname, getters.authHost, getters.token)
commit('SWAP_USER', data)
},
async ToggleUsersFilter({ commit, dispatch, state }, filters) {
const defaultFilters = {
local: false,
@ -146,14 +188,6 @@ const users = {
const currentFilters = { ...defaultFilters, ...filters }
commit('SET_USERS_FILTERS', currentFilters)
dispatch('SearchUsers', { query: state.searchQuery, page: 1 })
},
async ToggleRight({ commit, getters }, { user, right }) {
user.roles[right]
? await deleteRight(user.nickname, right, getters.authHost, getters.token)
: await addRight(user.nickname, right, getters.authHost, getters.token)
const updatedUser = { ...user, roles: { ...user.roles, [right]: !user.roles[right] }}
commit('SWAP_USER', updatedUser)
}
}
}

View file

@ -10,14 +10,19 @@ const service = axios.create({
service.interceptors.response.use(
response => response,
error => {
let errorMessage
console.log(`Error ${error}`)
console.log(error.response.data)
// If there's an "error" property in the json, use it
const edata = error.response.data.error ? error.response.data.error : error.response.data
if (error.response) {
// If there's an "error" property in the json, use it
const edata = error.response.data.error ? error.response.data.error : error.response.data
errorMessage = `${error.message} - ${edata}`
} else {
errorMessage = error
}
Message({
message: `${error.message} - ${edata}`,
message: errorMessage,
type: 'error',
duration: 5 * 1000
})

View file

@ -21,7 +21,7 @@
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="showDeactivatedButton(report.account)"
@click.native="handleDeactivation(report.account)">
@click.native="toggleActivation(report.account)">
{{ report.account.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
@ -139,17 +139,19 @@ export default {
return 'primary'
}
},
handleDeletion(user) {
this.$store.dispatch('DeleteUsers', [user])
},
parseTimestamp(timestamp) {
return moment(timestamp).format('L HH:mm')
},
showDeactivatedButton(id) {
return this.$store.state.user.id !== id
},
handleDeactivation({ nickname }) {
this.$store.dispatch('ToggleUserActivation', nickname)
},
handleDeletion(user) {
this.$store.dispatch('DeleteUser', user)
toggleActivation(user) {
user.deactivated
? this.$store.dispatch('ActivateUsers', [user])
: this.$store.dispatch('DeactivateUsers', [user])
},
toggleTag(user, tag) {
user.tags.includes(tag)

View file

@ -150,87 +150,69 @@ export default {
},
methods: {
mappers() {
const applyActionToAllUsers = (filteredUsers, fn) => Promise.all(filteredUsers.map(fn))
.then(() => {
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
}).catch((err) => {
const applyAction = async(users, dispatchAction) => {
try {
await dispatchAction(users)
} catch (err) {
console.log(err)
return
}
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
}
return {
grantRight: (right) => () => {
const filterUsersFn = user => user.local && !user.roles[right] && this.$store.state.user.id !== user.id
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
const addRightFn = async(users) => await this.$store.dispatch('AddRight', { users, right })
const filtered = this.selectedUsers.filter(filterUsersFn)
applyActionToAllUsers(filtered, toggleRightFn)
applyAction(filtered, addRightFn)
},
revokeRight: (right) => () => {
const filterUsersFn = user => user.local && user.roles[right] && this.$store.state.user.id !== user.id
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
const deleteRightFn = async(users) => await this.$store.dispatch('DeleteRight', { users, right })
const filtered = this.selectedUsers.filter(filterUsersFn)
applyActionToAllUsers(filtered, toggleRightFn)
applyAction(filtered, deleteRightFn)
},
activate: () => {
const filtered = this.selectedUsers.filter(user => user.deactivated && this.$store.state.user.id !== user.id)
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', users)
applyActionToAllUsers(filtered, toggleActivationFn)
applyAction(filtered, activateUsersFn)
},
deactivate: () => {
const filtered = this.selectedUsers.filter(user => !user.deactivated && this.$store.state.user.id !== user.id)
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', users)
applyActionToAllUsers(filtered, toggleActivationFn)
applyAction(filtered, deactivateUsersFn)
},
remove: () => {
const filtered = this.selectedUsers.filter(user => this.$store.state.user.id !== user.id)
const deleteAccountFn = async(user) => await this.$store.dispatch('DeleteUser', user)
const deleteAccountFn = async(users) => await this.$store.dispatch('DeleteUsers', users)
applyActionToAllUsers(filtered, deleteAccountFn)
applyAction(filtered, deleteAccountFn)
},
addTag: (tag) => async() => {
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && !user.tags.includes(tag)
: !user.tags.includes(tag)
const users = this.selectedUsers.filter(filterUsersFn)
addTag: (tag) => () => {
const filtered = this.selectedUsers.filter(user =>
tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && !user.tags.includes(tag)
: !user.tags.includes(tag))
const addTagFn = async(users) => await this.$store.dispatch('AddTag', { users, tag })
try {
await this.$store.dispatch('AddTag', { users, tag })
} catch (err) {
console.log(err)
return
}
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
applyAction(filtered, addTagFn)
},
removeTag: (tag) => async() => {
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && user.tags.includes(tag)
: user.tags.includes(tag)
const users = this.selectedUsers.filter(filterUsersFn)
const filtered = this.selectedUsers.filter(user =>
tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && user.tags.includes(tag)
: user.tags.includes(tag))
const removeTagFn = async(users) => await this.$store.dispatch('RemoveTag', { users, tag })
try {
await this.$store.dispatch('RemoveTag', { users, tag })
} catch (err) {
console.log(err)
return
}
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
applyAction(filtered, removeTagFn)
},
requirePasswordReset: () => {
this.selectedUsers.map(user => this.$store.dispatch('RequirePasswordReset', user))

View file

@ -80,7 +80,7 @@
<el-dropdown-item
v-if="showDeactivatedButton(scope.row.id)"
:divided="showAdminAction(scope.row)"
@click.native="handleDeactivation(scope.row)">
@click.native="toggleActivation(scope.row)">
{{ scope.row.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
@ -275,11 +275,13 @@ export default {
this.$store.dispatch('RequirePasswordReset', { nickname })
},
handleDeactivation({ nickname }) {
this.$store.dispatch('ToggleUserActivation', nickname)
toggleActivation(user) {
user.deactivated
? this.$store.dispatch('ActivateUsers', [user])
: this.$store.dispatch('DeactivateUsers', [user])
},
handleDeletion(user) {
this.$store.dispatch('DeleteUser', user)
this.$store.dispatch('DeleteUsers', [user])
},
handlePageChange(page) {
const searchQuery = this.$store.state.users.searchQuery
@ -308,7 +310,9 @@ export default {
: this.$store.dispatch('AddTag', { users: [user], tag })
},
toggleUserRight(user, right) {
this.$store.dispatch('ToggleRight', { user, right })
user.roles[right]
? this.$store.dispatch('DeleteRight', { users: [user], right })
: this.$store.dispatch('AddRight', { users: [user], right })
}
}
}

View file

@ -75,7 +75,7 @@ describe('Users actions', () => {
store = new Vuex.Store(cloneDeep(storeConfig))
})
it('grants admin and moderator rights to a local user', async (done) => {
it('grants admin right to a local user', async (done) => {
const wrapper = mount(Users, {
store,
localVue,
@ -87,14 +87,28 @@ describe('Users actions', () => {
const user = store.state.users.fetchedUsers[2]
expect(user.roles.admin).toBe(false)
expect(user.roles.moderator).toBe(false)
wrapper.find(htmlElement(3, 1)).trigger('click')
await flushPromises()
wrapper.find(htmlElement(3, 2)).trigger('click')
await flushPromises()
const updatedUser = store.state.users.fetchedUsers[2]
expect(updatedUser.roles.admin).toBe(true)
done()
})
it('grants moderator right to a local user', async (done) => {
const wrapper = mount(Users, {
store,
localVue,
sync: false,
stubs: ['router-link']
})
await flushPromises()
const user = store.state.users.fetchedUsers[2]
expect(user.roles.admin).toBe(false)
expect(user.roles.moderator).toBe(false)
wrapper.find(htmlElement(3, 2)).trigger('click')
const updatedUser = store.state.users.fetchedUsers[2]
expect(updatedUser.roles.moderator).toBe(true)
done()
})
@ -126,9 +140,7 @@ describe('Users actions', () => {
const user = store.state.users.fetchedUsers[1]
expect(user.deactivated).toBe(false)
wrapper.find(htmlElement(2, 1)).trigger('click')
await flushPromises()
const updatedUser = store.state.users.fetchedUsers[1]
expect(updatedUser.deactivated).toBe(true)
@ -166,9 +178,7 @@ describe('Users actions', () => {
expect(user2.tags.length).toBe(1)
wrapper.find(htmlElement(1, 5)).trigger('click')
await flushPromises()
wrapper.find(htmlElement(2, 5)).trigger('click')
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[0]
const updatedUser2 = store.state.users.fetchedUsers[1]
@ -188,33 +198,13 @@ describe('Users actions', () => {
const user = store.state.users.fetchedUsers[1]
expect(user.tags.length).toBe(1)
wrapper.find(htmlElement(2, 6)).trigger('click')
await flushPromises()
const updatedUser = store.state.users.fetchedUsers[1]
expect(updatedUser.tags.length).toBe(0)
done()
})
it('shows check icon when tag is added', async (done) => {
const wrapper = mount(Users, {
store,
localVue,
sync: false,
stubs: ['router-link']
})
await flushPromises()
expect(wrapper.find(`${htmlElement(1, 5)} i`).exists()).toBe(false)
wrapper.find(htmlElement(1, 5)).trigger('click')
await flushPromises()
expect(wrapper.find(`${htmlElement(1, 5)} i`).exists()).toBe(true)
done()
})
it('does not change user index in array when tag is added', async (done) => {
const wrapper = mount(Users, {
store,

View file

@ -51,7 +51,6 @@ describe('Apply users actions to multiple users', () => {
expect(user2.roles.admin).toBe(false)
expect(user3.roles.admin).toBe(false)
grantRight('admin')()
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[0]
const updatedUser2 = store.state.users.fetchedUsers[1]
@ -88,7 +87,6 @@ describe('Apply users actions to multiple users', () => {
expect(user2.roles.moderator).toBe(false)
expect(user3.roles.moderator).toBe(false)
grantRight('moderator')()
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[0]
const updatedUser2 = store.state.users.fetchedUsers[1]
@ -123,7 +121,6 @@ describe('Apply users actions to multiple users', () => {
expect(user1.roles.admin).toBe(true)
expect(user2.roles.admin).toBe(false)
revokeRight('admin')()
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[0]
const updatedUser2 = store.state.users.fetchedUsers[2]
@ -173,7 +170,6 @@ describe('Apply users actions to multiple users', () => {
const user = store.state.users.fetchedUsers[2]
expect(user.deactivated).toBe(true)
activate()
await flushPromises()
const updatedUser = store.state.users.fetchedUsers[2]
expect(updatedUser.deactivated).toBe(false)
@ -203,7 +199,6 @@ describe('Apply users actions to multiple users', () => {
expect(user1.deactivated).toBe(false)
expect(user2.deactivated).toBe(false)
deactivate()
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[0]
const updatedUser2 = store.state.users.fetchedUsers[1]
@ -270,7 +265,6 @@ describe('Apply users actions to multiple users', () => {
expect(user1.tags.length).toBe(0)
expect(user2.tags.length).toBe(1)
addTag('strip_media')()
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[0]
const updatedUser2 = store.state.users.fetchedUsers[1]
@ -311,7 +305,6 @@ describe('Apply users actions to multiple users', () => {
expect(user1.tags.length).toBe(1)
expect(user2.tags.length).toBe(1)
removeTag('strip_media')()
await flushPromises()
const updatedUser1 = store.state.users.fetchedUsers[1]
const updatedUser2 = store.state.users.fetchedUsers[2]