diff --git a/src/api/__mocks__/users.js b/src/api/__mocks__/users.js index 2e10334e..c8e6fe94 100644 --- a/src/api/__mocks__/users.js +++ b/src/api/__mocks__/users.js @@ -1,7 +1,7 @@ const users = [ - { deactivated: false, id: '1', nickname: 'john', local: true }, - { deactivated: false, id: '2', nickname: 'bob', local: false }, - { deactivated: true, id: '3', nickname: 'allis', local: true } + { deactivated: false, id: '2', nickname: 'allis', local: true, roles: { admin: true, moderator: false }, tags: [] }, + { deactivated: false, id: '10', nickname: 'bob', local: false, roles: { admin: false, moderator: true }, tags: ['sandbox'] }, + { deactivated: true, id: 'abc', nickname: 'john', local: true, roles: { admin: false, moderator: false }, tags: ['strip_media'] } ] export async function fetchUsers(showLocalUsersOnly, token, page = 1) { @@ -27,3 +27,29 @@ export async function searchUsers(query, showLocalUsersOnly, token, page = 1) { page_size: 50 }}) } + +export async function addRight(nickname, right, token) { + return Promise.resolve({ data: + { [`is_${right}`]: true } + }) +} + +export async function deleteRight(nickname, right, token) { + return Promise.resolve({ data: + { [`is_${right}`]: false } + }) +} + +export async function deleteUser(nickname, token) { + return Promise.resolve({ data: + nickname + }) +} + +export async function tagUser(nickname, tag, token) { + return Promise.resolve() +} + +export async function untagUser(nickname, tag, token) { + return Promise.resolve() +} diff --git a/src/api/users.js b/src/api/users.js index 9c92532e..43ebe5e1 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -5,7 +5,7 @@ export async function fetchUsers(showLocalUsersOnly, token, page = 1) { return await request({ url: `/api/pleroma/admin/users?page=${page}&local_only=${showLocalUsersOnly}`, method: 'get', - headers: token ? { 'Authorization': `Bearer ${getToken()}` } : {} + headers: authHeaders(token) }) } @@ -13,7 +13,7 @@ export async function toggleUserActivation(nickname, token) { return await request({ url: `/api/pleroma/admin/users/${nickname}/toggle_activation`, method: 'patch', - headers: token ? { 'Authorization': `Bearer ${getToken()}` } : {} + headers: authHeaders(token) }) } @@ -21,7 +21,50 @@ export async function searchUsers(query, showLocalUsersOnly, token, page = 1) { return await request({ url: `/api/pleroma/admin/users?query=${query}&page=${page}&local_only=${showLocalUsersOnly}`, method: 'get', - headers: token ? { 'Authorization': `Bearer ${getToken()}` } : {} + headers: authHeaders(token) }) } +export async function addRight(nickname, right, token) { + return await request({ + url: `/api/pleroma/admin/permission_group/${nickname}/${right}`, + method: 'post', + headers: authHeaders(token) + }) +} + +export async function deleteRight(nickname, right, token) { + return await request({ + url: `/api/pleroma/admin/permission_group/${nickname}/${right}`, + method: 'delete', + headers: authHeaders(token) + }) +} + +export async function deleteUser(nickname, token) { + return await request({ + url: `/api/pleroma/admin/user.json?nickname=${nickname}`, + method: 'delete', + headers: authHeaders(token) + }) +} + +export async function tagUser(nickname, tag, token) { + return await request({ + url: '/api/pleroma/admin/users/tag', + method: 'put', + headers: authHeaders(token), + data: { nicknames: [nickname], tags: [tag] } + }) +} + +export async function untagUser(nickname, tag, token) { + return await request({ + url: '/api/pleroma/admin/users/tag', + method: 'delete', + headers: authHeaders(token), + data: { nicknames: [nickname], tags: [tag] } + }) +} + +const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {} diff --git a/src/store/modules/user.js b/src/store/modules/user.js index 58aaea55..1da451b6 100644 --- a/src/store/modules/user.js +++ b/src/store/modules/user.js @@ -87,29 +87,11 @@ const user = { }) }) }, - - // 第三方验证登录 - // LoginByThirdparty({ commit, state }, code) { - // return new Promise((resolve, reject) => { - // commit('SET_CODE', code) - // loginByThirdparty(state.status, state.email, state.code).then(response => { - // commit('SET_TOKEN', response.data.token) - // setToken(response.data.token) - // resolve() - // }).catch(error => { - // reject(error) - // }) - // }) - // }, - - // 登出 LogOut({ commit }) { commit('SET_TOKEN', '') commit('SET_ROLES', []) removeToken() }, - - // 前端 登出 FedLogOut({ commit }) { return new Promise(resolve => { commit('SET_TOKEN', '') @@ -117,24 +99,6 @@ const user = { resolve() }) } - - // 动态修改权限 - // ChangeRoles({ commit, dispatch }, role) { - // return new Promise(resolve => { - // commit('SET_TOKEN', role) - // setToken(role) - // getUserInfo(role).then(response => { - // const data = response.data - // commit('SET_ROLES', data.roles) - // commit('SET_NAME', data.name) - // commit('SET_ID', data.id) - // commit('SET_AVATAR', data.avatar) - // commit('SET_INTRODUCTION', data.introduction) - // dispatch('GenerateRoutes', data) // 动态修改权限后 重绘侧边菜单 - // resolve() - // }) - // }) - // } } } diff --git a/src/store/modules/users.js b/src/store/modules/users.js index 8db05813..e3bf6e63 100644 --- a/src/store/modules/users.js +++ b/src/store/modules/users.js @@ -1,4 +1,4 @@ -import { fetchUsers, toggleUserActivation, searchUsers } from '@/api/users' +import { addRight, fetchUsers, deleteRight, deleteUser, searchUsers, tagUser, toggleUserActivation, untagUser } from '@/api/users' const users = { state: { @@ -22,7 +22,7 @@ const users = { }) state.fetchedUsers = [...usersWithoutSwapped, user].sort((a, b) => - a.id.localeCompare(b.id) + a.nickname.localeCompare(b.nickname) ) }, SET_COUNT: (state, count) => { @@ -70,6 +70,30 @@ const users = { async ToggleLocalUsersFilter({ commit, dispatch, state }, value) { commit('SET_LOCAL_USERS_FILTER', value) dispatch('SearchUsers', { query: state.searchQuery, page: 1 }) + }, + async ToggleRight({ commit, getters }, { user, right }) { + user.roles[right] + ? await deleteRight(user.nickname, right, getters.token) + : await addRight(user.nickname, right, getters.token) + + const updatedUser = { ...user, roles: { ...user.roles, [right]: !user.roles[right] }} + commit('SWAP_USER', updatedUser) + }, + async DeleteUser({ commit, getters }, user) { + await deleteUser(user.nickname, getters.token) + const updatedUser = { ...user, deactivated: true } + commit('SWAP_USER', updatedUser) + }, + async ToggleTag({ commit, getters }, { user, tag }) { + if (user.tags.includes(tag)) { + await untagUser(user.nickname, tag, getters.token) + const updatedUser = { ...user, tags: user.tags.filter(userTag => userTag !== tag) } + commit('SWAP_USER', updatedUser) + } else { + await tagUser(user.nickname, tag, getters.token) + const updatedUser = { ...user, tags: [...user.tags, tag] } + commit('SWAP_USER', updatedUser) + } } } } diff --git a/src/utils/request.js b/src/utils/request.js index 9de05d36..0bf34b09 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -10,41 +10,8 @@ const service = axios.create({ // response interceptor service.interceptors.response.use( response => response, - /** - * 下面的注释为通过在response里,自定义code来标示请求状态 - * 当code返回如下情况则说明权限有问题,登出并返回到登录页 - * 如想通过 xmlhttprequest 来状态码标识 逻辑可写在下面error中 - * 以下代码均为样例,请结合自生需求加以修改,若不需要,则可删除 - */ - // response => { - // const res = response.data - // if (res.code !== 20000) { - // Message({ - // message: res.message, - // type: 'error', - // duration: 5 * 1000 - // }) - // // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; - // if (res.code === 50008 || res.code === 50012 || res.code === 50014) { - // // 请自行在引入 MessageBox - // // import { Message, MessageBox } from 'element-ui' - // MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', { - // confirmButtonText: '重新登录', - // cancelButtonText: '取消', - // type: 'warning' - // }).then(() => { - // store.dispatch('FedLogOut').then(() => { - // location.reload() // 为了重新实例化vue-router对象 避免bug - // }) - // }) - // } - // return Promise.reject('error') - // } else { - // return response.data - // } - // }, error => { - console.log('err' + error) // for debug + console.log('err' + error) Message({ message: error.message, type: 'error', diff --git a/src/views/users/index.vue b/src/views/users/index.vue index 111d29a2..8802cccb 100644 --- a/src/views/users/index.vue +++ b/src/views/users/index.vue @@ -7,24 +7,74 @@ - + + + @@ -72,12 +122,7 @@ export default { return this.$store.state.app.device === 'mobile' }, width() { - return this.isMobile ? 60 : false - }, - rowStyle(id) { - return { - 'data-user-id': id - } + return this.isMobile ? 55 : false } }, created() { @@ -89,7 +134,7 @@ export default { this.$store.dispatch('FetchUsers', { page: 1 }) }, methods: { - handleDeactivate({ nickname }) { + handleDeactivation({ nickname }) { this.$store.dispatch('ToggleUserActivation', nickname) }, handlePageChange(page) { @@ -103,11 +148,23 @@ export default { showDeactivatedButton(id) { return this.$store.state.user.id !== id }, + showAdminAction({ local, id }) { + return local && this.showDeactivatedButton(id) + }, handleLocalUsersCheckbox(e) { this.$store.dispatch('ToggleLocalUsersFilter', e) }, activationIcon(status) { return status ? 'el-icon-error' : 'el-icon-success' + }, + toggleUserRight(user, right) { + this.$store.dispatch('ToggleRight', { user, right }) + }, + handleDeletion(user) { + this.$store.dispatch('DeleteUser', user) + }, + toggleTag(user, tag) { + this.$store.dispatch('ToggleTag', { user, tag }) } } } @@ -144,6 +201,13 @@ only screen and (max-width: 760px), h1 { margin-left: 7px; } + .el-dropdown-link { + cursor: pointer; + color: #409EFF; + } + .el-icon-arrow-down { + font-size: 12px; + } .search { width: 50%; margin-bottom: 21.5px; @@ -156,6 +220,18 @@ only screen and (max-width: 760px), align-items: baseline; margin-left: 7px; } + .el-tag { + width: 30px; + display: inline-block; + margin-bottom: 4px; + font-weight: bold; + &.el-tag--success { + padding-left: 8px; + } + &.el-tag--danger { + padding-left: 8px; + } + } } } diff --git a/test/views/users/index.test.js b/test/views/users/index.test.js index 33c4d1b2..b8f2b019 100644 --- a/test/views/users/index.test.js +++ b/test/views/users/index.test.js @@ -11,7 +11,7 @@ localVue.use(Element) jest.mock('@/api/users') -describe('Users', () => { +describe('Search and filter users', () => { let store beforeEach(() => { @@ -29,24 +29,6 @@ describe('Users', () => { done() }) - it('toggles activation status on button click', async (done) => { - const wrapper = mount(Users, { - store, - localVue - }) - await wrapper.vm.$nextTick() - - const user = store.state.users.fetchedUsers[1] - expect(user.deactivated).toBe(false) - - wrapper.find('.el-table__fixed-body-wrapper table tr:nth-child(2) button').trigger('click') - await wrapper.vm.$nextTick() - - const updatedUser = store.state.users.fetchedUsers[1] - expect(updatedUser.deactivated).toBe(true) - done() - }) - it('starts a search on input change', async (done) => { const wrapper = mount(Users, { store, @@ -95,7 +77,7 @@ describe('Users', () => { done() }) - it('shows local users with query search', async (done) => { + it('shows local users with search query', async (done) => { const wrapper = mount(Users, { store, localVue @@ -134,3 +116,164 @@ describe('Users', () => { done() }) }) + +describe('Users actions', () => { + let store + const htmlElement = (trChild, liChild) => + `.el-table__fixed-body-wrapper table tr:nth-child(${trChild}) ul.el-dropdown-menu li:nth-child(${liChild})` + + beforeEach(() => { + store = new Vuex.Store(cloneDeep(storeConfig)) + }) + + it('grants admin and moderator rights to a local user', async (done) => { + const wrapper = mount(Users, { + store, + localVue + }) + await wrapper.vm.$nextTick() + + 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 wrapper.vm.$nextTick() + wrapper.find(htmlElement(3, 2)).trigger('click') + await wrapper.vm.$nextTick() + + const updatedUser = store.state.users.fetchedUsers[2] + expect(updatedUser.roles.admin).toBe(true) + expect(updatedUser.roles.moderator).toBe(true) + done() + }) + + it('does not show actions that grant admin and moderator rights to external users', async (done) => { + const wrapper = mount(Users, { + store, + localVue + }) + await wrapper.vm.$nextTick() + + const dropdownMenuItem = wrapper.find(htmlElement(2, 1)) + expect(dropdownMenuItem.text()).toBe('Deactivate account') + done() + }) + + it('toggles activation status', async (done) => { + const wrapper = mount(Users, { + store, + localVue + }) + await wrapper.vm.$nextTick() + + const user = store.state.users.fetchedUsers[1] + expect(user.deactivated).toBe(false) + + wrapper.find(htmlElement(2, 1)).trigger('click') + await wrapper.vm.$nextTick() + + const updatedUser = store.state.users.fetchedUsers[1] + expect(updatedUser.deactivated).toBe(true) + done() + }) + + it('deactivates user when Delete action is called', async (done) => { + const wrapper = mount(Users, { + store, + localVue + }) + await wrapper.vm.$nextTick() + + const user = store.state.users.fetchedUsers[1] + expect(user.deactivated).toBe(false) + + wrapper.find(htmlElement(2, 2)).trigger('click') + await wrapper.vm.$nextTick() + + const updatedUser = store.state.users.fetchedUsers[1] + expect(updatedUser.deactivated).toBe(true) + done() + }) + + it('adds tags', async (done) => { + const wrapper = mount(Users, { + store, + localVue + }) + await wrapper.vm.$nextTick() + + const user1 = store.state.users.fetchedUsers[0] + const user2 = store.state.users.fetchedUsers[1] + expect(user1.tags.length).toBe(0) + expect(user2.tags.length).toBe(1) + + wrapper.find(htmlElement(1, 5)).trigger('click') + await wrapper.vm.$nextTick() + wrapper.find(htmlElement(2, 5)).trigger('click') + await wrapper.vm.$nextTick() + + const updatedUser1 = store.state.users.fetchedUsers[0] + const updatedUser2 = store.state.users.fetchedUsers[1] + expect(updatedUser1.tags.length).toBe(1) + expect(updatedUser2.tags.length).toBe(2) + done() + }) + + it('deletes tags', async (done) => { + const wrapper = mount(Users, { + store, + localVue + }) + await wrapper.vm.$nextTick() + + const user = store.state.users.fetchedUsers[1] + expect(user.tags.length).toBe(1) + + wrapper.find(htmlElement(2, 6)).trigger('click') + await wrapper.vm.$nextTick() + + 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 + }) + await wrapper.vm.$nextTick() + + expect(wrapper.find(`${htmlElement(1, 5)} i`).exists()).toBe(false) + + wrapper.find(htmlElement(1, 5)).trigger('click') + await wrapper.vm.$nextTick() + + 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, + localVue + }) + await wrapper.vm.$nextTick() + + const firstUserNickname = store.state.users.fetchedUsers[0].nickname + const secondUserNickname = store.state.users.fetchedUsers[1].nickname + expect(firstUserNickname).toBe('allis') + expect(secondUserNickname).toBe('bob') + + wrapper.find(htmlElement(2, 5)).trigger('click') + await wrapper.vm.$nextTick() + + const firstUserNicknameAfterToggle = store.state.users.fetchedUsers[0].nickname + const secondUserNicknameAfterToggle = store.state.users.fetchedUsers[1].nickname + + expect(firstUserNicknameAfterToggle).toEqual('allis') + expect(secondUserNicknameAfterToggle).toEqual('bob') + done() + }) +})