diff --git a/changelog/unreleased/bugfix-admin-settings-keyboard-navigation b/changelog/unreleased/bugfix-admin-settings-keyboard-navigation new file mode 100644 index 00000000000..ece2dcb2b94 --- /dev/null +++ b/changelog/unreleased/bugfix-admin-settings-keyboard-navigation @@ -0,0 +1,7 @@ +Bugfix: Admin settings keyboard navigation + +We've fixed a bug where keyboard navigation stopped working after deleting a resource in the admin settings app. +We also fixed a bug where initial keyboard navigation didn't work, when no resource was selected. + +https://github.com/owncloud/web/pull/10417 +https://github.com/owncloud/web/issues/10186 diff --git a/packages/web-app-admin-settings/src/components/Groups/CreateGroupModal.vue b/packages/web-app-admin-settings/src/components/Groups/CreateGroupModal.vue index 28a7936c37a..c15262b4b5a 100644 --- a/packages/web-app-admin-settings/src/components/Groups/CreateGroupModal.vue +++ b/packages/web-app-admin-settings/src/components/Groups/CreateGroupModal.vue @@ -17,7 +17,8 @@ import { useGettext } from 'vue3-gettext' import { computed, defineComponent, ref, PropType, unref, watch } from 'vue' import { Group } from '@ownclouders/web-client/src/generated' -import { MaybeRef, Modal, useClientService, useEventBus, useMessages } from '@ownclouders/web-pkg' +import { MaybeRef, Modal, useClientService, useMessages } from '@ownclouders/web-pkg' +import { useGroupSettingsStore } from '../../composables' export default defineComponent({ name: 'CreateGroupModal', @@ -28,8 +29,8 @@ export default defineComponent({ setup(props, { emit, expose }) { const { $gettext } = useGettext() const { showMessage, showErrorMessage } = useMessages() - const eventBus = useEventBus() const clientService = useClientService() + const groupSettingsStore = useGroupSettingsStore() const group: MaybeRef = ref({ displayName: '' }) const formData = ref({ @@ -62,7 +63,7 @@ export default defineComponent({ const client = clientService.graphAuthenticated const response = await client.groups.createGroup(unref(group)) showMessage({ title: $gettext('Group was created successfully') }) - eventBus.publish('app.admin-settings.groups.add', { ...response?.data, members: [] }) + groupSettingsStore.upsertGroup(response?.data) } catch (error) { console.error(error) showErrorMessage({ diff --git a/packages/web-app-admin-settings/src/components/Groups/GroupsList.vue b/packages/web-app-admin-settings/src/components/Groups/GroupsList.vue index a94fc989fb0..692fffcb69d 100644 --- a/packages/web-app-admin-settings/src/components/Groups/GroupsList.vue +++ b/packages/web-app-admin-settings/src/components/Groups/GroupsList.vue @@ -29,7 +29,7 @@ :model-value="allGroupsSelected" hide-label @update:model-value=" - allGroupsSelected ? $emit('unSelectAllGroups') : $emit('selectGroups', paginatedItems) + allGroupsSelected ? unselectAllGroups() : selectGroups(paginatedItems) " /> @@ -41,7 +41,7 @@ :option="rowData.item" :label="getSelectGroupLabel(rowData.item)" hide-label - @update:model-value="toggleGroup(rowData.item)" + @update:model-value="selectGroup(rowData.item)" @click.stop="rowClicked([rowData.item, $event])" /> @@ -106,13 +106,14 @@ diff --git a/packages/web-app-admin-settings/src/views/Spaces.vue b/packages/web-app-admin-settings/src/views/Spaces.vue index 222e4a5014c..0e56a3aa431 100644 --- a/packages/web-app-admin-settings/src/views/Spaces.vue +++ b/packages/web-app-admin-settings/src/views/Spaces.vue @@ -32,18 +32,11 @@
- + - +
@@ -81,6 +74,9 @@ import { computed, defineComponent, onBeforeUnmount, onMounted, ref, unref } fro import { useTask } from 'vue-concurrency' import { useGettext } from 'vue3-gettext' +import { useSpaceSettingsStore } from '../composables' +import { storeToRefs } from 'pinia' + export default defineComponent({ name: 'SpacesView', components: { @@ -96,7 +92,6 @@ export default defineComponent({ } }, setup() { - const spaces = ref([]) const clientService = useClientService() const { $gettext } = useGettext() const { isSideBarOpen, sideBarActivePanel } = useSideBar() @@ -105,14 +100,15 @@ export default defineComponent({ const loadResourcesEventToken = ref(null) let updateQuotaForSpaceEventToken: string const template = ref(null) - const selectedSpaces = ref([]) + const spaceSettingsStore = useSpaceSettingsStore() + const { spaces, selectedSpaces } = storeToRefs(spaceSettingsStore) const currentPageQuery = useRouteQuery('page', '1') const currentPage = computed(() => { return parseInt(queryItemAsString(unref(currentPageQuery))) }) - const itemsPerPageQuery = useRouteQuery('admin-settings-items-per-page', '1') + const itemsPerPageQuery = useRouteQuery('items-per-page', '1') const itemsPerPage = computed(() => { return parseInt(queryItemAsString(unref(itemsPerPageQuery))) }) @@ -127,38 +123,20 @@ export default defineComponent({ const drives = drivesResponse.map((space) => buildSpace({ ...space, serverUrl: configStore.serverUrl }) ) - spaces.value = drives + spaceSettingsStore.setSpaces(drives) }) const breadcrumbs = computed(() => [ { text: $gettext('Administration Settings'), to: { path: '/admin-settings' } }, { text: $gettext('Spaces'), - onClick: () => eventBus.publish('app.admin-settings.list.load') + onClick: () => { + spaceSettingsStore.setSelectedSpaces([]) + loadResourcesTask.perform() + } } ]) - const selectSpaces = (paginatedSpaces) => { - selectedSpaces.value.splice(0, selectedSpaces.value.length, ...paginatedSpaces) - } - - const toggleSelectSpace = (toggledSpace, deselect = false) => { - if (deselect) { - selectedSpaces.value.splice(0, selectedSpaces.value.length) - } - const isSpaceSelected = unref(selectedSpaces).find((s) => s.id === toggledSpace.id) - if (!isSpaceSelected) { - return selectedSpaces.value.push(toggledSpace) - } - - const index = selectedSpaces.value.findIndex((s) => s.id === toggledSpace.id) - selectedSpaces.value.splice(index, 1) - } - - const unselectAllSpaces = () => { - selectedSpaces.value.splice(0, selectedSpaces.value.length) - } - const { actions: deleteActions } = useSpaceActionsDelete() const { actions: disableActions } = useSpaceActionsDisable() const { actions: editQuotaActions } = useSpaceActionsEditQuota() @@ -231,6 +209,7 @@ export default defineComponent({ onMounted(async () => { await loadResourcesTask.perform() + loadResourcesEventToken.value = eventBus.subscribe( 'app.admin-settings.list.load', async () => { @@ -257,6 +236,7 @@ export default defineComponent({ }) onBeforeUnmount(() => { + spaceSettingsStore.reset() eventBus.unsubscribe('app.admin-settings.list.load', unref(loadResourcesEventToken)) eventBus.unsubscribe( 'app.admin-settings.spaces.space.quota.updated', @@ -274,10 +254,7 @@ export default defineComponent({ selectedSpaces, sideBarAvailablePanels, sideBarPanelContext, - template, - selectSpaces, - toggleSelectSpace, - unselectAllSpaces + template } } }) diff --git a/packages/web-app-admin-settings/src/views/Users.vue b/packages/web-app-admin-settings/src/views/Users.vue index dafcb24ea6e..b6b0ccdcf5c 100644 --- a/packages/web-app-admin-settings/src/views/Users.vue +++ b/packages/web-app-admin-settings/src/views/Users.vue @@ -31,15 +31,7 @@ - + @@ -145,7 +137,7 @@ import { useUserActionsEditQuota, useUserActionsCreateUser } from '../composables' -import { Drive, User, Group } from '@ownclouders/web-client/src/generated' +import { User, Group } from '@ownclouders/web-client/src/generated' import { AppLoadingSpinner, ItemFilter, @@ -159,9 +151,6 @@ import { useSideBar, SideBarPanel, SideBarPanelContext, - useUserStore, - useMessages, - useSpacesStore, useCapabilityStore, useConfigStore } from '@ownclouders/web-pkg' @@ -177,12 +166,13 @@ import { } from 'vue' import { useTask } from 'vue-concurrency' import { useGettext } from 'vue3-gettext' -import { diff } from 'deep-object-diff' import Mark from 'mark.js' import { format } from 'util' -import { isEqual, isEmpty, omit } from 'lodash-es' +import { omit } from 'lodash-es' import { storeToRefs } from 'pinia' +import { useUserSettingsStore } from '../composables/stores/userSettings' + export default defineComponent({ name: 'UsersView', components: { @@ -197,23 +187,13 @@ export default defineComponent({ const { $gettext } = useGettext() const router = useRouter() const route = useRoute() - const { showErrorMessage } = useMessages() const capabilityStore = useCapabilityStore() const capabilityRefs = storeToRefs(capabilityStore) const clientService = useClientService() const configStore = useConfigStore() - const userStore = useUserStore() - const spacesStore = useSpacesStore() - - const currentPageQuery = useRouteQuery('page', '1') - const currentPage = computed(() => { - return parseInt(queryItemAsString(unref(currentPageQuery))) - }) - const itemsPerPageQuery = useRouteQuery('admin-settings-items-per-page', '1') - const itemsPerPage = computed(() => { - return parseInt(queryItemAsString(unref(itemsPerPageQuery))) - }) + const userSettingsStore = useUserSettingsStore() + const { users, selectedUsers } = storeToRefs(userSettingsStore) const writableGroups = computed(() => { return unref(groups).filter((g) => !g.groupTypes?.includes('ReadOnly')) @@ -232,26 +212,22 @@ export default defineComponent({ const { actions: editLoginActions } = useUserActionsEditLogin() const { actions: editQuotaActions } = useUserActionsEditQuota() - const users = ref([]) const groups = ref([]) const roles = ref([]) - const selectedUsers = ref([]) const additionalUserDataLoadedForUserIds = ref([]) const applicationId = ref() const selectedUserIds = computed(() => unref(selectedUsers).map((selectedUser) => selectedUser.id) ) const isFilteringMandatory = ref(configStore.options.userListRequiresFilter) + const sideBarLoading = ref(false) const template = ref() const displayNameQuery = useRouteQuery('q_displayName') const filterTermDisplayName = ref(queryItemAsString(unref(displayNameQuery))) const markInstance = ref(null) - let loadResourcesEventToken: string let editQuotaActionEventToken: string - let addUserEventToken: string - let updateUsersEventToken: string const loadGroupsTask = useTask(function* (signal) { const groupsResponse = yield clientService.graphAuthenticated.groups.listGroups('displayName') @@ -267,7 +243,7 @@ export default defineComponent({ const loadUsersTask = useTask(function* (signal) { if (unref(isFilteringMandatory) && !unref(isFilteringActive)) { - return (users.value = []) + return userSettingsStore.setUsers([]) } const filter = Object.values(filters) @@ -294,7 +270,7 @@ export default defineComponent({ 'displayName', filter ) - users.value = usersResponse.data.value || [] + userSettingsStore.setUsers(usersResponse.data.value || []) }) const loadResourcesTask = useTask(function* (signal) { @@ -354,14 +330,14 @@ export default defineComponent({ const filterGroups = (groups) => { filters.groups.ids.value = groups.map((g) => g.id) loadUsersTask.perform() - selectedUsers.value = [] + userSettingsStore.setSelectedUsers([]) additionalUserDataLoadedForUserIds.value = [] return resetPagination() } const filterRoles = (roles) => { filters.roles.ids.value = roles.map((r) => r.id) loadUsersTask.perform() - selectedUsers.value = [] + userSettingsStore.setSelectedUsers([]) additionalUserDataLoadedForUserIds.value = [] return resetPagination() } @@ -375,7 +351,7 @@ export default defineComponent({ }) filters.displayName.value.value = unref(filterTermDisplayName) loadUsersTask.perform() - selectedUsers.value = [] + userSettingsStore.setSelectedUsers([]) additionalUserDataLoadedForUserIds.value = [] return resetPagination() } @@ -399,15 +375,9 @@ export default defineComponent({ }) const updateSpaceQuota = ({ spaceId, quota }) => { - const userIndex = unref(users).findIndex((u) => u.drive?.id === spaceId) - if (userIndex >= 0) { - unref(users)[userIndex].drive.quota = quota - } - - const selectedIndex = unref(selectedUsers).findIndex((u) => u.drive?.id === spaceId) - if (selectedIndex >= 0) { - unref(selectedUsers)[selectedIndex].drive.quota = quota - } + const user = unref(users).find((u) => u.drive?.id === spaceId) + user.drive.quota = quota + userSettingsStore.upsertUser(user) } onMounted(async () => { @@ -421,16 +391,6 @@ export default defineComponent({ } await loadResourcesTask.perform() - loadResourcesEventToken = eventBus.subscribe('app.admin-settings.list.load', async () => { - await loadResourcesTask.perform() - selectedUsers.value = [] - - const pageCount = Math.ceil(unref(users).length / unref(itemsPerPage)) - if (unref(currentPage) > 1 && unref(currentPage) > pageCount) { - // reset pagination to avoid empty lists (happens when deleting all items on the last page) - currentPageQuery.value = pageCount.toString() - } - }) watch( [users, displayNameQuery], @@ -448,14 +408,6 @@ export default defineComponent({ } ) - addUserEventToken = eventBus.subscribe('app.admin-settings.users.add', (user) => { - users.value.push(user) - }) - updateUsersEventToken = eventBus.subscribe( - 'app.admin-settings.users.update', - updateLocalUsers - ) - editQuotaActionEventToken = eventBus.subscribe( 'app.admin-settings.users.user.quota.updated', updateSpaceQuota @@ -463,125 +415,11 @@ export default defineComponent({ }) onBeforeUnmount(() => { - eventBus.unsubscribe('app.admin-settings.list.load', loadResourcesEventToken) - eventBus.unsubscribe('app.admin-settings.users.add', addUserEventToken) - eventBus.unsubscribe('app.admin-settings.users.update', updateUsersEventToken) + userSettingsStore.reset() eventBus.unsubscribe('app.admin-settings.users.user.quota.updated', editQuotaActionEventToken) }) - const updateLocalUsers = (usersToUpdate: User[]) => { - for (const _user of usersToUpdate) { - const userIndex = unref(users).findIndex((user) => user.id === _user.id) - unref(users)[userIndex] = _user - const selectedUserIndex = unref(selectedUsers).findIndex((user) => user.id === _user.id) - if (selectedUserIndex >= 0) { - selectedUsers.value = [...unref(selectedUsers).filter(({ id }) => id !== _user.id), _user] - } - } - } - - const onEditUser = async ({ user, editUser }) => { - try { - const client = clientService.graphAuthenticated - const graphEditUserPayloadExtractor = (user) => { - return omit(user, ['drive', 'appRoleAssignments', 'memberOf']) - } - const graphEditUserPayload = diff( - graphEditUserPayloadExtractor(user), - graphEditUserPayloadExtractor(editUser) - ) - - if (!isEmpty(graphEditUserPayload)) { - await client.users.editUser(editUser.id, graphEditUserPayload) - } - - if (!isEqual(user.drive?.quota?.total, editUser.drive?.quota?.total)) { - await onUpdateUserDrive(editUser) - } - - if (!isEqual(user.memberOf, editUser.memberOf)) { - await onUpdateUserGroupAssignments(user, editUser) - } - - if ( - !isEqual(user.appRoleAssignments[0]?.appRoleId, editUser.appRoleAssignments[0]?.appRoleId) - ) { - await onUpdateUserAppRoleAssignments(user, editUser) - } - - const { data: updatedUser } = await client.users.getUser(user.id) - const userIndex = unref(users).findIndex((user) => user.id === updatedUser.id) - users.value[userIndex] = updatedUser - const selectedUserIndex = unref(selectedUsers).findIndex( - (user) => user.id === updatedUser.id - ) - if (selectedUserIndex >= 0) { - // FIXME: why do we need to update selectedUsers? - selectedUsers.value[selectedUserIndex] = updatedUser - } - - eventBus.publish('sidebar.entity.saved') - - if (userStore.user.id === updatedUser.id) { - userStore.setUser(updatedUser) - } - - return updatedUser - } catch (error) { - console.error(error) - showErrorMessage({ - title: $gettext('Failed to edit user'), - errors: [error] - }) - } - } - - const onUpdateUserDrive = async (editUser: User) => { - const client = clientService.graphAuthenticated - const updateDriveResponse = await client.drives.updateDrive( - editUser.drive.id, - { quota: { total: editUser.drive.quota.total } } as Drive, - {} - ) - - if (editUser.id === userStore.user.id) { - // Load current user quota - spacesStore.updateSpaceField({ - id: editUser.drive.id, - field: 'spaceQuota', - value: updateDriveResponse.data.quota - }) - } - } - const onUpdateUserAppRoleAssignments = (user: User, editUser: User) => { - const client = clientService.graphAuthenticated - return client.users.createUserAppRoleAssignment(user.id, { - appRoleId: editUser.appRoleAssignments[0].appRoleId, - resourceId: unref(applicationId), - principalId: editUser.id - }) - } - const onUpdateUserGroupAssignments = (user: User, editUser: User) => { - const client = clientService.graphAuthenticated - const groupsToAdd = editUser.memberOf.filter( - (editUserGroup) => !user.memberOf.some((g) => g.id === editUserGroup.id) - ) - const groupsToDelete = user.memberOf.filter( - (editUserGroup) => !editUser.memberOf.some((g) => g.id === editUserGroup.id) - ) - const requests = [] - - for (const groupToAdd of groupsToAdd) { - requests.push(client.groups.addMember(groupToAdd.id, user.id, configStore.serverUrl)) - } - for (const groupToDelete of groupsToDelete) { - requests.push(client.groups.deleteMember(groupToDelete.id, user.id)) - } - - return Promise.all(requests) - } - const sideBarPanelContext = computed>(() => { return { parent: null, @@ -612,7 +450,7 @@ export default defineComponent({ user: items.length === 1 ? items[0] : null, roles: unref(roles), groups: unref(groups), - onConfirm: onEditUser + applicationId: unref(applicationId) }) } ] satisfies SideBarPanel[] @@ -626,7 +464,6 @@ export default defineComponent({ users, roles, groups, - applicationId, loadResourcesTask, loadAdditionalUserDataTask, clientService, @@ -640,8 +477,8 @@ export default defineComponent({ isFilteringMandatory, sideBarPanelContext, sideBarAvailablePanels, - onEditUser, - createUserAction + createUserAction, + userSettingsStore } }, computed: { @@ -650,31 +487,13 @@ export default defineComponent({ { text: this.$gettext('Administration Settings'), to: { path: '/admin-settings' } }, { text: this.$gettext('Users'), - onClick: () => eventBus.publish('app.admin-settings.list.load') + onClick: () => { + this.userSettingsStore.setSelectedUsers([]) + this.loadResourcesTask.perform() + } } ] } - }, - methods: { - selectUsers(users) { - this.selectedUsers.splice(0, this.selectedUsers.length, ...users) - }, - toggleSelectUser(toggledUser, deselect = false) { - if (deselect) { - this.selectedUsers.splice(0, this.selectedUsers.length) - } - const isUserSelected = this.selectedUsers.find((user) => user.id === toggledUser.id) - - if (!isUserSelected) { - return this.selectedUsers.push(this.users.find((u) => u.id === toggledUser.id)) - } - - const index = this.selectedUsers.findIndex((user) => user.id === toggledUser.id) - this.selectedUsers.splice(index, 1) - }, - unselectAllUsers() { - this.selectedUsers.splice(0, this.selectedUsers.length) - } } }) diff --git a/packages/web-app-admin-settings/tests/unit/components/Groups/CreateGroupModal.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Groups/CreateGroupModal.spec.ts index 22f8282c1f6..72da9c67e0a 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Groups/CreateGroupModal.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Groups/CreateGroupModal.spec.ts @@ -9,6 +9,7 @@ import { import { mock } from 'vitest-mock-extended' import { AxiosResponse } from 'axios' import { Modal, eventBus, useMessages } from '@ownclouders/web-pkg' +import { useGroupSettingsStore } from '../../../../src/composables' describe('CreateGroupModal', () => { describe('computed method "isFormInvalid"', () => { @@ -80,12 +81,12 @@ describe('CreateGroupModal', () => { mockAxiosResolve({ id: 'e3515ffb-d264-4dfc-8506-6c239f6673b5' }) ) - const eventSpy = vi.spyOn(eventBus, 'publish') await wrapper.vm.onConfirm() const { showMessage } = useMessages() expect(showMessage).toHaveBeenCalled() - expect(eventSpy).toHaveBeenCalled() + const { upsertGroup } = useGroupSettingsStore() + expect(upsertGroup).toHaveBeenCalled() }) it('should show message on error', async () => { @@ -100,12 +101,12 @@ describe('CreateGroupModal', () => { mocks.$clientService.graphAuthenticated.groups.createGroup.mockRejectedValue( mockAxiosResolve({ id: 'e3515ffb-d264-4dfc-8506-6c239f6673b5' }) ) - const eventSpy = vi.spyOn(eventBus, 'publish') await wrapper.vm.onConfirm() const { showErrorMessage } = useMessages() expect(showErrorMessage).toHaveBeenCalled() - expect(eventSpy).not.toHaveBeenCalled() + const { upsertGroup } = useGroupSettingsStore() + expect(upsertGroup).not.toHaveBeenCalled() }) }) }) diff --git a/packages/web-app-admin-settings/tests/unit/components/Groups/GroupsList.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Groups/GroupsList.spec.ts index f2993406059..95628ea7513 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Groups/GroupsList.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Groups/GroupsList.spec.ts @@ -2,6 +2,7 @@ import GroupsList from '../../../../src/components/Groups/GroupsList.vue' import { defaultComponentMocks, defaultPlugins, mount, shallowMount } from 'web-test-helpers' import { displayPositionedDropdown, eventBus, queryItemAsString } from '@ownclouders/web-pkg' import { SideBarEventTopics } from '@ownclouders/web-pkg' +import { useGroupSettingsStore } from '../../../../src/composables' const getGroupMocks = () => [ { id: '1', members: [] }, @@ -56,36 +57,74 @@ describe('GroupsList', () => { ).toEqual([]) }) }) - it('emits events on row click', () => { - const groups = getGroupMocks() - const { wrapper } = getWrapper({ props: { groups } }) - wrapper.vm.rowClicked([groups[0]]) - expect(wrapper.emitted('toggleSelectGroup')).toBeTruthy() - }) it('should show the context menu on right click', async () => { const groups = getGroupMocks() const spyDisplayPositionedDropdown = vi.mocked(displayPositionedDropdown) - const { wrapper } = getWrapper({ mountType: mount, props: { groups } }) + const { wrapper } = getWrapper({ mountType: mount, groups }) await wrapper.find(`[data-item-id="${groups[0].id}"]`).trigger('contextmenu') expect(spyDisplayPositionedDropdown).toHaveBeenCalledTimes(1) }) it('should show the context menu on context menu button click', async () => { const groups = getGroupMocks() const spyDisplayPositionedDropdown = vi.mocked(displayPositionedDropdown) - const { wrapper } = getWrapper({ mountType: mount, props: { groups } }) + const { wrapper } = getWrapper({ mountType: mount, groups }) await wrapper.find('.groups-table-btn-action-dropdown').trigger('click') expect(spyDisplayPositionedDropdown).toHaveBeenCalledTimes(1) }) it('should show the group details on details button click', async () => { const groups = getGroupMocks() const eventBusSpy = vi.spyOn(eventBus, 'publish') - const { wrapper } = getWrapper({ mountType: mount, props: { groups } }) + const { wrapper } = getWrapper({ mountType: mount, groups }) await wrapper.find('.groups-table-btn-details').trigger('click') expect(eventBusSpy).toHaveBeenCalledWith(SideBarEventTopics.open) }) + describe('toggle selection', () => { + describe('selectGroups method', () => { + it('selects all groups', async () => { + const groups = getGroupMocks() + const { wrapper } = getWrapper({ mountType: shallowMount, groups }) + wrapper.vm.selectGroups(groups) + const { setSelectedGroups } = useGroupSettingsStore() + expect(setSelectedGroups).toHaveBeenCalledWith(groups) + }) + }) + describe('selectGroup method', () => { + it('selects a group', async () => { + const groups = getGroupMocks() + const { wrapper } = getWrapper({ mountType: shallowMount, groups: [groups[0]] }) + wrapper.vm.selectGroup(groups[0]) + const { addSelectedGroup } = useGroupSettingsStore() + expect(addSelectedGroup).toHaveBeenCalledWith(groups[0]) + }) + it('de-selects a selected group', async () => { + const groups = getGroupMocks() + const { wrapper } = getWrapper({ + mountType: shallowMount, + groups: [groups[0]], + selectedGroups: [groups[0]] + }) + wrapper.vm.selectGroup(groups[0]) + const { setSelectedGroups } = useGroupSettingsStore() + expect(setSelectedGroups).toHaveBeenCalledWith([]) + }) + }) + describe('unselectAllGroups method', () => { + it('de-selects all selected groups', async () => { + const groups = getGroupMocks() + const { wrapper } = getWrapper({ + mountType: shallowMount, + groups: [groups[0]], + selectedGroups: [groups[0]] + }) + wrapper.vm.unselectAllGroups() + const { setSelectedGroups } = useGroupSettingsStore() + expect(setSelectedGroups).toHaveBeenCalledWith([]) + }) + }) + }) }) -function getWrapper({ mountType = shallowMount, props = {} } = {}) { +function getWrapper({ mountType = shallowMount, groups = [], selectedGroups = [] } = {}) { vi.mocked(queryItemAsString).mockImplementationOnce(() => '1') vi.mocked(queryItemAsString).mockImplementationOnce(() => '100') const mocks = defaultComponentMocks() @@ -93,13 +132,16 @@ function getWrapper({ mountType = shallowMount, props = {} } = {}) { return { wrapper: mountType(GroupsList, { props: { - groups: [], - selectedGroups: [], - headerPosition: 0, - ...props + headerPosition: 0 }, global: { - plugins: [...defaultPlugins()], + plugins: [ + ...defaultPlugins({ + piniaOptions: { + groupSettingsStore: { groups, selectedGroups } + } + }) + ], mocks, provide: mocks, stubs: { diff --git a/packages/web-app-admin-settings/tests/unit/components/Groups/SideBar/EditPanel.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Groups/SideBar/EditPanel.spec.ts index 63bdae1bc97..60086193438 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Groups/SideBar/EditPanel.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Groups/SideBar/EditPanel.spec.ts @@ -1,7 +1,14 @@ import EditPanel from '../../../../../src/components/Groups/SideBar/EditPanel.vue' -import { defaultComponentMocks, defaultPlugins, mockAxiosReject, mount } from 'web-test-helpers' +import { + defaultComponentMocks, + defaultPlugins, + mockAxiosReject, + mockAxiosResolve, + mount +} from 'web-test-helpers' import { mock } from 'vitest-mock-extended' import { AxiosResponse } from 'axios' +import { eventBus, useMessages } from '@ownclouders/web-pkg' describe('EditPanel', () => { it('renders all available inputs', () => { @@ -56,6 +63,41 @@ describe('EditPanel', () => { }) }) + describe('method "onEditGroup"', () => { + it('should emit event on success', async () => { + const { wrapper, mocks } = getWrapper() + + const clientService = mocks.$clientService + clientService.graphAuthenticated.groups.editGroup.mockResolvedValue(mockAxiosResolve()) + clientService.graphAuthenticated.groups.getGroup.mockResolvedValue( + mockAxiosResolve({ id: '1', displayName: 'administrators' }) + ) + + const editGroup = { + id: '1', + name: 'administrators' + } + + const busStub = vi.spyOn(eventBus, 'publish') + const updatedGroup = await wrapper.vm.onEditGroup(editGroup) + + expect(updatedGroup.id).toEqual('1') + expect(updatedGroup.displayName).toEqual('administrators') + expect(busStub).toHaveBeenCalled() + }) + + it('should show message on error', async () => { + vi.spyOn(console, 'error').mockImplementation(() => undefined) + const { wrapper, mocks } = getWrapper() + const clientService = mocks.$clientService + clientService.graphAuthenticated.groups.editGroup.mockImplementation(() => mockAxiosReject()) + await wrapper.vm.onEditGroup({}) + + const { showErrorMessage } = useMessages() + expect(showErrorMessage).toHaveBeenCalled() + }) + }) + describe('computed method "invalidFormData"', () => { it('should be false if formData is invalid', () => { const { wrapper } = getWrapper() diff --git a/packages/web-app-admin-settings/tests/unit/components/Spaces/SpacesList.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Spaces/SpacesList.spec.ts index 3d23d2eb34a..02f29da62b8 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Spaces/SpacesList.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Spaces/SpacesList.spec.ts @@ -4,6 +4,8 @@ import { SortDir, eventBus, queryItemAsString } from '@ownclouders/web-pkg' import { displayPositionedDropdown } from '@ownclouders/web-pkg' import { SideBarEventTopics } from '@ownclouders/web-pkg' import { nextTick } from 'vue' +import { useSpaceSettingsStore } from '../../../../src/composables' +import { mock } from 'vitest-mock-extended' import { OcTable } from 'design-system/src/components' import { SpaceResource } from '@ownclouders/web-client' @@ -60,7 +62,7 @@ vi.mock('@ownclouders/web-pkg', async (importOriginal) => ({ describe('SpacesList', () => { it('should render all spaces in a table', () => { - const { wrapper } = getWrapper({ spaces: [spaceMocks[0]] }) + const { wrapper } = getWrapper({ spaces: spaceMocks }) expect(wrapper.html()).toMatchSnapshot() }) it.each(['name', 'members', 'totalQuota', 'usedQuota', 'remainingQuota', 'status'])( @@ -93,11 +95,6 @@ describe('SpacesList', () => { expect(wrapper.vm.sortBy).toEqual(sortBy) expect(wrapper.vm.sortDir).toEqual(sortDir) }) - it('emits events on file click', () => { - const { wrapper } = getWrapper({ spaces: [spaceMocks[0]] }) - wrapper.vm.fileClicked([spaceMocks[0]]) - expect(wrapper.emitted().toggleSelectSpace).toBeTruthy() - }) it('shows only filtered spaces if filter applied', async () => { const { wrapper } = getWrapper({ spaces: spaceMocks }) wrapper.vm.filterTerm = 'Another' @@ -123,6 +120,45 @@ describe('SpacesList', () => { await wrapper.find('.spaces-table-btn-details').trigger('click') expect(eventBusSpy).toHaveBeenCalledWith(SideBarEventTopics.open) }) + describe('toggle selection', () => { + describe('selectSpaces method', () => { + it('selects all spaces', async () => { + const spaces = [ + mock({ id: '1', name: 'Some Space' }), + mock({ id: '2', name: 'Some other Space' }) + ] + const { wrapper } = getWrapper({ mountType: shallowMount, spaces }) + wrapper.vm.selectSpaces(spaces) + const { setSelectedSpaces } = useSpaceSettingsStore() + expect(setSelectedSpaces).toHaveBeenCalledWith(spaces) + }) + }) + describe('selectSpace method', () => { + it('selects a space', async () => { + const spaces = [mock({ id: '1', name: 'Some Space' })] + const { wrapper } = getWrapper({ mountType: shallowMount, spaces }) + wrapper.vm.selectSpace(spaces[0]) + const { addSelectedSpace } = useSpaceSettingsStore() + expect(addSelectedSpace).toHaveBeenCalledWith(spaces[0]) + }) + it('de-selects a selected space', async () => { + const spaces = [mock({ id: '1', name: 'Some Space' })] + const { wrapper } = getWrapper({ mountType: shallowMount, spaces, selectedSpaces: spaces }) + wrapper.vm.selectSpace(spaces[0]) + const { setSelectedSpaces } = useSpaceSettingsStore() + expect(setSelectedSpaces).toHaveBeenCalledWith([]) + }) + }) + describe('unselectAllSpaces method', () => { + it('de-selects all selected spaces', async () => { + const spaces = [mock({ id: '1', name: 'Some Space' })] + const { wrapper } = getWrapper({ mountType: shallowMount, spaces }) + wrapper.vm.unselectAllSpaces() + const { setSelectedSpaces } = useSpaceSettingsStore() + expect(setSelectedSpaces).toHaveBeenCalledWith([]) + }) + }) + }) }) function getWrapper({ mountType = mount, spaces = [], selectedSpaces = [] } = {}) { @@ -133,12 +169,16 @@ function getWrapper({ mountType = mount, spaces = [], selectedSpaces = [] } = {} return { wrapper: mountType(SpacesList, { props: { - spaces, - selectedSpaces, headerPosition: 0 }, global: { - plugins: [...defaultPlugins()], + plugins: [ + ...defaultPlugins({ + piniaOptions: { + spaceSettingsStore: { spaces, selectedSpaces } + } + }) + ], mocks, provide: mocks, stubs: { diff --git a/packages/web-app-admin-settings/tests/unit/components/Spaces/__snapshots__/SpacesList.spec.ts.snap b/packages/web-app-admin-settings/tests/unit/components/Spaces/__snapshots__/SpacesList.spec.ts.snap index c1f76af2070..6235f85b676 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Spaces/__snapshots__/SpacesList.spec.ts.snap +++ b/packages/web-app-admin-settings/tests/unit/components/Spaces/__snapshots__/SpacesList.spec.ts.snap @@ -84,6 +84,32 @@ exports[`SpacesList > should render all spaces in a table 1`] = ` + + + + + + 2 Another space + user1, user2... +1 + 5 + 2 GB + 500 MB + 1.5 GB + + Disabled + +
+ + @@ -91,7 +117,7 @@ exports[`SpacesList > should render all spaces in a table 1`] = `
-

1 spaces in total

+

2 spaces in total

diff --git a/packages/web-app-admin-settings/tests/unit/components/Users/AddToGroupsModal.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Users/AddToGroupsModal.spec.ts index 37280a9f01c..e49c0b422c8 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Users/AddToGroupsModal.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Users/AddToGroupsModal.spec.ts @@ -8,6 +8,7 @@ import { import { mock } from 'vitest-mock-extended' import { Group, User } from '@ownclouders/web-client/src/generated' import { Modal, eventBus, useMessages } from '@ownclouders/web-pkg' +import { useUserSettingsStore } from '../../../../src/composables/stores/userSettings' describe('AddToGroupsModal', () => { it('renders the input', () => { @@ -31,7 +32,8 @@ describe('AddToGroupsModal', () => { await wrapper.vm.onConfirm() const { showMessage } = useMessages() expect(showMessage).toHaveBeenCalled() - expect(eventSpy).toHaveBeenCalled() + const { upsertUser } = useUserSettingsStore() + expect(upsertUser).toHaveBeenCalledTimes(users.length) }) it('should show message on error', async () => { @@ -44,12 +46,12 @@ describe('AddToGroupsModal', () => { mocks.$clientService.graphAuthenticated.users.getUser.mockRejectedValue(new Error('')) wrapper.vm.selectedOptions = groups - const eventSpy = vi.spyOn(eventBus, 'publish') await wrapper.vm.onConfirm() const { showErrorMessage } = useMessages() expect(showErrorMessage).toHaveBeenCalled() - expect(eventSpy).not.toHaveBeenCalled() + const { upsertUser } = useUserSettingsStore() + expect(upsertUser).not.toHaveBeenCalled() }) }) }) diff --git a/packages/web-app-admin-settings/tests/unit/components/Users/CreateUserModal.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Users/CreateUserModal.spec.ts index ffffbaf2ac9..0cc66af2004 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Users/CreateUserModal.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Users/CreateUserModal.spec.ts @@ -9,6 +9,7 @@ import { import { mock } from 'vitest-mock-extended' import { AxiosResponse } from 'axios' import { Modal, eventBus, useMessages } from '@ownclouders/web-pkg' +import { useUserSettingsStore } from '../../../../src/composables/stores/userSettings' describe('CreateUserModal', () => { describe('computed method "isFormInvalid"', () => { @@ -146,12 +147,12 @@ describe('CreateUserModal', () => { mockAxiosResolve({ id: 'e3515ffb-d264-4dfc-8506-6c239f6673b5' }) ) - const eventSpy = vi.spyOn(eventBus, 'publish') await wrapper.vm.onConfirm() + const { upsertUser } = useUserSettingsStore() + expect(upsertUser).toHaveBeenCalled() const { showMessage } = useMessages() expect(showMessage).toHaveBeenCalled() - expect(eventSpy).toHaveBeenCalled() }) it('should show message on error', async () => { diff --git a/packages/web-app-admin-settings/tests/unit/components/Users/LoginModal.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Users/LoginModal.spec.ts index 7cf83ae49fc..c8971d9429b 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Users/LoginModal.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Users/LoginModal.spec.ts @@ -7,8 +7,9 @@ import { } from 'web-test-helpers' import { mock } from 'vitest-mock-extended' import { User } from '@ownclouders/web-client/src/generated' -import { Modal, eventBus, useMessages } from '@ownclouders/web-pkg' +import { Modal, useMessages } from '@ownclouders/web-pkg' import { OcSelect } from 'design-system/src/components' +import { useUserSettingsStore } from '../../../../src/composables/stores/userSettings' describe('LoginModal', () => { it('renders the input including two options', () => { @@ -32,12 +33,11 @@ describe('LoginModal', () => { mockAxiosResolve({ id: 'e3515ffb-d264-4dfc-8506-6c239f6673b5' }) ) - const eventSpy = vi.spyOn(eventBus, 'publish') - await wrapper.vm.onConfirm() const { showMessage } = useMessages() expect(showMessage).toHaveBeenCalled() - expect(eventSpy).toHaveBeenCalled() + const { upsertUser } = useUserSettingsStore() + expect(upsertUser).toHaveBeenCalledTimes(users.length) expect(mocks.$clientService.graphAuthenticated.users.editUser).toHaveBeenCalledTimes( users.length ) diff --git a/packages/web-app-admin-settings/tests/unit/components/Users/RemoveFromGroupsModal.spec.ts b/packages/web-app-admin-settings/tests/unit/components/Users/RemoveFromGroupsModal.spec.ts index 6b98ec4e008..b2cdfecea61 100644 --- a/packages/web-app-admin-settings/tests/unit/components/Users/RemoveFromGroupsModal.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/components/Users/RemoveFromGroupsModal.spec.ts @@ -8,6 +8,7 @@ import { import { mock } from 'vitest-mock-extended' import { Group, User } from '@ownclouders/web-client/src/generated' import { Modal, eventBus, useMessages } from '@ownclouders/web-pkg' +import { useUserSettingsStore } from '../../../../src/composables/stores/userSettings' describe('RemoveFromGroupsModal', () => { it('renders the input', () => { @@ -29,12 +30,12 @@ describe('RemoveFromGroupsModal', () => { ) wrapper.vm.selectedOptions = groups - const eventSpy = vi.spyOn(eventBus, 'publish') await wrapper.vm.onConfirm() const { showMessage } = useMessages() expect(showMessage).toHaveBeenCalled() - expect(eventSpy).toHaveBeenCalled() + const { upsertUser } = useUserSettingsStore() + expect(upsertUser).toHaveBeenCalledTimes(users.length) }) it('should show message on error', async () => { @@ -55,7 +56,8 @@ describe('RemoveFromGroupsModal', () => { await wrapper.vm.onConfirm() const { showErrorMessage } = useMessages() expect(showErrorMessage).toHaveBeenCalled() - expect(eventSpy).not.toHaveBeenCalled() + const { upsertUser } = useUserSettingsStore() + expect(upsertUser).not.toHaveBeenCalled() }) }) }) 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 b9af2f18035..df07bd7604d 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 @@ -2,6 +2,7 @@ import UsersList from '../../../../src/components/Users/UsersList.vue' import { defaultComponentMocks, defaultPlugins, mount, shallowMount } from 'web-test-helpers' import { displayPositionedDropdown, eventBus, queryItemAsString } from '@ownclouders/web-pkg' import { SideBarEventTopics } from '@ownclouders/web-pkg' +import { useUserSettingsStore } from '../../../../src/composables/stores/userSettings' const getUserMocks = () => [{ id: '1', displayName: 'jan' }] vi.mock('@ownclouders/web-pkg', async (importOriginal) => ({ @@ -14,19 +15,15 @@ describe('UsersList', () => { describe('computed method "allUsersSelected"', () => { it('should be true if all users are selected', () => { const { wrapper } = getWrapper({ - props: { - users: getUserMocks(), - selectedUsers: getUserMocks() - } + users: getUserMocks(), + selectedUsers: getUserMocks() }) expect(wrapper.vm.allUsersSelected).toBeTruthy() }) it('should be false if not every user is selected', () => { const { wrapper } = getWrapper({ - props: { - users: getUserMocks(), - selectedUsers: [] - } + users: getUserMocks(), + selectedUsers: [] }) expect(wrapper.vm.allUsersSelected).toBeFalsy() }) @@ -87,51 +84,87 @@ describe('UsersList', () => { ]) }) }) - it('emits events on row click', () => { - const users = getUserMocks() - const { wrapper } = getWrapper({ props: { users } }) - wrapper.vm.rowClicked([users[0]]) - expect(wrapper.emitted('toggleSelectUser')).toBeTruthy() - }) it('should show the context menu on right click', async () => { const users = getUserMocks() const spyDisplayPositionedDropdown = vi.mocked(displayPositionedDropdown) - const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + const { wrapper } = getWrapper({ mountType: mount, 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 = vi.mocked(displayPositionedDropdown) - const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + const { wrapper } = getWrapper({ mountType: mount, 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 = vi.spyOn(eventBus, 'publish') - const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + const { wrapper } = getWrapper({ mountType: mount, 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 = vi.spyOn(eventBus, 'publish') - const { wrapper } = getWrapper({ mountType: mount, props: { users } }) + const { wrapper } = getWrapper({ mountType: mount, users }) await wrapper.find('.users-table-btn-edit').trigger('click') expect(eventBusSpy).toHaveBeenCalledWith(SideBarEventTopics.openWithPanel, 'EditPanel') }) + describe('toggle selection', () => { + describe('selectUsers method', () => { + it('selects all users', async () => { + const users = getUserMocks() + const { wrapper } = getWrapper({ mountType: shallowMount, users }) + wrapper.vm.selectUsers(users) + const { setSelectedUsers } = useUserSettingsStore() + expect(setSelectedUsers).toHaveBeenCalledWith(users) + }) + }) + describe('selectUsers method', () => { + it('selects a user', async () => { + const users = getUserMocks() + const { wrapper } = getWrapper({ mountType: shallowMount, users: [users[0]] }) + wrapper.vm.selectUser(users[0]) + const { addSelectedUser } = useUserSettingsStore() + expect(addSelectedUser).toHaveBeenCalledWith(users[0]) + }) + it('de-selects a selected user', async () => { + const users = getUserMocks() + const { wrapper } = getWrapper({ + mountType: shallowMount, + users: [users[0]], + selectedUsers: [users[0]] + }) + wrapper.vm.selectUser(users[0]) + const { setSelectedUsers } = useUserSettingsStore() + expect(setSelectedUsers).toHaveBeenCalledWith([]) + }) + }) + describe('unselectAllUsers method', () => { + it('de-selects all selected users', async () => { + const users = getUserMocks() + const { wrapper } = getWrapper({ + mountType: shallowMount, + users: [users[0]], + selectedUsers: [users[0]] + }) + wrapper.vm.unselectAllUsers() + const { setSelectedUsers } = useUserSettingsStore() + expect(setSelectedUsers).toHaveBeenCalledWith([]) + }) + }) + }) }) -function getWrapper({ mountType = shallowMount, props = {} } = {}) { +function getWrapper({ mountType = shallowMount, users = [], selectedUsers = [] } = {}) { vi.mocked(queryItemAsString).mockImplementationOnce(() => '1') vi.mocked(queryItemAsString).mockImplementationOnce(() => '100') const mocks = defaultComponentMocks() return { wrapper: mountType(UsersList, { props: { - users: [], - selectedUsers: [], roles: [ { displayName: 'Admin', @@ -150,11 +183,16 @@ function getWrapper({ mountType = shallowMount, props = {} } = {}) { id: '4' } ], - headerPosition: 0, - ...props + headerPosition: 0 }, global: { - plugins: [...defaultPlugins()], + plugins: [ + ...defaultPlugins({ + piniaOptions: { + userSettingsStore: { users, selectedUsers } + } + }) + ], mocks, provide: mocks, stubs: { diff --git a/packages/web-app-admin-settings/tests/unit/composables/actions/groups/useGroupActionsDelete.spec.ts b/packages/web-app-admin-settings/tests/unit/composables/actions/groups/useGroupActionsDelete.spec.ts index 6623b7a2d9b..6e9dd492b9c 100644 --- a/packages/web-app-admin-settings/tests/unit/composables/actions/groups/useGroupActionsDelete.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/composables/actions/groups/useGroupActionsDelete.spec.ts @@ -2,8 +2,8 @@ import { useGroupActionsDelete } from '../../../../../src/composables/actions/gr import { mock } from 'vitest-mock-extended' import { unref } from 'vue' import { Group } from '@ownclouders/web-client/src/generated' -import { eventBus } from '@ownclouders/web-pkg' import { defaultComponentMocks, getComposableWrapper } from 'web-test-helpers' +import { useGroupSettingsStore } from '../../../../../src/composables' describe('useGroupActionsDelete', () => { describe('method "isVisible"', () => { @@ -32,26 +32,26 @@ describe('useGroupActionsDelete', () => { }) describe('method "deleteGroups"', () => { it('should successfully delete all given gropups and reload the groups list', () => { - const eventSpy = vi.spyOn(eventBus, 'publish') getWrapper({ setup: async ({ deleteGroups }, { clientService }) => { const group = mock({ id: '1' }) await deleteGroups([group]) expect(clientService.graphAuthenticated.groups.deleteGroup).toHaveBeenCalledWith(group.id) - expect(eventSpy).toHaveBeenCalledWith('app.admin-settings.list.load') + const { removeGroups } = useGroupSettingsStore() + expect(removeGroups).toHaveBeenCalled() } }) }) it('should handle errors', () => { vi.spyOn(console, 'error').mockImplementation(() => undefined) - const eventSpy = vi.spyOn(eventBus, 'publish') getWrapper({ setup: async ({ deleteGroups }, { clientService }) => { clientService.graphAuthenticated.groups.deleteGroup.mockRejectedValue({}) const group = mock({ id: '1' }) await deleteGroups([group]) expect(clientService.graphAuthenticated.groups.deleteGroup).toHaveBeenCalledWith(group.id) - expect(eventSpy).toHaveBeenCalledWith('app.admin-settings.list.load') + const { removeGroups } = useGroupSettingsStore() + expect(removeGroups).toHaveBeenCalled() } }) }) diff --git a/packages/web-app-admin-settings/tests/unit/composables/actions/users/useUserActionsDelete.spec.ts b/packages/web-app-admin-settings/tests/unit/composables/actions/users/useUserActionsDelete.spec.ts index 89525a20e2b..c8018379c42 100644 --- a/packages/web-app-admin-settings/tests/unit/composables/actions/users/useUserActionsDelete.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/composables/actions/users/useUserActionsDelete.spec.ts @@ -2,8 +2,9 @@ import { useUserActionsDelete } from '../../../../../src/composables/actions/use import { mock } from 'vitest-mock-extended' import { unref } from 'vue' import { User } from '@ownclouders/web-client/src/generated' -import { eventBus, useCapabilityStore } from '@ownclouders/web-pkg' +import { useCapabilityStore } from '@ownclouders/web-pkg' import { defaultComponentMocks, getComposableWrapper, writable } from 'web-test-helpers' +import { useUserSettingsStore } from '../../../../../src/composables/stores/userSettings' describe('useUserActionsDelete', () => { describe('method "isVisible"', () => { @@ -27,26 +28,26 @@ describe('useUserActionsDelete', () => { }) describe('method "deleteUsers"', () => { it('should successfully delete all given users and reload the users list', () => { - const eventSpy = vi.spyOn(eventBus, 'publish') getWrapper({ setup: async ({ deleteUsers }, { clientService }) => { const user = mock({ id: '1' }) await deleteUsers([user]) expect(clientService.graphAuthenticated.users.deleteUser).toHaveBeenCalledWith(user.id) - expect(eventSpy).toHaveBeenCalledWith('app.admin-settings.list.load') + const { removeUsers } = useUserSettingsStore() + expect(removeUsers).toHaveBeenCalled() } }) }) it('should handle errors', () => { vi.spyOn(console, 'error').mockImplementation(() => undefined) - const eventSpy = vi.spyOn(eventBus, 'publish') getWrapper({ setup: async ({ deleteUsers }, { clientService }) => { clientService.graphAuthenticated.users.deleteUser.mockRejectedValue({}) const user = mock({ id: '1' }) await deleteUsers([user]) expect(clientService.graphAuthenticated.users.deleteUser).toHaveBeenCalledWith(user.id) - expect(eventSpy).toHaveBeenCalledWith('app.admin-settings.list.load') + const { removeUsers } = useUserSettingsStore() + expect(removeUsers).toHaveBeenCalled() } }) }) diff --git a/packages/web-app-admin-settings/tests/unit/views/Groups.spec.ts b/packages/web-app-admin-settings/tests/unit/views/Groups.spec.ts index b67e880ac19..e2195b25bfc 100644 --- a/packages/web-app-admin-settings/tests/unit/views/Groups.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/views/Groups.spec.ts @@ -1,9 +1,8 @@ import Groups from '../../../src/views/Groups.vue' -import { mockAxiosResolve, mockAxiosReject } from 'web-test-helpers/src/mocks' -import { mock, mockDeep } from 'vitest-mock-extended' -import { ClientService, eventBus, useMessages } from '@ownclouders/web-pkg' +import { mockAxiosResolve } from 'web-test-helpers/src/mocks' +import { mockDeep } from 'vitest-mock-extended' +import { ClientService } from '@ownclouders/web-pkg' import { defaultComponentMocks, defaultPlugins, mount } from 'web-test-helpers' -import { Group } from '@ownclouders/web-client/src/generated' const selectors = { batchActionsStub: 'batch-actions-stub' } const getClientServiceMock = () => { @@ -19,45 +18,9 @@ vi.mock('@ownclouders/web-pkg', async (importOriginal) => ({ })) describe('Groups view', () => { - describe('method "onEditGroup"', () => { - it('should emit event on success', async () => { - const clientService = getClientServiceMock() - clientService.graphAuthenticated.groups.editGroup.mockResolvedValue(mockAxiosResolve()) - clientService.graphAuthenticated.groups.getGroup.mockResolvedValue( - mockAxiosResolve({ id: '1', displayName: 'administrators' }) - ) - const { wrapper } = getWrapper({ clientService }) - - const editGroup = { - id: '1', - name: 'administrators' - } - - const busStub = vi.spyOn(eventBus, 'publish') - await wrapper.vm.loadResourcesTask.last - - const updatedGroup = await wrapper.vm.onEditGroup(editGroup) - - expect(updatedGroup.id).toEqual('1') - expect(updatedGroup.displayName).toEqual('administrators') - expect(busStub).toHaveBeenCalled() - }) - - it('should show message on error', async () => { - vi.spyOn(console, 'error').mockImplementation(() => undefined) - const clientService = getClientServiceMock() - clientService.graphAuthenticated.groups.editGroup.mockImplementation(() => mockAxiosReject()) - const { wrapper } = getWrapper({ clientService }) - await wrapper.vm.onEditGroup({}) - - const { showErrorMessage } = useMessages() - expect(showErrorMessage).toHaveBeenCalled() - }) - }) - describe('computed method "sideBarAvailablePanels"', () => { describe('EditPanel', () => { - it('should be available when one group is selected', () => { + it('should be available when one group is selected', async () => { const { wrapper } = getWrapper() expect( wrapper.vm.sideBarAvailablePanels @@ -101,29 +64,37 @@ describe('Groups view', () => { expect(wrapper.find(selectors.batchActionsStub).exists()).toBeFalsy() }) it('display when one group selected', async () => { - const { wrapper } = getWrapper() + const { wrapper } = getWrapper({ selectedGroups: [{ id: '1' }] }) await wrapper.vm.loadResourcesTask.last - wrapper.vm.toggleSelectGroup({ id: '1' }) await wrapper.vm.$nextTick() expect(wrapper.find(selectors.batchActionsStub).exists()).toBeTruthy() }) it('display when more than one groups selected', async () => { - const { wrapper } = getWrapper() + const { wrapper } = getWrapper({ selectedGroups: [{ id: '1' }, { id: '2' }] }) await wrapper.vm.loadResourcesTask.last - wrapper.vm.selectGroups([mock({ groupTypes: [] }), mock({ groupTypes: [] })]) await wrapper.vm.$nextTick() expect(wrapper.find(selectors.batchActionsStub).exists()).toBeTruthy() }) }) }) -function getWrapper({ clientService = getClientServiceMock() } = {}) { +function getWrapper({ + clientService = getClientServiceMock(), + groups = [], + selectedGroups = [] +} = {}) { const mocks = { ...defaultComponentMocks(), $clientService: clientService } return { wrapper: mount(Groups, { global: { - plugins: [...defaultPlugins()], + plugins: [ + ...defaultPlugins({ + piniaOptions: { + groupSettingsStore: { groups, selectedGroups } + } + }) + ], mocks, provide: mocks, stubs: { diff --git a/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts b/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts index 98d2b38033c..04048fca71d 100644 --- a/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/views/Spaces.spec.ts @@ -1,6 +1,6 @@ import { mockAxiosResolve } from 'web-test-helpers/src/mocks' -import { Graph } from '@ownclouders/web-client' -import { mockDeep } from 'vitest-mock-extended' +import { Graph, SpaceResource } from '@ownclouders/web-client' +import { mock, mockDeep } from 'vitest-mock-extended' import { ClientService, useAppDefaults } from '@ownclouders/web-pkg' import { defaultComponentMocks, defaultPlugins, mount } from 'web-test-helpers' import Spaces from '../../../src/views/Spaces.vue' @@ -28,7 +28,8 @@ describe('Spaces view', () => { expect(wrapper.find(selectors.loadingSpinnerStub).exists()).toBeTruthy() }) it('should render spaces list after loading has been finished', async () => { - const { wrapper } = getWrapper() + const spaces = [{ id: '1', name: 'Some Space' }] + const { wrapper } = getWrapper({ spaces }) await wrapper.vm.loadResourcesTask.last expect(wrapper.html()).toMatchSnapshot() expect(wrapper.find(selectors.spacesListStub).exists()).toBeTruthy() @@ -41,46 +42,6 @@ describe('Spaces view', () => { await wrapper.vm.loadResourcesTask.last expect(wrapper.find(selectors.noContentMessageStub).exists()).toBeTruthy() }) - describe('toggle selection', () => { - describe('selectSpaces method', () => { - it('selects all spaces', async () => { - const spaces = [{ name: 'Some Space' }, { name: 'Some other Space' }] - const { wrapper } = getWrapper({ spaces }) - await wrapper.vm.loadResourcesTask.last - wrapper.vm.selectSpaces(spaces) - expect(wrapper.vm.selectedSpaces.length).toBe(spaces.length) - }) - }) - describe('toggleSelectSpace method', () => { - it('selects a space', async () => { - const spaces = [{ name: 'Some Space' }] - const { wrapper } = getWrapper() - await wrapper.vm.loadResourcesTask.last - wrapper.vm.toggleSelectSpace(spaces[0]) - expect(wrapper.vm.selectedSpaces).toEqual( - expect.arrayContaining([expect.objectContaining({ name: spaces[0].name })]) - ) - }) - it('de-selects a selected space', async () => { - const spaces = [{ name: 'Some Space' }] - const { wrapper } = getWrapper() - await wrapper.vm.loadResourcesTask.last - wrapper.vm.selectedSpaces = spaces - wrapper.vm.toggleSelectSpace(spaces[0]) - expect(wrapper.vm.selectedSpaces.length).toBe(0) - }) - }) - describe('unselectAllSpaces method', () => { - it('de-selects all selected spaces', async () => { - const spaces = [{ name: 'Some Space' }] - const { wrapper } = getWrapper({ spaces }) - await wrapper.vm.loadResourcesTask.last - wrapper.vm.selectedSpaces = spaces - wrapper.vm.unselectAllSpaces() - expect(wrapper.vm.selectedSpaces.length).toBe(0) - }) - }) - }) describe('batch actions', () => { it('do not display when no space selected', async () => { const { wrapper } = getWrapper() @@ -88,25 +49,34 @@ describe('Spaces view', () => { expect(wrapper.find(selectors.batchActionsStub).exists()).toBeFalsy() }) it('display when one space selected', async () => { - const spaces = [{ name: 'Some Space' }] - const { wrapper } = getWrapper({ spaces }) + const spaces = [{ id: '1', name: 'Some Space' }] + const { wrapper } = getWrapper({ spaces, selectedSpaces: spaces }) await wrapper.vm.loadResourcesTask.last - wrapper.vm.toggleSelectSpace(spaces[0]) await wrapper.vm.$nextTick() expect(wrapper.find(selectors.batchActionsStub).exists()).toBeTruthy() }) it('display when more than one space selected', async () => { - const spaces = [{ name: 'Some Space' }, { name: 'Some other Space' }] - const { wrapper } = getWrapper({ spaces }) + const spaces = [ + { id: '1', name: 'Some Space' }, + { id: '1', name: 'Some other Space' } + ] + const { wrapper } = getWrapper({ spaces, selectedSpaces: spaces }) await wrapper.vm.loadResourcesTask.last - wrapper.vm.selectSpaces(spaces) await wrapper.vm.$nextTick() expect(wrapper.find(selectors.batchActionsStub).exists()).toBeTruthy() }) }) }) -function getWrapper({ spaces = [{ name: 'Some Space' }] } = {}) { +function getWrapper({ + spaces = [ + { + id: '1', + name: 'space' + } + ], + selectedSpaces = [] +} = {}) { const $clientService = mockDeep() $clientService.graphAuthenticated.drives.listAllDrives.mockResolvedValue( mockAxiosResolve({ value: spaces }) @@ -119,7 +89,16 @@ function getWrapper({ spaces = [{ name: 'Some Space' }] } = {}) { return { wrapper: mount(Spaces, { global: { - plugins: [...defaultPlugins()], + plugins: [ + ...defaultPlugins({ + piniaOptions: { + spaceSettingsStore: { + spaces: spaces.map((s) => mock(s)), + selectedSpaces + } + } + }) + ], mocks, provide: mocks, stubs: { 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 9634648f2e9..88e864140cc 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 @@ -1,14 +1,6 @@ import Users from '../../../src/views/Users.vue' -import { - OptionsConfig, - UserAction, - eventBus, - useAppDefaults, - useMessages, - useSpacesStore -} from '@ownclouders/web-pkg' +import { OptionsConfig, UserAction, useAppDefaults } from '@ownclouders/web-pkg' import { mock, mockDeep } from 'vitest-mock-extended' -import { mockAxiosResolve, mockAxiosReject } from 'web-test-helpers/src/mocks' import { defaultComponentMocks, defaultPlugins, mount, shallowMount } from 'web-test-helpers' import { AxiosResponse } from 'axios' import { ClientService, queryItemAsString } from '@ownclouders/web-pkg' @@ -107,7 +99,7 @@ const selectors = { describe('Users view', () => { describe('list view', () => { it('renders list initially', async () => { - const { wrapper } = getMountedWrapper({ mountType: mount }) + const { wrapper } = getMountedWrapper({ mountType: mount, users: [getDefaultUser()] }) await wrapper.vm.loadResourcesTask.last expect(wrapper.html()).toMatchSnapshot() }) @@ -139,119 +131,6 @@ describe('Users view', () => { }) }) - describe('method "onEditUser"', () => { - it('should emit event on success', async () => { - const editUser = { - appRoleAssignments: [ - { - appRoleId: '2', - resourceId: 'some-graph-app-id', - principalId: '1' - } - ], - displayName: 'administrator', - id: '1', - mail: 'administrator@example.org', - memberOf: [ - { - displayName: 'admins', - id: '1' - } - ], - drive: { - id: '1', - name: 'admin', - quota: { - total: 1000000000 - } - }, - passwordProfile: { - password: 'administrator' - } - } - - const clientService = getClientService() - clientService.graphAuthenticated.users.editUser.mockResolvedValue(mockAxiosResolve()) - clientService.graphAuthenticated.users.createUserAppRoleAssignment.mockResolvedValue( - mockAxiosResolve() - ) - clientService.graphAuthenticated.groups.addMember.mockResolvedValue(mockAxiosResolve()) - clientService.graphAuthenticated.drives.updateDrive.mockResolvedValue( - mockAxiosResolve({ - id: '1', - name: 'admin', - quota: { remaining: 1000000000, state: 'normal', total: 1000000000, used: 0 } - }) - ) - clientService.graphAuthenticated.users.getUser.mockResolvedValue( - mockAxiosResolve({ - appRoleAssignments: [ - { - appRoleId: '2', - id: '1', - principalId: '1', - principalType: 'User', - resourceDisplayName: 'ownCloud Infinite Scale', - resourceId: 'some-graph-app-id' - } - ], - displayName: 'administrator', - drive: { - id: '1', - name: 'admin', - quota: { remaining: 1000000000, state: 'normal', total: 1000000000, used: 0 } - }, - id: '1', - mail: 'administrator@example.org', - memberOf: [ - { - displayName: 'admins', - id: '1' - } - ], - onPremisesSamAccountName: 'admin', - surname: 'Admin' - }) - ) - - const { wrapper } = getMountedWrapper({ clientService }) - - const busStub = vi.spyOn(eventBus, 'publish') - - await wrapper.vm.loadResourcesTask.last - - const userToUpDate = wrapper.vm.users.find((user) => user.id === '1') - const updatedUser = await wrapper.vm.onEditUser({ user: userToUpDate, editUser }) - - expect(updatedUser.id).toEqual('1') - expect(updatedUser.displayName).toEqual('administrator') - expect(updatedUser.mail).toEqual('administrator@example.org') - expect(updatedUser.appRoleAssignments[0].appRoleId).toEqual('2') - expect(updatedUser.drive.quota.total).toEqual(1000000000) - expect(updatedUser.memberOf[0].id).toEqual('1') - - expect(busStub).toHaveBeenCalled() - const spacesStore = useSpacesStore() - expect(spacesStore.updateSpaceField).toHaveBeenCalled() - }) - - it('should show message on error', async () => { - vi.spyOn(console, 'error').mockImplementation(() => undefined) - const clientService = getClientService() - clientService.graphAuthenticated.users.editUser.mockImplementation(() => mockAxiosReject()) - const { wrapper } = getMountedWrapper({ clientService }) - - await wrapper.vm.loadResourcesTask.last - await wrapper.vm.onEditUser({ - user: {}, - editUser: {} - }) - - const { showErrorMessage } = useMessages() - expect(showErrorMessage).toHaveBeenCalled() - }) - }) - describe('computed method "sideBarAvailablePanels"', () => { it('should contain EditPanel when one user is selected', () => { const { wrapper } = getMountedWrapper() @@ -286,14 +165,16 @@ describe('Users view', () => { expect(wrapper.find('batch-actions-stub').exists()).toBeFalsy() }) it('display when one user selected', async () => { - const { wrapper } = getMountedWrapper({ mountType: mount }) + const { wrapper } = getMountedWrapper({ mountType: mount, selectedUsers: [{ id: '1' }] }) 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 }) + const { wrapper } = getMountedWrapper({ + mountType: mount, + selectedUsers: [{ id: '1' }, { id: '2' }] + }) await wrapper.vm.loadResourcesTask.last wrapper.vm.selectUsers([mock(), mock()]) await wrapper.vm.$nextTick() @@ -394,7 +275,9 @@ function getMountedWrapper({ groupFilterQuery = null, roleFilterQuery = null, options = {}, - createUserActionEnabled = true + createUserActionEnabled = true, + users = [], + selectedUsers = [] }: { mountType?: typeof shallowMount | typeof mount clientService?: ReturnType> @@ -403,6 +286,8 @@ function getMountedWrapper({ roleFilterQuery?: string options?: OptionsConfig createUserActionEnabled?: boolean + users?: User[] + selectedUsers?: User[] } = {}) { vi.mocked(queryItemAsString).mockImplementationOnce(() => displayNameFilterQuery) vi.mocked(queryItemAsString).mockImplementationOnce(() => groupFilterQuery) @@ -428,7 +313,11 @@ function getMountedWrapper({ global: { plugins: [ ...defaultPlugins({ - piniaOptions: { userState: { user }, configState: { options } } + piniaOptions: { + userState: { user }, + configState: { options }, + userSettingsStore: { users, selectedUsers } + } }) ], mocks, diff --git a/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap b/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap index 1a8dd75c302..ec8176528b2 100644 --- a/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap +++ b/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Spaces.spec.ts.snap @@ -17,7 +17,7 @@ exports[`Spaces view > loading states > should render spaces list after loading
- +
diff --git a/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Users.spec.ts.snap b/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Users.spec.ts.snap index a5dc097951c..88dea88a55d 100644 --- a/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Users.spec.ts.snap +++ b/packages/web-app-admin-settings/tests/unit/views/__snapshots__/Users.spec.ts.snap @@ -118,37 +118,11 @@ exports[`Users view > list view > renders list initially 1`] = ` - + - function(...a) { - let r = v(t); - r.called = !0, r.callCount++, r.calls.push(a); - let i = r.next.shift(); - if (i) { - r.results.push(i); - let [s, l] = i; - if (s === "ok") - return l; - throw l; - } - let o, c = "ok"; - if (r.impl) - try { - new.target ? o = Reflect.construct(r.impl, a, new.target) : o = r.impl.apply(this, a), c = "ok"; - } catch (s) { - throw o = s, c = "error", r.results.push([c, s]), s; - } - let x = [c, o]; - if (b(o)) { - let s = o.then((l) => x[1] = l).catch((l) => { - throw x[0] = "error", x[1] = l, l; - }); - Object.assign(s, o), o = s; - } - return r.results.push(x), o; - } + Admin admin@example.org Admin diff --git a/packages/web-test-helpers/src/mocks/pinia.ts b/packages/web-test-helpers/src/mocks/pinia.ts index e495e033f3b..0e33c577b06 100644 --- a/packages/web-test-helpers/src/mocks/pinia.ts +++ b/packages/web-test-helpers/src/mocks/pinia.ts @@ -1,6 +1,6 @@ import { createTestingPinia } from '@pinia/testing' import defaultTheme from '../../../web-runtime/themes/owncloud/theme.json' -import { User } from '../../../web-client/src/generated' +import { User, Group } from '../../../web-client/src/generated' import { Message, Modal, WebThemeType } from '../../../web-pkg/src/composables/piniaStores' import { Capabilities } from '../../../web-client/src/ocs' import { mock } from 'vitest-mock-extended' @@ -32,6 +32,18 @@ export type PiniaMockOptions = { } messagesState?: { messages?: Message[] } modalsState?: { modals?: Modal[] } + spaceSettingsStore?: { + spaces?: SpaceResource[] + selectedSpaces?: SpaceResource[] + } + groupSettingsStore?: { + groups?: Group[] + selectedGroups?: Group[] + } + userSettingsStore?: { + users?: User[] + selectedUsers?: User[] + } resourcesStore?: { resources?: Resource[] currentFolder?: Resource @@ -58,6 +70,9 @@ export function createMockStore({ messagesState = {}, modalsState = {}, resourcesStore = {}, + userSettingsStore = {}, + groupSettingsStore = {}, + spaceSettingsStore = {}, sharesState = {}, spacesState = {}, userState = {}, @@ -105,7 +120,10 @@ export function createMockStore({ }, resources: { resources: [], ...resourcesStore }, shares: { shares: [], ...sharesState }, - spaces: { spaces: [], spaceMembers: [], ...spacesState }, + spaces: { spaces: [], spaceMembers: [], ...spacesState, ...spaceSettingsStore }, + userSettings: { users: [], selectedUsers: [], ...userSettingsStore }, + groupSettings: { groups: [], selectedGroups: [], ...groupSettingsStore }, + spaceSettings: { spaces: [], selectedSpaces: [], ...spaceSettingsStore }, user: { user: { ...mock({ id: '1' }), ...(userState?.user && { ...userState.user }) } }, capabilities: { isInitialized: capabilityState?.isInitialized ? capabilityState.isInitialized : true,