diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 3d698810e..728a248fb 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -312,6 +312,7 @@ export default shield( currentUser: allow, Group: isAuthenticated, GroupMembers: isAllowedSeeingGroupMembers, + GroupCount: isAuthenticated, Post: allow, profilePagePosts: allow, Comment: allow, diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 5e22bd743..c6248c0c6 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -14,7 +14,9 @@ import { createOrUpdateLocations } from './users/location' export default { Query: { Group: async (_object, params, context, _resolveInfo) => { - const { isMember, id, slug } = params + const { isMember, id, slug, first, offset } = params + let pagination = '' + if (first !== undefined && offset !== undefined) pagination = `SKIP ${offset} LIMIT ${first}` const matchParams = { id, slug } removeUndefinedNullValuesFromObject(matchParams) const session = context.driver.session() @@ -27,6 +29,7 @@ export default { WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} + ${pagination} ` } else { if (isMember === false) { @@ -36,6 +39,7 @@ export default { WITH group WHERE group.groupType IN ['public', 'closed'] RETURN group {.*, myRole: NULL} + ${pagination} ` } else { groupCypher = ` @@ -44,6 +48,7 @@ export default { WITH group, membership WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner']) RETURN group {.*, myRole: membership.role} + ${pagination} ` } } @@ -81,6 +86,39 @@ export default { session.close() } }, + GroupCount: async (_object, params, context, _resolveInfo) => { + const { isMember } = params + const { + user: { id: userId }, + } = context + const session = context.driver.session() + const readTxResultPromise = session.readTransaction(async (txc) => { + let cypher + if (isMember) { + cypher = `MATCH (user:User)-[membership:MEMBER_OF]->(group:Group) + WHERE user.id = $userId + AND membership.role IN ['usual', 'admin', 'owner'] + RETURN toString(count(group)) AS count` + } else { + cypher = `MATCH (group:Group) + OPTIONAL MATCH (user:User)-[membership:MEMBER_OF]->(group) + WHERE user.id = $userId + WITH group, membership + WHERE group.groupType IN ['public', 'closed'] + OR membership.role IN ['usual', 'admin', 'owner'] + RETURN toString(count(group)) AS count` + } + const transactionResponse = await txc.run(cypher, { userId }) + return transactionResponse.records.map((record) => record.get('count')) + }) + try { + return parseInt(await readTxResultPromise) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Mutation: { CreateGroup: async (_parent, params, context, _resolveInfo) => { diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index c4890fdce..ce90fad1d 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -65,8 +65,8 @@ type Query { isMember: Boolean # if 'undefined' or 'null' then get all groups id: ID slug: String - # first: Int # not implemented yet - # offset: Int # not implemented yet + first: Int + offset: Int # orderBy: [_GroupOrdering] # not implemented yet # filter: _GroupFilter # not implemented yet ): [Group] @@ -79,6 +79,8 @@ type Query { # filter: _UserFilter # not implemented yet ): [User] + GroupCount(isMember: Boolean): Int + # AvailableGroupTypes: [GroupType]! # AvailableGroupActionRadii: [GroupActionRadius]! diff --git a/webapp/components/AvatarMenu/AvatarMenu.spec.js b/webapp/components/AvatarMenu/AvatarMenu.spec.js index 15f536ee7..5495c9ae6 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.spec.js +++ b/webapp/components/AvatarMenu/AvatarMenu.spec.js @@ -90,10 +90,10 @@ describe('AvatarMenu.vue', () => { expect(profileLink.exists()).toBe(true) }) - it('displays a link to "My groups"', () => { + it('displays a link to "Groups"', () => { const profileLink = wrapper .findAll('.ds-menu-item span') - .at(wrapper.vm.routes.findIndex((route) => route.path === '/my-groups')) + .at(wrapper.vm.routes.findIndex((route) => route.path === '/groups')) expect(profileLink.exists()).toBe(true) }) diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue index 5caec07f2..a5b56ba43 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.vue +++ b/webapp/components/AvatarMenu/AvatarMenu.vue @@ -77,8 +77,8 @@ export default { icon: 'user', }, { - name: this.$t('header.avatarMenu.myGroups'), - path: '/my-groups', + name: this.$t('header.avatarMenu.Groups'), + path: '/groups', icon: 'users', }, { diff --git a/webapp/components/Group/GroupButton.vue b/webapp/components/Group/GroupButton.vue index 2000e3046..db72467ef 100644 --- a/webapp/components/Group/GroupButton.vue +++ b/webapp/components/Group/GroupButton.vue @@ -1,5 +1,5 @@ diff --git a/webapp/components/Group/GroupForm.vue b/webapp/components/Group/GroupForm.vue index e0166bb45..6604a56d5 100644 --- a/webapp/components/Group/GroupForm.vue +++ b/webapp/components/Group/GroupForm.vue @@ -163,7 +163,7 @@ - + {{ $t('actions.cancel') }} diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index 5ee5869ce..e3510c74d 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -145,8 +145,8 @@ export const changeGroupMemberRoleMutation = () => { export const groupQuery = (i18n) => { const lang = i18n ? i18n.locale().toUpperCase() : 'EN' return gql` - query ($isMember: Boolean, $id: ID, $slug: String) { - Group(isMember: $isMember, id: $id, slug: $slug) { + query ($isMember: Boolean, $id: ID, $slug: String, $first: Int, $offset: Int) { + Group(isMember: $isMember, id: $id, slug: $slug, first: $first, offset: $offset) { id name slug @@ -190,3 +190,11 @@ export const groupMembersQuery = () => { } ` } + +export const groupCountQuery = () => { + return gql` + query ($isMember: Boolean) { + GroupCount(isMember: $isMember) + } + ` +} diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 67c88e4f4..39fd2de9f 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -407,6 +407,7 @@ "addMemberToGroup": "Zur Gruppe hinzufügen", "addUser": "Benutzer hinzufügen", "addUserPlaceholder": "eindeutiger Benutzername > @slug-from-user", + "allGroups": "Alle Gruppen", "categories": "Thema ::: Themen", "changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!", "contentMenu": { @@ -476,7 +477,7 @@ }, "header": { "avatarMenu": { - "myGroups": "Mein Gruppen", + "Groups": "Gruppen", "myProfile": "Mein Profil" } }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index ba1d65881..18b579df4 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -407,6 +407,7 @@ "addMemberToGroup": "Add to group", "addUser": "Add User", "addUserPlaceholder": "unique username > @slug-from-user", + "allGroups": "All Groups", "categories": "Topic ::: Topics", "changeMemberRole": "The role has been changed to “{role}”!", "contentMenu": { @@ -476,7 +477,7 @@ }, "header": { "avatarMenu": { - "myGroups": "My groups", + "Groups": "Groups", "myProfile": "My profile" } }, diff --git a/webapp/pages/group/create.vue b/webapp/pages/group/create.vue index b16cf933f..ebdbbe37c 100644 --- a/webapp/pages/group/create.vue +++ b/webapp/pages/group/create.vue @@ -58,7 +58,6 @@ export default { }, }) this.$toast.success(this.$t('group.groupCreated')) - // this.$router.history.push('/my-groups') this.$router.history.push({ name: 'group-id-slug', params: { id: responseId, slug: responseSlug }, diff --git a/webapp/pages/groups.spec.js b/webapp/pages/groups.spec.js new file mode 100644 index 000000000..0fbacd6b3 --- /dev/null +++ b/webapp/pages/groups.spec.js @@ -0,0 +1,35 @@ +import { config, mount } from '@vue/test-utils' +import groups from './groups.vue' + +const localVue = global.localVue + +config.stubs['nuxt-link'] = '' +config.stubs['client-only'] = '' + +describe('groups', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $t: jest.fn(), + } + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(groups, { + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('div')).toBe(true) + }) + }) +}) diff --git a/webapp/pages/groups.vue b/webapp/pages/groups.vue new file mode 100644 index 000000000..386905c5b --- /dev/null +++ b/webapp/pages/groups.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/webapp/pages/my-groups.vue b/webapp/pages/my-groups.vue deleted file mode 100644 index 7302b92ad..000000000 --- a/webapp/pages/my-groups.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - -