feat(backend): pin public group posts (#8606)

* feat(backend): pin more than one post

* add postPinnedCount query, better names for env variable

* add store and mixin for pinned posts counts

* test pinned post store

* context menu for pin posts

* fix typos

* unpin posts is always possible

* feat(backend): pin public group posts

* allow posts in public groups to be pinned by admins

---------

Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
Moriz Wahl 2025-05-28 19:52:30 +02:00 committed by GitHub
parent b736a2a2e3
commit fb2ef852a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 204 additions and 2 deletions

View File

@ -10,6 +10,7 @@ import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import Image from '@db/models/Image'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import { createPostMutation } from '@graphql/queries/createPostMutation'
import createServer, { getContext } from '@src/server'
@ -1305,6 +1306,130 @@ describe('pin posts', () => {
})
})
describe('post in public group', () => {
beforeEach(async () => {
await mutate({
mutation: createGroupMutation(),
variables: {
name: 'Public Group',
id: 'public-group',
about: 'This is a public group',
groupType: 'public',
actionRadius: 'regional',
description:
'This is a public group to test if the posts of this group can be pinned.',
categoryIds,
},
})
await mutate({
mutation: createPostMutation(),
variables: {
id: 'public-group-post',
title: 'Public group post',
content: 'This is a post in a public group',
groupId: 'public-group',
categoryIds,
},
})
variables = { ...variables, id: 'public-group-post' }
})
it('can be pinned', async () => {
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
data: {
pinPost: {
id: 'public-group-post',
author: {
slug: 'testuser',
},
pinnedBy: {
id: 'current-user',
name: 'Admin',
role: 'admin',
},
},
},
errors: undefined,
})
})
})
describe('post in closed group', () => {
beforeEach(async () => {
await mutate({
mutation: createGroupMutation(),
variables: {
name: 'Closed Group',
id: 'closed-group',
about: 'This is a closed group',
groupType: 'closed',
actionRadius: 'regional',
description:
'This is a closed group to test if the posts of this group can be pinned.',
categoryIds,
},
})
await mutate({
mutation: createPostMutation(),
variables: {
id: 'closed-group-post',
title: 'Closed group post',
content: 'This is a post in a closed group',
groupId: 'closed-group',
categoryIds,
},
})
variables = { ...variables, id: 'closed-group-post' }
})
it('can not be pinned', async () => {
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
data: {
pinPost: null,
},
errors: undefined,
})
})
})
describe('post in hidden group', () => {
beforeEach(async () => {
await mutate({
mutation: createGroupMutation(),
variables: {
name: 'Hidden Group',
id: 'hidden-group',
about: 'This is a hidden group',
groupType: 'hidden',
actionRadius: 'regional',
description:
'This is a hidden group to test if the posts of this group can be pinned.',
categoryIds,
},
})
await mutate({
mutation: createPostMutation(),
variables: {
id: 'hidden-group-post',
title: 'Hidden group post',
content: 'This is a post in a hidden group',
groupId: 'hidden-group',
categoryIds,
},
})
variables = { ...variables, id: 'hidden-group-post' }
})
it('can not be pinned', async () => {
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
data: {
pinPost: null,
},
errors: undefined,
})
})
})
describe('PostOrdering', () => {
beforeEach(async () => {
await Factory.build('post', {

View File

@ -351,7 +351,8 @@ export default {
const pinPostCypher = `
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
MATCH (post:Post {id: $params.id})
WHERE NOT((post)-[:IN]->(:Group))
WHERE NOT EXISTS((post)-[:IN]->(:Group)) OR
(post)-[:IN]->(:Group { groupType: 'public'})
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
SET post.pinned = true
RETURN post, pinned.createdAt as pinnedAt`

View File

@ -197,6 +197,79 @@ describe('ContentMenu.vue', () => {
],
])
})
describe('post in public group', () => {
it('can pin unpinned post', async () => {
getters['auth/isAdmin'] = () => true
const wrapper = await openContentMenu({
isOwner: false,
resourceType: 'contribution',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
pinnedBy: null,
group: {
groupType: 'public',
},
},
})
wrapper
.findAll('.ds-menu-item')
.filter((item) => item.text() === 'post.menu.pin')
.at(0)
.trigger('click')
expect(wrapper.emitted('pinPost')).toEqual([
[
{
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
pinnedBy: null,
group: {
groupType: 'public',
},
},
],
])
})
})
describe('post in closed group', () => {
it('can not be pinned', async () => {
getters['auth/isAdmin'] = () => true
const wrapper = await openContentMenu({
isOwner: false,
resourceType: 'contribution',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
pinnedBy: null,
group: {
groupType: 'closed',
},
},
})
expect(
wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'),
).toHaveLength(0)
})
})
describe('post in hidden group', () => {
it('can not be pinned', async () => {
getters['auth/isAdmin'] = () => true
const wrapper = await openContentMenu({
isOwner: false,
resourceType: 'contribution',
resource: {
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
pinnedBy: null,
group: {
groupType: 'hidden',
},
},
})
expect(
wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'),
).toHaveLength(0)
})
})
})
describe('when maxPinnedPosts = 3', () => {

View File

@ -82,7 +82,7 @@ export default {
})
}
if (this.isAdmin && !this.resource.group) {
if (this.isAdmin && (!this.resource.group || this.resource.group.groupType === 'public')) {
if (!this.resource.pinnedBy && this.canBePinned) {
routes.push({
label: this.$t(`post.menu.pin`),

View File

@ -53,6 +53,7 @@ export default (i18n) => {
id
name
slug
groupType
}
}
}
@ -95,6 +96,7 @@ export const filterPosts = (i18n) => {
id
name
slug
groupType
}
}
}
@ -136,6 +138,7 @@ export const profilePagePosts = (i18n) => {
id
name
slug
groupType
}
}
}