diff --git a/changelog/unreleased/enhancement-user-group-filter b/changelog/unreleased/enhancement-user-group-filter new file mode 100644 index 00000000000..67e9db579d9 --- /dev/null +++ b/changelog/unreleased/enhancement-user-group-filter @@ -0,0 +1,6 @@ +Enhancement: User group filter + +Users in the users list can now be filtered by their group assignments. + +https://github.com/owncloud/web/issues/8377 +https://github.com/owncloud/web/pull/8378 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 3e76dc46dea..ef8ea31cfbe 100644 --- a/packages/web-app-admin-settings/src/components/Groups/GroupsList.vue +++ b/packages/web-app-admin-settings/src/components/Groups/GroupsList.vue @@ -4,7 +4,7 @@ id="groups-filter" v-model="filterTerm" class="oc-ml-m oc-my-s" - :label="$gettext('Filter groups')" + :label="$gettext('Search')" autocomplete="off" /> + + + + , @@ -253,22 +261,17 @@ export default defineComponent({ }, watch: { filterTerm() { - if (!this.markInstance) { - return + if (this.$refs.tableRef) { + this.markInstance = new Mark(this.$refs.tableRef.$el) + this.markInstance.unmark() + this.markInstance.mark(this.filterTerm, { + element: 'span', + className: 'highlight-mark', + exclude: ['th *', 'tfoot *'] + }) } - this.markInstance.unmark() - this.markInstance.mark(this.filterTerm, { - element: 'span', - className: 'highlight-mark', - exclude: ['th *', 'tfoot *'] - }) } }, - mounted() { - this.$nextTick(() => { - this.markInstance = new Mark(this.$refs.tableRef.$el) - }) - }, methods: { filter(users, filterTerm) { if (!(filterTerm || '').trim()) { diff --git a/packages/web-app-admin-settings/src/views/Users.vue b/packages/web-app-admin-settings/src/views/Users.vue index 3faff4bf91e..c1b5d805e39 100644 --- a/packages/web-app-admin-settings/src/views/Users.vue +++ b/packages/web-app-admin-settings/src/views/Users.vue @@ -2,7 +2,6 @@
+
@@ -81,8 +96,13 @@ import DetailsPanel from '../components/Users/SideBar/DetailsPanel.vue' import EditPanel from '../components/Users/SideBar/EditPanel.vue' import BatchActions from 'web-pkg/src/components/BatchActions.vue' import Delete from '../mixins/users/delete' -import NoContentMessage from 'web-pkg/src/components/NoContentMessage.vue' -import { useAccessToken, useGraphClient, useStore } from 'web-pkg/src/composables' +import { + queryItemAsString, + useAccessToken, + useGraphClient, + useRouteQuery, + useStore +} from 'web-pkg/src/composables' import { computed, defineComponent, @@ -98,16 +118,19 @@ import { eventBus } from 'web-pkg/src/services/eventBus' import { mapActions, mapGetters, mapMutations, mapState } from 'vuex' import AppTemplate from '../components/AppTemplate.vue' import { useSideBar } from 'web-pkg/src/composables/sideBar' +import ItemFilter from 'web-pkg/src/components/ItemFilter.vue' +import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue' export default defineComponent({ name: 'UsersView', components: { + AppLoadingSpinner, AppTemplate, UsersList, - NoContentMessage, CreateUserModal, BatchActions, - ContextActions + ContextActions, + ItemFilter }, mixins: [Delete], setup() { @@ -128,6 +151,8 @@ export default defineComponent({ let loadResourcesEventToken let userUpdatedEventToken + const groupFilterParam = useRouteQuery('q_groups') + const loadGroupsTask = useTask(function* (signal) { const groupsResponse = yield unref(graphClient).groups.listGroups() groups.value = groupsResponse.data.value @@ -138,10 +163,14 @@ export default defineComponent({ roles.value = applicationsResponse.data.value[0].appRoles }) - const loadResourcesTask = useTask(function* (signal) { - const usersResponse = yield unref(graphClient).users.listUsers('displayName') + const loadUsersTask = useTask(function* (signal, groupIds) { + const groupFilter = groupIds?.map((id) => `memberOf/any(m:m/id eq '${id}')`).join(' and ') + const usersResponse = yield unref(graphClient).users.listUsers('displayName', groupFilter) users.value = usersResponse.data.value || [] + }) + const loadResourcesTask = useTask(function* (signal, groupIds = null) { + yield loadUsersTask.perform(groupIds) yield loadGroupsTask.perform() yield loadAppRolesTask.perform() }) @@ -156,6 +185,11 @@ export default defineComponent({ return data }) + const filterGroups = (groups) => { + const groupIds = groups.map((g) => g.id) + loadUsersTask.perform(groupIds) + } + watch( selectedUsers, async () => { @@ -189,7 +223,8 @@ export default defineComponent({ }) onMounted(async () => { - await loadResourcesTask.perform() + const groupFilterIds = queryItemAsString(unref(groupFilterParam))?.split('+') + await loadResourcesTask.perform(groupFilterIds) loadResourcesEventToken = eventBus.subscribe('app.admin-settings.list.load', () => { loadResourcesTask.perform() selectedUsers.value = [] @@ -225,7 +260,8 @@ export default defineComponent({ accessToken, listHeaderPosition, createUserModalOpen, - batchActions + batchActions, + filterGroups } }, computed: { 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 3a4b7ad2574..f256283eb2a 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 @@ -3,7 +3,7 @@ exports[`SpacesList should render all spaces in a table 1`] = `
- +
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 3dbc6716744..9fb04d68456 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 @@ -12,6 +12,7 @@ import { shallowMount } from 'web-test-helpers' import { AxiosResponse } from 'axios' +import { queryItemAsString } from 'web-pkg' jest.mock('web-pkg/src/composables/appDefaults') @@ -82,6 +83,10 @@ const getDefaultGraphMock = () => { return graph } +const selectors = { + itemFilterGroupsStub: 'item-filter-stub[filtername="groups"]' +} + describe('Users view', () => { describe('method "createUser"', () => { it('should hide the modal and show message on success', async () => { @@ -325,9 +330,48 @@ describe('Users view', () => { expect(wrapper.find('batch-actions-stub').exists()).toBeTruthy() }) }) + + describe('filter', () => { + it('does filter users by groups when the "selectionChange"-event is triggered', async () => { + const graphMock = getDefaultGraphMock() + const { wrapper } = getMountedWrapper({ mountType: mount, graph: graphMock }) + await wrapper.vm.loadResourcesTask.last + expect(graphMock.users.listUsers).toHaveBeenCalledTimes(1) + ;(wrapper.findComponent(selectors.itemFilterGroupsStub).vm as any).$emit( + 'selectionChange', + [{ id: '1' }] + ) + await wrapper.vm.$nextTick() + expect(graphMock.users.listUsers).toHaveBeenCalledTimes(2) + expect(graphMock.users.listUsers).toHaveBeenNthCalledWith( + 2, + 'displayName', + "memberOf/any(m:m/id eq '1')" + ) + }) + it('does filter initially if group ids are given via query param', async () => { + const groupIdsQueryParam = '1+2' + const graphMock = getDefaultGraphMock() + const { wrapper } = getMountedWrapper({ + mountType: mount, + graph: graphMock, + queryItem: groupIdsQueryParam + }) + await wrapper.vm.loadResourcesTask.last + expect(graphMock.users.listUsers).toHaveBeenCalledWith( + 'displayName', + "memberOf/any(m:m/id eq '1') and memberOf/any(m:m/id eq '2')" + ) + }) + }) }) -function getMountedWrapper({ mountType = shallowMount, graph = getDefaultGraphMock() } = {}) { +function getMountedWrapper({ + mountType = shallowMount, + graph = getDefaultGraphMock(), + queryItem = null +} = {}) { + jest.mocked(queryItemAsString).mockImplementation(() => queryItem) const mocks = { ...defaultComponentMocks() } @@ -351,9 +395,9 @@ function getMountedWrapper({ mountType = shallowMount, graph = getDefaultGraphMo stubs: { CreateUserModal: true, AppLoadingSpinner: true, - NoContentMessage: true, - UsersList: true, OcBreadcrumb: true, + OcTable: true, + ItemFilter: true, BatchActions: true } } diff --git a/packages/web-client/src/graph.ts b/packages/web-client/src/graph.ts index 7e187a1ac46..98eec837ea6 100644 --- a/packages/web-client/src/graph.ts +++ b/packages/web-client/src/graph.ts @@ -50,7 +50,7 @@ export interface Graph { changeOwnPassword: (currentPassword: string, newPassword: string) => AxiosPromise editUser: (userId: string, user: User) => AxiosPromise deleteUser: (userId: string) => AxiosPromise - listUsers: (orderBy?: string) => AxiosPromise + listUsers: (orderBy?: string, filter?: string) => AxiosPromise createUserAppRoleAssignment: ( userId: string, appRoleAssignment: AppRoleAssignment @@ -131,12 +131,12 @@ export const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => { meChangepasswordApiFactory.changeOwnPassword({ currentPassword, newPassword }), editUser: (userId: string, user: User) => userApiFactory.updateUser(userId, user), deleteUser: (userId: string) => userApiFactory.deleteUser(userId), - listUsers: (orderBy?: any) => + listUsers: (orderBy?: any, filter?: string) => usersApiFactory.listUsers( 0, 0, '', - '', + filter, false, new Set([orderBy]), new Set([]), diff --git a/packages/web-pkg/src/components/ItemFilter.vue b/packages/web-pkg/src/components/ItemFilter.vue index cd84960a781..e93bb4c3a9b 100644 --- a/packages/web-pkg/src/components/ItemFilter.vue +++ b/packages/web-pkg/src/components/ItemFilter.vue @@ -1,5 +1,5 @@