Merge pull request #6091 from Ocelot-Social-Community/filter-posts-in-my-groups

feat(backend): filter posts in my groups
This commit is contained in:
Moriz Wahl 2023-03-07 16:01:29 +01:00 committed by GitHub
commit 5538ef93db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 208 additions and 1 deletions

View File

@ -0,0 +1,40 @@
import { mergeWith, isArray } from 'lodash'
const getMyGroupIds = async (context) => {
const { user } = context
if (!(user && user.id)) return []
const session = context.driver.session()
const readTxResultPromise = await session.readTransaction(async (transaction) => {
const cypher = `
MATCH (group:Group)<-[membership:MEMBER_OF]-(:User { id: $userId })
WHERE membership.role IN ['usual', 'admin', 'owner']
RETURN collect(group.id) AS myGroupIds`
const getMyGroupIdsResponse = await transaction.run(cypher, { userId: user.id })
return getMyGroupIdsResponse.records.map((record) => record.get('myGroupIds'))
})
try {
const [myGroupIds] = readTxResultPromise
return myGroupIds
} finally {
session.close()
}
}
export const filterPostsOfMyGroups = async (params, context) => {
if (!(params.filter && params.filter.postsInMyGroups)) return params
delete params.filter.postsInMyGroups
const myGroupIds = await getMyGroupIds(context)
params.filter = mergeWith(
params.filter,
{
group: { id_in: myGroupIds },
},
(objValue, srcValue) => {
if (isArray(objValue)) {
return objValue.concat(srcValue)
}
},
)
return params
}

View File

@ -6,6 +6,7 @@ import { mergeImage, deleteImage } from './images/images'
import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
import CONFIG from '../../config'
const maintainPinnedPosts = (params) => {
@ -21,12 +22,14 @@ const maintainPinnedPosts = (params) => {
export default {
Query: {
Post: async (object, params, context, resolveInfo) => {
params = await filterPostsOfMyGroups(params, context)
params = await filterInvisiblePosts(params, context)
params = await filterForMutedUsers(params, context)
params = await maintainPinnedPosts(params)
return neo4jgraphql(object, params, context, resolveInfo)
},
profilePagePosts: async (object, params, context, resolveInfo) => {
params = await filterPostsOfMyGroups(params, context)
params = await filterInvisiblePosts(params, context)
params = await filterForMutedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo)

View File

@ -1678,5 +1678,59 @@ describe('Posts in Groups', () => {
})
})
})
describe('filter posts in my groups', () => {
describe('without any posts in groups', () => {
beforeAll(async () => {
authenticatedUser = await anyUser.toJson()
})
it('finds no posts', async () => {
const result = await query({
query: filterPosts(),
variables: { filter: { postsInMyGroups: true } },
})
expect(result.data.Post).toHaveLength(0)
expect(result).toMatchObject({
data: {
Post: [],
},
errors: undefined,
})
})
})
describe('with posts in groups', () => {
beforeAll(async () => {
// member of hidden-group and closed-group
authenticatedUser = await allGroupsUser.toJson()
})
it('finds two posts', async () => {
const result = await query({
query: filterPosts(),
variables: { filter: { postsInMyGroups: true } },
})
expect(result.data.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
{
id: 'post-to-closed-group',
title: 'A post to a closed group',
content: 'I am posting into a closed group as a member of the group',
},
{
id: 'post-to-hidden-group',
title: 'A post to a hidden group',
content: 'I am posting into a hidden group as a member of the group',
},
]),
},
errors: undefined,
})
})
})
})
})
})

View File

