From 72a13f028c4da32536577f8ccd3acc76c5bc9671 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 26 Jan 2023 14:33:47 +0100 Subject: [PATCH 1/4] Add context menu to users --- .../enhancement-add-context-menu-to-users | 6 + .../src/components/Users/ContextActions.vue | 62 ++++++++++ .../src/components/Users/UsersList.vue | 85 +++++++++++--- .../src/mixins/users/delete.ts | 96 +++++++++++++++ .../src/mixins/users/edit.ts | 27 +++++ .../src/views/Users.vue | 110 +++++------------- 6 files changed, 293 insertions(+), 93 deletions(-) create mode 100644 changelog/unreleased/enhancement-add-context-menu-to-users create mode 100644 packages/web-app-admin-settings/src/components/Users/ContextActions.vue create mode 100644 packages/web-app-admin-settings/src/mixins/users/delete.ts create mode 100644 packages/web-app-admin-settings/src/mixins/users/edit.ts diff --git a/changelog/unreleased/enhancement-add-context-menu-to-users b/changelog/unreleased/enhancement-add-context-menu-to-users new file mode 100644 index 00000000000..469b913f235 --- /dev/null +++ b/changelog/unreleased/enhancement-add-context-menu-to-users @@ -0,0 +1,6 @@ +Enhancement: Add context menu to users + +A context menu has been added to the users management page in the admin settings. It can be toggled via right-click and quick-action. + +https://github.com/owncloud/web/pull/8324 +https://github.com/owncloud/web/issues/8323 diff --git a/packages/web-app-admin-settings/src/components/Users/ContextActions.vue b/packages/web-app-admin-settings/src/components/Users/ContextActions.vue new file mode 100644 index 00000000000..ac5c42e7b78 --- /dev/null +++ b/packages/web-app-admin-settings/src/components/Users/ContextActions.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/web-app-admin-settings/src/components/Users/UsersList.vue b/packages/web-app-admin-settings/src/components/Users/UsersList.vue index 4f8cd4a93ef..2a80090d75a 100644 --- a/packages/web-app-admin-settings/src/components/Users/UsersList.vue +++ b/packages/web-app-admin-settings/src/components/Users/UsersList.vue @@ -8,7 +8,7 @@ autocomplete="off" /> diff --git a/packages/web-app-admin-settings/tests/unit/components/Users/DeleteUserModal.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Users/DeleteUserModal.spec.ts deleted file mode 100644 index c66df16bcd2..00000000000 --- a/packages/web-app-admin-settings/tests/unit/components/Users/DeleteUserModal.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import DeleteUserModal from '../../../../src/components/Users/DeleteUserModal.vue' -import { defaultPlugins, shallowMount } from 'web-test-helpers' - -describe('DeleteUserModal', () => { - describe('computed method "title"', () => { - it('should be singular if one user is given', () => { - const { wrapper } = getWrapper({ - propsData: { - users: [{ id: '1', onPremisesSamAccountName: 'Marie' }] - } - }) - expect(wrapper.vm.title).toEqual('Delete user Marie?') - }) - it('should be plural if multiple users are given', () => { - const { wrapper } = getWrapper({ - propsData: { - users: [{ id: '1' }, { id: '2' }] - } - }) - expect(wrapper.vm.title).toEqual('Delete 2 selected users?') - }) - }) - - describe('computed method "message"', () => { - it('should be singular if one user is given', () => { - const { wrapper } = getWrapper({ - propsData: { - users: [{ id: '1', onPremisesSamAccountName: 'Marie' }] - } - }) - expect(wrapper.vm.message).toEqual('Are you sure you want to delete this user?') - }) - it('should be plural if multiple users are given', () => { - const { wrapper } = getWrapper({ - propsData: { - users: [{ id: '1' }, { id: '2' }] - } - }) - expect(wrapper.vm.message).toEqual('Are you sure you want to delete all selected users?') - }) - }) -}) - -function getWrapper({ propsData = {} } = {}) { - return { - wrapper: shallowMount(DeleteUserModal, { - props: { ...propsData }, - global: { - plugins: [...defaultPlugins()] - } - }) - } -} From 4eef936e12485610d2340029a6ea32f1919a05be Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 26 Jan 2023 14:52:48 +0100 Subject: [PATCH 3/4] Add unit tests --- .../src/components/Users/UsersList.vue | 4 +- .../unit/components/Users/UsersList.spec.ts | 67 +++++++++++++--- .../tests/unit/views/Users.spec.ts | 80 +++++++++---------- 3 files changed, 95 insertions(+), 56 deletions(-) diff --git a/packages/web-app-admin-settings/src/components/Users/UsersList.vue b/packages/web-app-admin-settings/src/components/Users/UsersList.vue index 2a80090d75a..3bd71aa613b 100644 --- a/packages/web-app-admin-settings/src/components/Users/UsersList.vue +++ b/packages/web-app-admin-settings/src/components/Users/UsersList.vue @@ -54,14 +54,14 @@ diff --git a/packages/web-app-admin-settings/tests/unit/components/Users/UsersList.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Users/UsersList.spec.ts index 26ac6fb0561..376dbd6a21b 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Users/UsersList.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Users/UsersList.spec.ts @@ -1,21 +1,29 @@ import UsersList from '../../../../src/components/Users/UsersList.vue' -import { defaultPlugins, shallowMount } from 'web-test-helpers' +import { defaultPlugins, mount, shallowMount } from 'web-test-helpers' +import { displayPositionedDropdown, eventBus } from 'web-pkg' +import { SideBarEventTopics } from 'web-pkg/src/composables/sideBar' + +const getUserMocks = () => [{ id: '1', displayName: 'jan' }] +jest.mock('web-pkg/src/helpers', () => ({ + ...jest.requireActual('web-pkg/src/helpers'), + displayPositionedDropdown: jest.fn() +})) describe('UsersList', () => { describe('computed method "allUsersSelected"', () => { it('should be true if all users are selected', () => { const { wrapper } = getWrapper({ - propsData: { - users: [{ id: '1', displayName: 'jan' }], - selectedUsers: [{ id: '1', displayName: 'jan' }] + props: { + users: getUserMocks(), + selectedUsers: getUserMocks() } }) expect(wrapper.vm.allUsersSelected).toBeTruthy() }) it('should be false if not every user is selected', () => { const { wrapper } = getWrapper({ - propsData: { - users: [{ id: '1', displayName: 'jan' }], + props: { + users: getUserMocks(), selectedUsers: [] } }) @@ -95,11 +103,47 @@ describe('UsersList', () => { ).toEqual([]) }) }) + + it('emits events on row click', () => { + const users = getUserMocks() + const { wrapper } = getWrapper({ props: { users } }) + wrapper.vm.rowClicked(users[0]) + expect(wrapper.emitted('unSelectAllUsers').length).toBeTruthy() + expect(wrapper.emitted('toggleSelectUser')).toBeTruthy() + }) + it('should show the context menu on right click', async () => { + const users = getUserMocks() + const spyDisplayPositionedDropdown = jest.mocked(displayPositionedDropdown) + const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + await wrapper.find(`[data-item-id="${users[0].id}"]`).trigger('contextmenu') + expect(spyDisplayPositionedDropdown).toHaveBeenCalledTimes(1) + }) + it('should show the context menu on context menu button click', async () => { + const users = getUserMocks() + const spyDisplayPositionedDropdown = jest.mocked(displayPositionedDropdown) + const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + await wrapper.find('.users-table-btn-action-dropdown').trigger('click') + expect(spyDisplayPositionedDropdown).toHaveBeenCalledTimes(1) + }) + it('should show the user details on details button click', async () => { + const users = getUserMocks() + const eventBusSpy = jest.spyOn(eventBus, 'publish') + const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + await wrapper.find('.users-table-btn-details').trigger('click') + expect(eventBusSpy).toHaveBeenCalledWith(SideBarEventTopics.open) + }) + it('should show the user edit panel on edit button click', async () => { + const users = getUserMocks() + const eventBusSpy = jest.spyOn(eventBus, 'publish') + const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + await wrapper.find('.users-table-btn-edit').trigger('click') + expect(eventBusSpy).toHaveBeenCalledWith(SideBarEventTopics.openWithPanel, 'EditPanel') + }) }) -function getWrapper({ propsData = {} } = {}) { +function getWrapper({ mountType = shallowMount, props = {} } = {}) { return { - wrapper: shallowMount(UsersList, { + wrapper: mountType(UsersList, { props: { users: [], selectedUsers: [], @@ -122,10 +166,13 @@ function getWrapper({ propsData = {} } = {}) { } ], headerPosition: 0, - ...propsData + ...props }, global: { - plugins: [...defaultPlugins()] + plugins: [...defaultPlugins()], + stubs: { + OcCheckbox: true + } } }) } diff --git a/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts b/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts index 1876eb38629..3dbc6716744 100644 --- a/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/views/Users.spec.ts @@ -8,10 +8,13 @@ import { defaultComponentMocks, defaultPlugins, defaultStoreMockOptions, + mount, shallowMount } from 'web-test-helpers' import { AxiosResponse } from 'axios' +jest.mock('web-pkg/src/composables/appDefaults') + const getDefaultUser = () => { return { id: '1', @@ -258,44 +261,6 @@ describe('Users view', () => { }) }) - describe('method "deleteUsers"', () => { - it('should hide the modal and show message on success', async () => { - const { wrapper } = getMountedWrapper() - const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') - const toggleDeleteUserModalStub = jest.spyOn(wrapper.vm, 'toggleDeleteUserModal') - await wrapper.vm.deleteUsers([{ id: '1' }]) - - expect(showMessageStub).toHaveBeenCalled() - expect(toggleDeleteUserModalStub).toHaveBeenCalledTimes(1) - }) - it('should show message on error', async () => { - jest.spyOn(console, 'error').mockImplementation(() => undefined) - const graph = getDefaultGraphMock() - graph.users.deleteUser.mockImplementation(() => mockAxiosReject()) - const { wrapper } = getMountedWrapper({ graph }) - const graphDeleteUserStub = jest.spyOn(graph.users, 'deleteUser') - const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') - const toggleDeleteUserModalStub = jest.spyOn(wrapper.vm, 'toggleDeleteUserModal') - await wrapper.vm.deleteUsers([{ id: '2' }]) - - expect(graphDeleteUserStub).toHaveBeenCalledTimes(1) - expect(showMessageStub).toHaveBeenCalled() - expect(toggleDeleteUserModalStub).toHaveBeenCalledTimes(0) - }) - it('should show message while user tries to delete own account', async () => { - const { wrapper } = getMountedWrapper() - const graph = getDefaultGraphMock() - const graphDeleteUserStub = jest.spyOn(graph.users, 'deleteUser') - const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage') - const toggleDeleteUserModalStub = jest.spyOn(wrapper.vm, 'toggleDeleteUserModal') - await wrapper.vm.deleteUsers([{ id: '1' }]) - - expect(graphDeleteUserStub).toHaveBeenCalledTimes(0) - expect(showMessageStub).toHaveBeenCalled() - expect(toggleDeleteUserModalStub).toHaveBeenCalled() - }) - }) - describe('computed method "sideBarAvailablePanels"', () => { it('should contain EditPanel when one user is selected', () => { const { wrapper } = getMountedWrapper() @@ -338,9 +303,31 @@ describe('Users view', () => { expect(wrapper.vm.allUsersSelected).toBeFalsy() }) }) + + describe('batch actions', () => { + it('do not display when no user selected', async () => { + const { wrapper } = getMountedWrapper({ mountType: mount }) + await wrapper.vm.loadResourcesTask.last + expect(wrapper.find('batch-actions-stub').exists()).toBeFalsy() + }) + it('display when one user selected', async () => { + const { wrapper } = getMountedWrapper({ mountType: mount }) + await wrapper.vm.loadResourcesTask.last + wrapper.vm.toggleSelectUser(getDefaultUser()) + await wrapper.vm.$nextTick() + expect(wrapper.find('batch-actions-stub').exists()).toBeTruthy() + }) + it('display when more than one users selected', async () => { + const { wrapper } = getMountedWrapper({ mountType: mount }) + await wrapper.vm.loadResourcesTask.last + wrapper.vm.toggleSelectAllUsers() + await wrapper.vm.$nextTick() + expect(wrapper.find('batch-actions-stub').exists()).toBeTruthy() + }) + }) }) -function getMountedWrapper({ data = {}, graph = getDefaultGraphMock() } = {}) { +function getMountedWrapper({ mountType = shallowMount, graph = getDefaultGraphMock() } = {}) { const mocks = { ...defaultComponentMocks() } @@ -357,13 +344,18 @@ function getMountedWrapper({ data = {}, graph = getDefaultGraphMock() } = {}) { return { mocks, - wrapper: shallowMount(Users, { - data: () => { - return { ...data } - }, + wrapper: mountType(Users, { global: { plugins: [...defaultPlugins(), store], - mocks + mocks, + stubs: { + CreateUserModal: true, + AppLoadingSpinner: true, + NoContentMessage: true, + UsersList: true, + OcBreadcrumb: true, + BatchActions: true + } } }) } From 5069181568c75c6408697634971b7a841fd7db39 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 26 Jan 2023 14:56:37 +0100 Subject: [PATCH 4/4] Fix imports --- .../web-app-admin-settings/src/views/Users.vue | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/web-app-admin-settings/src/views/Users.vue b/packages/web-app-admin-settings/src/views/Users.vue index d857e96068e..5b5b0cd4c4b 100644 --- a/packages/web-app-admin-settings/src/views/Users.vue +++ b/packages/web-app-admin-settings/src/views/Users.vue @@ -86,16 +86,22 @@ import EditPanel from '../components/Users/SideBar/EditPanel.vue' import BatchActions from '../components/BatchActions.vue' import Delete from '../mixins/users/delete' import NoContentMessage from 'web-pkg/src/components/NoContentMessage.vue' -import { useAccessToken, useStore } from 'web-pkg/src/composables' -import { defineComponent, ref, onBeforeUnmount, onMounted, unref, watch } from 'vue' +import { useAccessToken, useGraphClient, useStore } from 'web-pkg/src/composables' +import { + computed, + defineComponent, + getCurrentInstance, + ref, + onBeforeUnmount, + onMounted, + unref, + watch +} from 'vue' import { useTask } from 'vue-concurrency' import { eventBus } from 'web-pkg/src/services/eventBus' import { mapActions, mapGetters, mapMutations, mapState } from 'vuex' -import { useGraphClient } from 'web-pkg/src/composables' import AppTemplate from '../components/AppTemplate.vue' import { useSideBar } from 'web-pkg/src/composables/sideBar' -import { computed } from 'vue' -import { getCurrentInstance } from 'vue-demi' export default defineComponent({ name: 'UsersView',