Merge pull request #5582 from Ocelot-Social-Community/list-all-groups

feat: 🍰 List All Groups
This commit is contained in:
Moriz Wahl 2022-10-26 18:13:23 +02:00 committed by GitHub
commit a6517ca0d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 291 additions and 87 deletions

View File

@ -312,6 +312,7 @@ export default shield(
currentUser: allow,
Group: isAuthenticated,
GroupMembers: isAllowedSeeingGroupMembers,
GroupCount: isAuthenticated,
Post: allow,
profilePagePosts: allow,
Comment: allow,

View File

@ -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) => {

View File

@ -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]!

View File

@ -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)
})

View File

@ -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',
},
{

View File

@ -1,5 +1,5 @@
<template>
<div>
<nuxt-link to="/my-groups"><base-button icon="users" circle ghost /></nuxt-link>
<nuxt-link to="/groups"><base-button icon="users" circle ghost /></nuxt-link>
</div>
</template>

View File

@ -163,7 +163,7 @@
<!-- submit -->
<ds-space margin-top="large">
<nuxt-link to="/my-groups">
<nuxt-link to="/groups">
<ds-button>{{ $t('actions.cancel') }}</ds-button>
</nuxt-link>
<ds-button type="submit" icon="save" primary :disabled="checkFormError(errors)" fill>

View File

@ -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)
}
`
}

View File

@ -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"
}
},

View File

@ -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"
}
},

View File

@ -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 },

View File

@ -0,0 +1,35 @@
import { config, mount } from '@vue/test-utils'
import groups from './groups.vue'
const localVue = global.localVue
config.stubs['nuxt-link'] = '<span class="nuxt-link"><slot /></span>'
config.stubs['client-only'] = '<span class="client-only"><slot /></span>'
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)
})
})
})

192
webapp/pages/groups.vue Normal file
View File

@ -0,0 +1,192 @@
<template>
<div>
<ds-space margin="small">
<tab-navigation :tabs="tabOptions" :activeTab="tabActive" @switch-tab="handleTab" />
</ds-space>
<ds-space margin="large" />
<ds-container>
<!-- create group -->
<ds-space centered>
<nuxt-link :to="{ name: 'group-create' }">
<base-button
class="group-add-button"
icon="plus"
size="large"
circle
filled
v-tooltip="{
content: $t('group.createNewGroup.tooltip'),
placement: 'left',
}"
/>
</nuxt-link>
</ds-space>
<!-- group list -->
<ds-space centered v-if="showPagination">
<pagination-buttons
:hasNext="hasNext"
:showPageCounter="true"
:hasPrevious="hasPrevious"
:activePage="activePage"
:activeResourceCount="activeTab.count"
:key="'Top'"
:pageSize="pageSize"
@back="previousResults"
@next="nextResults"
/>
</ds-space>
<group-list :groups="myGroups" />
<ds-space centered v-if="showPagination">
<pagination-buttons
:hasNext="hasNext"
:showPageCounter="true"
:hasPrevious="hasPrevious"
:activePage="activePage"
:activeResourceCount="activeTab.count"
:key="'Bottom'"
:pageSize="pageSize"
@back="previousResults"
@next="nextResults"
/>
</ds-space>
</ds-container>
</div>
</template>
<script>
import GroupList from '~/components/Group/GroupList'
import { groupQuery, groupCountQuery } from '~/graphql/groups.js'
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
const tabToFilterMapping = (tab) => {
return {
myGroups: { isMember: true },
allGroups: {},
}[tab]
}
export default {
name: 'Groups',
components: {
GroupList,
TabNavigation,
PaginationButtons,
},
data() {
return {
Group: [],
groupFilter: { isMember: true },
tabActive: 'myGroups',
pageSize: 5,
activePage: 0,
myGroupsCount: 0,
allGroupsCount: 0,
}
},
methods: {
handleTab(tab) {
if (this.tabActive !== tab) {
this.tabActive = tab
this.activePage = 0
this.groupFilter = tabToFilterMapping(tab)
this.$apollo.queries.Group.refetch()
}
},
previousResults() {
this.activePage--
this.$apollo.queries.Group.refetch()
},
nextResults() {
this.activePage++
this.$apollo.queries.Group.refetch()
},
},
computed: {
activeTab() {
return this.tabOptions.find((tab) => tab.type === this.tabActive)
},
showPagination() {
return this.activeTab.count > this.pageSize
},
hasNext() {
return (this.activePage + 1) * this.pageSize < this.activeTab.count
},
hasPrevious() {
return this.activePage > 0
},
pagination() {
return {
first: this.pageSize,
offset: this.activePage * this.pageSize,
}
},
myGroups() {
return this.Group ? this.Group : []
},
tabOptions() {
return [
{
type: 'myGroups',
title: this.$t('group.myGroups'),
count: this.myGroupsCount,
disabled: this.myGroupsCount === 0,
},
{
type: 'allGroups',
title: this.$t('group.allGroups'),
count: this.allGroupsCount,
disabled: this.allGroupsCount === 0,
},
]
},
},
apollo: {
Group: {
query() {
return groupQuery(this.$i18n)
},
variables() {
return {
...this.groupFilter,
...this.pagination,
}
},
error(error) {
this.Group = []
this.$toast.error(error.message)
},
fetchPolicy: 'cache-and-network',
},
MyGroupsCount: {
query() {
return groupCountQuery()
},
variables() {
return {
isMember: true,
}
},
update({ GroupCount }) {
this.myGroupsCount = GroupCount
},
fetchPolicy: 'cache-and-network',
},
AllGroupsCount: {
query() {
return groupCountQuery()
},
update({ GroupCount }) {
this.allGroupsCount = GroupCount
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style lang="scss">
.group-add-button {
box-shadow: $box-shadow-x-large;
}
</style>

View File

@ -1,73 +0,0 @@
<template>
<div>
<ds-space margin="small">
<ds-heading tag="h1">{{ $t('group.myGroups') }}</ds-heading>
</ds-space>
<ds-space margin="large" />
<ds-container>
<!-- create group -->
<ds-space centered>
<nuxt-link :to="{ name: 'group-create' }">
<base-button
class="group-add-button"
icon="plus"
size="large"
circle
filled
v-tooltip="{
content: $t('group.createNewGroup.tooltip'),
placement: 'left',
}"
/>
</nuxt-link>
</ds-space>
<!-- group list -->
<group-list :groups="myGroups" />
</ds-container>
</div>
</template>
<script>
import GroupList from '~/components/Group/GroupList'
import { groupQuery } from '~/graphql/groups.js'
export default {
name: 'MyGroups',
components: {
GroupList,
},
data() {
return {
Group: [],
}
},
computed: {
myGroups() {
return this.Group ? this.Group : []
},
},
apollo: {
Group: {
query() {
return groupQuery(this.$i18n)
},
variables() {
return {
isMember: true,
}
},
error(error) {
this.Group = []
this.$toast.error(error.message)
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style lang="scss">
.group-add-button {
box-shadow: $box-shadow-x-large;
}
</style>