@ -82,6 +82,7 @@ input _PostFilter {
emotions_single: _PostEMOTEDFilter
emotions_every: _PostEMOTEDFilter
group: _GroupFilter
postsInMyGroups: Boolean
}
enum _PostOrdering {

View File

@ -9,12 +9,14 @@ let wrapper
describe('FollowingFilter', () => {
const mutations = {
'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'posts/TOGGLE_FILTER_BY_MY_GROUPS': jest.fn(),
}
const getters = {
'auth/user': () => {
return { id: 'u34' }
},
'posts/filteredByUsersFollowed': jest.fn(),
'posts/filteredByPostsInMyGroups': jest.fn(),
}
const mocks = {
@ -34,12 +36,18 @@ describe('FollowingFilter', () => {
describe('mount', () => {
it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
getters['posts/filteredByPostsInMyGroups'] = jest.fn(() => true)
const wrapper = Wrapper()
expect(
wrapper
.find('.following-filter .filter-list .follower-item .base-button')
.classes('--filled'),
).toBe(true)
expect(
wrapper
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
.classes('--filled'),
).toBe(true)
})
describe('click "filter-by-followed" button', () => {
@ -48,5 +56,14 @@ describe('FollowingFilter', () => {
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
})
})
describe('click "filter-by-my-groups" button', () => {
it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => {
wrapper
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
.trigger('click')
expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled()
})
})
})
})

View File

@ -10,6 +10,15 @@
@click="toggleFilteredByFollowed(currentUser.id)"
/>
</li>
<li class="item posts-in-my-groups-item">
<labeled-button
icon="users"
:label="$t('filter-menu.my-groups')"
:filled="filteredByPostsInMyGroups"
:title="$t('contribution.filterMyGroups')"
@click="toggleFilteredByMyGroups()"
/>
</li>
</template>
</filter-menu-section>
</template>
@ -28,12 +37,14 @@ export default {
computed: {
...mapGetters({
filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByPostsInMyGroups: 'posts/filteredByPostsInMyGroups',
currentUser: 'auth/user',
}),
},
methods: {
...mapMutations({
toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
toggleFilteredByMyGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS',
}),
},
}

View File

@ -270,9 +270,11 @@
"filterFollow": "Beiträge von Nutzern filtern, denen ich folge",
"filterMasonryGrid": {
"myFriends": "Nutzer denen ich folge",
"myGroups": "Meine Gruppen",
"myTopics": "Meine Themen",
"noFilter": "Beiträge filtern"
},
"filterMyGroups": "Beiträge in meinen Gruppen",
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
"languageSelectLabel": "Sprache Deines Beitrags",
"languageSelectText": "Sprache wählen",
@ -380,6 +382,7 @@
"filter-by": "Filtern nach ...",
"following": "Nutzer denen ich folge",
"languages": "Sprachen",
"my-groups": "Meinen Gruppen",
"order": {
"newest": {
"hint": "Sortiere die Neuesten nach vorn",

View File

@ -270,9 +270,11 @@
"filterFollow": "Filter contributions from users I follow",
"filterMasonryGrid": {
"myFriends": "Users I follow",
"myGroups": "My groups",
"myTopics": "My topics",
"noFilter": "Filter posts"
},
"filterMyGroups": "Contributions in my groups",
"inappropriatePicture": "This image may be inappropriate for some people.",
"languageSelectLabel": "Language of your contribution",
"languageSelectText": "Select Language",
@ -380,6 +382,7 @@
"filter-by": "Filter by ...",
"following": "Users I follow",
"languages": "Languages",
"my-groups": "My groups",
"order": {
"newest": {
"hint": "Sort posts by the newest first",

View File

@ -30,7 +30,11 @@
<div class="filterButtonMenu" :class="{ 'hide-filter': hideByScroll }">
<base-button
class="my-filter-button"
v-if="!postsFilter['categories_some'] && !postsFilter['author']"
v-if="
!postsFilter['categories_some'] &&
!postsFilter['author'] &&
!postsFilter['postsInMyGroups']
"
right
@click="showFilter = !showFilter"
filled
@ -66,6 +70,20 @@
/>
</span>
<span v-if="postsFilter['postsInMyGroups']">
<base-button class="my-filter-button" right @click="showFilter = !showFilter" filled>
{{ $t('contribution.filterMasonryGrid.myGroups') }}
</base-button>
<base-button
class="filter-remove"
@click="resetByGroups"
icon="close"
:title="$t('filter-menu.deleteFilter')"
style="margin-left: -8px"
filled
/>
</span>
<div id="my-filter" v-if="showFilter">
<div @mouseleave="showFilter = false">
<filter-menu-component @showFilterMenu="showFilterMenu" />
@ -203,6 +221,7 @@ export default {
methods: {
...mapMutations({
resetByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
resetByGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS',
resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'posts/TOGGLE_CATEGORY',
}),

View File

@ -30,6 +30,18 @@ export const mutations = {
}
}
},
TOGGLE_FILTER_BY_MY_GROUPS(state) {
const filter = clone(state.filter)
if (get(filter, 'postsInMyGroups')) {
delete filter.postsInMyGroups
state.filter = filter
} else {
state.filter = {
...filter,
postsInMyGroups: true,
}
}
},
RESET_CATEGORIES(state) {
const filter = clone(state.filter)
delete filter.categories_some
@ -84,6 +96,9 @@ export const getters = {
filteredByUsersFollowed(state) {
return !!get(state.filter, 'author.followedBy_some.id')
},
filteredByPostsInMyGroups(state) {
return !!get(state.filter, 'postsInMyGroups')
},
filteredByEmotions(state) {
return get(state.filter, 'emotions_some.emotion_in') || []
},

View File

@ -56,6 +56,18 @@ describe('getters', () => {
})
})
describe('filteredByPostsInMyGroups', () => {
it('returns true if filter is set', () => {
state = { filter: { postsInMyGroups: true } }
expect(getters.filteredByPostsInMyGroups(state)).toBe(true)
})
it('returns false if filter is not set', () => {
state = { filter: { categories_some: { id_in: [23] } } }
expect(getters.filteredByUsersFollowed(state)).toBe(false)
})
})
describe('filteredByEmotions', () => {
it('returns an emotions array if filter is set', () => {
state = { filter: { emotions_some: { emotion_in: ['sad'] } } }
@ -230,6 +242,35 @@ describe('mutations', () => {
})
})
describe('TOGGLE_FILTER_BY_MY_GROUPS', () => {
beforeEach(() => {
testMutation = () => {
mutations.TOGGLE_FILTER_BY_MY_GROUPS(state)
return getters.filter(state)
}
})
describe('given empty filter', () => {
beforeEach(() => {
state = { filter: {} }
})
it('sets postsInMyGroups filter to true', () => {
expect(testMutation()).toEqual({ postsInMyGroups: true })
})
})
describe('already filtered', () => {
beforeEach(() => {
state = { filter: { postsInMyGroups: true } }
})
it('removes postsInMyGroups filter', () => {
expect(testMutation()).toEqual({})
})
})
})
describe('TOGGLE_ORDER', () => {
beforeEach(() => {
testMutation = (key) => {