mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
feat(backend): pin more than one post (#8598)
* 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 --------- Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
parent
903ce7071f
commit
b736a2a2e3
@ -47,3 +47,4 @@ AWS_REGION=
|
||||
AWS_BUCKET=
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
MAX_PINNED_POSTS=1
|
||||
|
||||
@ -123,6 +123,9 @@ const options = {
|
||||
INVITE_CODES_GROUP_PER_USER:
|
||||
(env.INVITE_CODES_GROUP_PER_USER && parseInt(env.INVITE_CODES_GROUP_PER_USER)) || 7,
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
MAX_PINNED_POSTS: Number.isNaN(Number(process.env.MAX_PINNED_POSTS))
|
||||
? 1
|
||||
: Number(process.env.MAX_PINNED_POSTS),
|
||||
}
|
||||
|
||||
const language = {
|
||||
|
||||
@ -1096,8 +1096,20 @@ describe('pin posts', () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
})
|
||||
|
||||
describe('are allowed to pin posts', () => {
|
||||
const postOrderingQuery = gql`
|
||||
query ($orderBy: [_PostOrdering]) {
|
||||
Post(orderBy: $orderBy) {
|
||||
id
|
||||
pinned
|
||||
createdAt
|
||||
pinnedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('MAX_PINNED_POSTS is 0', () => {
|
||||
beforeEach(async () => {
|
||||
CONFIG.MAX_PINNED_POSTS = 0
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
@ -1110,217 +1122,458 @@ describe('pin posts', () => {
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
author: {
|
||||
name: 'Admin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets createdAt date for PINNED', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets redundant `pinned` property for performant ordering', async () => {
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
const expected = {
|
||||
data: { pinPost: { pinned: true } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created by another admin', () => {
|
||||
let otherAdmin
|
||||
beforeEach(async () => {
|
||||
otherAdmin = await Factory.build('user', {
|
||||
role: 'admin',
|
||||
name: 'otherAdmin',
|
||||
it('throws with error that pinning posts is not allowed', async () => {
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
|
||||
data: { pinPost: null },
|
||||
errors: [{ message: 'Pinned posts are not allowed!' }],
|
||||
})
|
||||
authenticatedUser = await otherAdmin.toJson()
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'created-by-one-admin-pinned-by-different-one',
|
||||
},
|
||||
{
|
||||
author: otherAdmin,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('MAX_PINNED_POSTS is 1', () => {
|
||||
beforeEach(() => {
|
||||
CONFIG.MAX_PINNED_POSTS = 1
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' }
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
describe('are allowed to pin posts', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
author: {
|
||||
name: 'Admin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets createdAt date for PINNED', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-and-pinned-by-same-admin',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('sets redundant `pinned` property for performant ordering', async () => {
|
||||
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
|
||||
const expected = {
|
||||
data: { pinPost: { pinned: true } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created by another admin', () => {
|
||||
let otherAdmin
|
||||
beforeEach(async () => {
|
||||
otherAdmin = await Factory.build('user', {
|
||||
role: 'admin',
|
||||
name: 'otherAdmin',
|
||||
})
|
||||
authenticatedUser = await otherAdmin.toJson()
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'created-by-one-admin-pinned-by-different-one',
|
||||
author: {
|
||||
name: 'otherAdmin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
{
|
||||
author: otherAdmin,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with the updated Post', async () => {
|
||||
authenticatedUser = await admin.toJson()
|
||||
variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' }
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'created-by-one-admin-pinned-by-different-one',
|
||||
author: {
|
||||
name: 'otherAdmin',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created by another user', () => {
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'p9876',
|
||||
author: {
|
||||
slug: 'the-author',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pinned post already exists', () => {
|
||||
let pinnedPost
|
||||
beforeEach(async () => {
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'only-pinned-post',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('removes previous `pinned` attribute', async () => {
|
||||
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes previous PINNED relationship', async () => {
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await database.neode.cypher(
|
||||
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
|
||||
{},
|
||||
)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PostOrdering', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('post', {
|
||||
id: 'im-a-pinned-post',
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinned: true,
|
||||
})
|
||||
await Factory.build('post', {
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
// fairly old, so this should be 3rd
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
})
|
||||
})
|
||||
|
||||
describe('order by `pinned_asc` and `createdAt_desc`', () => {
|
||||
beforeEach(() => {
|
||||
// this is the ordering in the frontend
|
||||
variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
|
||||
})
|
||||
|
||||
it('pinned post appear first even when created before other posts', async () => {
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'im-a-pinned-post',
|
||||
pinned: true,
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinnedAt: expect.any(String),
|
||||
},
|
||||
{
|
||||
id: 'p9876',
|
||||
pinned: null,
|
||||
createdAt: expect.any(String),
|
||||
pinnedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
pinned: null,
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
pinnedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('post created by another user', () => {
|
||||
it('responds with the updated Post', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'p9876',
|
||||
author: {
|
||||
slug: 'the-author',
|
||||
},
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
name: 'Admin',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
describe('MAX_PINNED_POSTS = 3', () => {
|
||||
const postsPinnedCountsQuery = `query { PostsPinnedCounts { maxPinnedPosts, currentlyPinnedPosts } }`
|
||||
|
||||
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pinned post already exists', () => {
|
||||
let pinnedPost
|
||||
beforeEach(async () => {
|
||||
CONFIG.MAX_PINNED_POSTS = 3
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'only-pinned-post',
|
||||
id: 'first-post',
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('removes previous `pinned` attribute', async () => {
|
||||
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes previous PINNED relationship', async () => {
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await database.neode.cypher(
|
||||
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
|
||||
{},
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'second-post',
|
||||
createdAt: '2018-10-22T17:26:29.070Z',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'third-post',
|
||||
createdAt: '2017-10-22T17:26:29.070Z',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
await Factory.build(
|
||||
'post',
|
||||
{
|
||||
id: 'another-post',
|
||||
},
|
||||
{
|
||||
author: admin,
|
||||
},
|
||||
)
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PostOrdering', () => {
|
||||
beforeEach(async () => {
|
||||
await Factory.build('post', {
|
||||
id: 'im-a-pinned-post',
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinned: true,
|
||||
})
|
||||
await Factory.build('post', {
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
// fairly old, so this should be 3rd
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
})
|
||||
})
|
||||
|
||||
describe('order by `pinned_asc` and `createdAt_desc`', () => {
|
||||
beforeEach(() => {
|
||||
// this is the ordering in the frontend
|
||||
variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
|
||||
describe('first post', () => {
|
||||
let result
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, id: 'first-post' }
|
||||
result = await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('pinned post appear first even when created before other posts', async () => {
|
||||
const postOrderingQuery = gql`
|
||||
query ($orderBy: [_PostOrdering]) {
|
||||
Post(orderBy: $orderBy) {
|
||||
id
|
||||
pinned
|
||||
createdAt
|
||||
pinnedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject({
|
||||
it('pins the first post', () => {
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'im-a-pinned-post',
|
||||
pinned: true,
|
||||
createdAt: '2019-11-22T17:26:29.070Z',
|
||||
pinnedAt: expect.any(String),
|
||||
pinPost: {
|
||||
id: 'first-post',
|
||||
pinned: true,
|
||||
pinnedAt: expect.any(String),
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
},
|
||||
{
|
||||
id: 'p9876',
|
||||
pinned: null,
|
||||
createdAt: expect.any(String),
|
||||
pinnedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'i-was-created-before-pinned-post',
|
||||
pinned: null,
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
pinnedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the correct counts', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: postsPinnedCountsQuery,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
PostsPinnedCounts: {
|
||||
maxPinnedPosts: 3,
|
||||
currentlyPinnedPosts: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('second post', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, id: 'second-post' }
|
||||
result = await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('pins the second post', () => {
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'second-post',
|
||||
pinned: true,
|
||||
pinnedAt: expect.any(String),
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the correct counts', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: postsPinnedCountsQuery,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
PostsPinnedCounts: {
|
||||
maxPinnedPosts: 3,
|
||||
currentlyPinnedPosts: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('third post', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, id: 'third-post' }
|
||||
result = await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('pins the second post', () => {
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
pinPost: {
|
||||
id: 'third-post',
|
||||
pinned: true,
|
||||
pinnedAt: expect.any(String),
|
||||
pinnedBy: {
|
||||
id: 'current-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the correct counts', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: postsPinnedCountsQuery,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
PostsPinnedCounts: {
|
||||
maxPinnedPosts: 3,
|
||||
currentlyPinnedPosts: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('another post', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, id: 'another-post' }
|
||||
result = await mutate({ mutation: pinPostMutation, variables })
|
||||
})
|
||||
|
||||
it('throws with max pinned posts is reached', () => {
|
||||
expect(result).toMatchObject({
|
||||
data: { pinPost: null },
|
||||
errors: [{ message: 'Max number of pinned posts is reached!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('post ordering', () => {
|
||||
beforeEach(() => {
|
||||
// this is the ordering in the frontend
|
||||
variables = { orderBy: ['pinned_asc', 'createdAt_desc'] }
|
||||
})
|
||||
|
||||
it('places the pinned posts first, though they are much older', async () => {
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
id: 'first-post',
|
||||
pinned: true,
|
||||
pinnedAt: expect.any(String),
|
||||
createdAt: '2019-10-22T17:26:29.070Z',
|
||||
},
|
||||
{
|
||||
id: 'second-post',
|
||||
pinned: true,
|
||||
pinnedAt: expect.any(String),
|
||||
createdAt: '2018-10-22T17:26:29.070Z',
|
||||
},
|
||||
{
|
||||
id: 'third-post',
|
||||
pinned: true,
|
||||
pinnedAt: expect.any(String),
|
||||
createdAt: '2017-10-22T17:26:29.070Z',
|
||||
},
|
||||
{
|
||||
id: 'another-post',
|
||||
pinned: null,
|
||||
pinnedAt: null,
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
{
|
||||
id: 'p9876',
|
||||
pinned: null,
|
||||
pinnedAt: null,
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,6 +10,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
import { validateEventParams } from './helpers/events'
|
||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||
@ -96,6 +97,17 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
PostsPinnedCounts: async (_object, params, context: Context, _resolveInfo) => {
|
||||
const [postsPinnedCount] = (
|
||||
await context.database.query({
|
||||
query: 'MATCH (p:Post { pinned: true }) RETURN COUNT (p) AS count',
|
||||
})
|
||||
).records.map((r) => Number(r.get('count').toString()))
|
||||
return {
|
||||
maxPinnedPosts: CONFIG.MAX_PINNED_POSTS,
|
||||
currentlyPinnedPosts: postsPinnedCount,
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
CreatePost: async (_parent, params, context, _resolveInfo) => {
|
||||
@ -330,56 +342,79 @@ export default {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
pinPost: async (_parent, params, context, _resolveInfo) => {
|
||||
pinPost: async (_parent, params, context: Context, _resolveInfo) => {
|
||||
if (CONFIG.MAX_PINNED_POSTS === 0) throw new Error('Pinned posts are not allowed!')
|
||||
let pinnedPostWithNestedAttributes
|
||||
const { driver, user } = context
|
||||
const session = driver.session()
|
||||
const { id: userId } = user
|
||||
let writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const deletePreviousRelationsResponse = await transaction.run(
|
||||
`
|
||||
const pinPostCypher = `
|
||||
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
|
||||
MATCH (post:Post {id: $params.id})
|
||||
WHERE NOT((post)-[:IN]->(:Group))
|
||||
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
|
||||
SET post.pinned = true
|
||||
RETURN post, pinned.createdAt as pinnedAt`
|
||||
|
||||
if (CONFIG.MAX_PINNED_POSTS === 1) {
|
||||
let writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const deletePreviousRelationsResponse = await transaction.run(
|
||||
`
|
||||
MATCH (:User)-[previousRelations:PINNED]->(post:Post)
|
||||
REMOVE post.pinned
|
||||
DELETE previousRelations
|
||||
RETURN post
|
||||
`,
|
||||
)
|
||||
return deletePreviousRelationsResponse.records.map(
|
||||
(record) => record.get('post').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
|
||||
writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const pinPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
|
||||
MATCH (post:Post {id: $params.id})
|
||||
WHERE NOT((post)-[:IN]->(:Group))
|
||||
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
|
||||
SET post.pinned = true
|
||||
RETURN post, pinned.createdAt as pinnedAt
|
||||
`,
|
||||
{ userId, params },
|
||||
)
|
||||
return pinPostTransactionResponse.records.map((record) => ({
|
||||
pinnedPost: record.get('post').properties,
|
||||
pinnedAt: record.get('pinnedAt'),
|
||||
}))
|
||||
return deletePreviousRelationsResponse.records.map(
|
||||
(record) => record.get('post').properties,
|
||||
)
|
||||
})
|
||||
const [transactionResult] = await writeTxResultPromise
|
||||
if (transactionResult) {
|
||||
const { pinnedPost, pinnedAt } = transactionResult
|
||||
pinnedPostWithNestedAttributes = {
|
||||
...pinnedPost,
|
||||
pinnedAt,
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
|
||||
writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const pinPostTransactionResponse = await transaction.run(pinPostCypher, {
|
||||
userId,
|
||||
params,
|
||||
})
|
||||
return pinPostTransactionResponse.records.map((record) => ({
|
||||
pinnedPost: record.get('post').properties,
|
||||
pinnedAt: record.get('pinnedAt'),
|
||||
}))
|
||||
})
|
||||
const [transactionResult] = await writeTxResultPromise
|
||||
if (transactionResult) {
|
||||
const { pinnedPost, pinnedAt } = transactionResult
|
||||
pinnedPostWithNestedAttributes = {
|
||||
...pinnedPost,
|
||||
pinnedAt,
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await session.close()
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
return pinnedPostWithNestedAttributes
|
||||
} else {
|
||||
const [currentPinnedPostCount] = (
|
||||
await context.database.query({
|
||||
query: `MATCH (:User)-[:PINNED]->(post:Post { pinned: true }) RETURN COUNT(post) AS count`,
|
||||
})
|
||||
).records.map((r) => Number(r.get('count').toString()))
|
||||
if (currentPinnedPostCount >= CONFIG.MAX_PINNED_POSTS) {
|
||||
throw new Error('Max number of pinned posts is reached!')
|
||||
}
|
||||
const [pinPostResult] = (
|
||||
await context.database.write({
|
||||
query: pinPostCypher,
|
||||
variables: { userId, params },
|
||||
})
|
||||
).records.map((r) => ({
|
||||
...r.get('post').properties,
|
||||
pinnedAt: r.get('pinnedAt'),
|
||||
}))
|
||||
return pinPostResult
|
||||
}
|
||||
return pinnedPostWithNestedAttributes
|
||||
},
|
||||
unpinPost: async (_parent, params, context, _resolveInfo) => {
|
||||
let unpinnedPost
|
||||
|
||||
@ -250,6 +250,11 @@ type Mutation {
|
||||
toggleObservePost(id: ID!, value: Boolean!): Post!
|
||||
}
|
||||
|
||||
type PinnedPostCounts {
|
||||
maxPinnedPosts: Int!
|
||||
currentlyPinnedPosts: Int!
|
||||
}
|
||||
|
||||
type Query {
|
||||
Post(
|
||||
id: ID
|
||||
@ -271,4 +276,5 @@ type Query {
|
||||
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
|
||||
PostsEmotionsByCurrentUser(postId: ID!): [String]
|
||||
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]
|
||||
PostsPinnedCounts: PinnedPostCounts!
|
||||
}
|
||||
|
||||
@ -433,6 +433,7 @@ export default shield(
|
||||
Room: isAuthenticated,
|
||||
Message: isAuthenticated,
|
||||
UnreadRooms: isAuthenticated,
|
||||
PostsPinnedCounts: isAdmin,
|
||||
|
||||
// Invite Code
|
||||
validateInviteCode: allow,
|
||||
|
||||
@ -16,7 +16,10 @@ const stubs = {
|
||||
},
|
||||
}
|
||||
|
||||
let getters, mutations, mocks, menuToggle, openModalSpy
|
||||
let getters, mutations, actions, mocks, menuToggle, openModalSpy
|
||||
|
||||
const maxPinnedPostsMock = jest.fn()
|
||||
const currentlyPinnedPostsMock = jest.fn()
|
||||
|
||||
describe('ContentMenu.vue', () => {
|
||||
beforeEach(() => {
|
||||
@ -38,10 +41,15 @@ describe('ContentMenu.vue', () => {
|
||||
getters = {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/isAdmin': () => false,
|
||||
'pinnedPosts/maxPinnedPosts': maxPinnedPostsMock,
|
||||
'pinnedPosts/currentlyPinnedPosts': currentlyPinnedPostsMock,
|
||||
}
|
||||
actions = {
|
||||
'pinnedPosts/fetch': jest.fn(),
|
||||
}
|
||||
|
||||
const openContentMenu = async (values = {}) => {
|
||||
const store = new Vuex.Store({ mutations, getters })
|
||||
const store = new Vuex.Store({ mutations, getters, actions })
|
||||
const wrapper = mount(ContentMenu, {
|
||||
propsData: {
|
||||
...values,
|
||||
@ -91,53 +99,208 @@ describe('ContentMenu.vue', () => {
|
||||
})
|
||||
|
||||
describe('admin can', () => {
|
||||
it('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,
|
||||
},
|
||||
describe('when maxPinnedPosts = 0', () => {
|
||||
beforeEach(() => {
|
||||
maxPinnedPostsMock.mockReturnValue(0)
|
||||
})
|
||||
wrapper
|
||||
.findAll('.ds-menu-item')
|
||||
.filter((item) => item.text() === 'post.menu.pin')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('pinPost')).toEqual([
|
||||
[
|
||||
{
|
||||
|
||||
it('not 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,
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('unpin pinned post', async () => {
|
||||
const wrapper = await openContentMenu({
|
||||
isOwner: false,
|
||||
resourceType: 'contribution',
|
||||
resource: {
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
})
|
||||
expect(
|
||||
wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'),
|
||||
).toHaveLength(0)
|
||||
})
|
||||
wrapper
|
||||
.findAll('.ds-menu-item')
|
||||
.filter((item) => item.text() === 'post.menu.unpin')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('unpinPost')).toEqual([
|
||||
[
|
||||
{
|
||||
|
||||
it('unpin pinned post', async () => {
|
||||
const wrapper = await openContentMenu({
|
||||
isOwner: false,
|
||||
resourceType: 'contribution',
|
||||
resource: {
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
wrapper
|
||||
.findAll('.ds-menu-item')
|
||||
.filter((item) => item.text() === 'post.menu.unpin')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('unpinPost')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when maxPinnedPosts = 1', () => {
|
||||
beforeEach(() => {
|
||||
maxPinnedPostsMock.mockReturnValue(1)
|
||||
})
|
||||
|
||||
it('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,
|
||||
},
|
||||
})
|
||||
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,
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('unpin pinned post', async () => {
|
||||
const wrapper = await openContentMenu({
|
||||
isOwner: false,
|
||||
resourceType: 'contribution',
|
||||
resource: {
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
})
|
||||
wrapper
|
||||
.findAll('.ds-menu-item')
|
||||
.filter((item) => item.text() === 'post.menu.unpin')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('unpinPost')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when maxPinnedPosts = 3', () => {
|
||||
describe('and max is not reached', () => {
|
||||
beforeEach(() => {
|
||||
maxPinnedPostsMock.mockReturnValue(3)
|
||||
currentlyPinnedPostsMock.mockReturnValue(2)
|
||||
})
|
||||
|
||||
it('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,
|
||||
},
|
||||
})
|
||||
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,
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('unpin pinned post', async () => {
|
||||
const wrapper = await openContentMenu({
|
||||
isOwner: false,
|
||||
resourceType: 'contribution',
|
||||
resource: {
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
})
|
||||
wrapper
|
||||
.findAll('.ds-menu-item')
|
||||
.filter((item) => item.text() === 'post.menu.unpin')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('unpinPost')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('and max is reached', () => {
|
||||
beforeEach(() => {
|
||||
maxPinnedPostsMock.mockReturnValue(3)
|
||||
currentlyPinnedPostsMock.mockReturnValue(3)
|
||||
})
|
||||
|
||||
it('not 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,
|
||||
},
|
||||
})
|
||||
expect(
|
||||
wrapper.findAll('.ds-menu-item').filter((item) => item.text() === 'post.menu.pin'),
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('unpin pinned post', async () => {
|
||||
const wrapper = await openContentMenu({
|
||||
isOwner: false,
|
||||
resourceType: 'contribution',
|
||||
resource: {
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
})
|
||||
wrapper
|
||||
.findAll('.ds-menu-item')
|
||||
.filter((item) => item.text() === 'post.menu.unpin')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('unpinPost')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
|
||||
pinnedBy: 'someone',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('can delete another user', async () => {
|
||||
|
||||
@ -32,12 +32,14 @@
|
||||
|
||||
<script>
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import PinnedPostsMixin from '~/mixins/pinnedPosts'
|
||||
|
||||
export default {
|
||||
name: 'ContentMenu',
|
||||
components: {
|
||||
Dropdown,
|
||||
},
|
||||
mixins: [PinnedPostsMixin],
|
||||
props: {
|
||||
placement: { type: String, default: 'top-end' },
|
||||
resource: { type: Object, required: true },
|
||||
@ -81,7 +83,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.isAdmin && !this.resource.group) {
|
||||
if (!this.resource.pinnedBy) {
|
||||
if (!this.resource.pinnedBy && this.canBePinned) {
|
||||
routes.push({
|
||||
label: this.$t(`post.menu.pin`),
|
||||
callback: () => {
|
||||
@ -90,13 +92,15 @@ export default {
|
||||
icon: 'link',
|
||||
})
|
||||
} else {
|
||||
routes.push({
|
||||
label: this.$t(`post.menu.unpin`),
|
||||
callback: () => {
|
||||
this.$emit('unpinPost', this.resource)
|
||||
},
|
||||
icon: 'unlink',
|
||||
})
|
||||
if (this.resource.pinnedBy) {
|
||||
routes.push({
|
||||
label: this.$t(`post.menu.unpin`),
|
||||
callback: () => {
|
||||
this.$emit('unpinPost', this.resource)
|
||||
},
|
||||
icon: 'unlink',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,6 +232,12 @@ export default {
|
||||
isAdmin() {
|
||||
return this.$store.getters['auth/isAdmin']
|
||||
},
|
||||
canBePinned() {
|
||||
return (
|
||||
this.maxPinnedPosts === 1 ||
|
||||
(this.maxPinnedPosts > 1 && this.currentlyPinnedPosts < this.maxPinnedPosts)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openItem(route, toggleMenu) {
|
||||
|
||||
@ -187,3 +187,14 @@ export const relatedContributions = (i18n) => {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const postsPinnedCountsQuery = () => {
|
||||
return gql`
|
||||
query {
|
||||
PostsPinnedCounts {
|
||||
maxPinnedPosts
|
||||
currentlyPinnedPosts
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
19
webapp/mixins/pinnedPosts.js
Normal file
19
webapp/mixins/pinnedPosts.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
maxPinnedPosts: 'pinnedPosts/maxPinnedPosts',
|
||||
currentlyPinnedPosts: 'pinnedPosts/currentlyPinnedPosts',
|
||||
isAdmin: 'auth/isAdmin',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
fetchPinnedPostsCount: 'pinnedPosts/fetch',
|
||||
}),
|
||||
},
|
||||
async created() {
|
||||
if (this.isAdmin && this.maxPinnedPosts === 0) await this.fetchPinnedPostsCount()
|
||||
},
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import { mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
@ -17,6 +18,7 @@ export default {
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
|
||||
this.storePinPost()
|
||||
refetchPostList()
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
@ -31,6 +33,7 @@ export default {
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
|
||||
this.storeUnpinPost()
|
||||
refetchPostList()
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
@ -53,5 +56,9 @@ export default {
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
},
|
||||
...mapMutations({
|
||||
storePinPost: 'pinnedPosts/pinPost',
|
||||
storeUnpinPost: 'pinnedPosts/unpinPost',
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@ -179,10 +179,10 @@ import {
|
||||
} from '~/components/utils/PostHelpers'
|
||||
import PostQuery from '~/graphql/PostQuery'
|
||||
import { groupQuery } from '~/graphql/groups'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import links from '~/constants/links.js'
|
||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||
import GetCategories from '~/mixins/getCategoriesMixin.js'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'PostSlug',
|
||||
@ -204,7 +204,7 @@ export default {
|
||||
PageParamsLink,
|
||||
UserTeaser,
|
||||
},
|
||||
mixins: [SortCategories, GetCategories],
|
||||
mixins: [GetCategories, postListActions, SortCategories],
|
||||
head() {
|
||||
return {
|
||||
title: this.title,
|
||||
@ -320,46 +320,6 @@ export default {
|
||||
this.post.isObservedByMe = comment.isPostObservedByMe
|
||||
this.post.observingUsersCount = comment.postObservingUsersCount
|
||||
},
|
||||
pinPost(post) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: PostMutations().pinPost,
|
||||
variables: { id: post.id },
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
},
|
||||
unpinPost(post) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: PostMutations().unpinPost,
|
||||
variables: { id: post.id },
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
},
|
||||
toggleObservePost(postId, value) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: PostMutations().toggleObservePost,
|
||||
variables: {
|
||||
value,
|
||||
id: postId,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
const message = this.$t(
|
||||
`post.menu.${value ? 'observedSuccessfully' : 'unobservedSuccessfully'}`,
|
||||
)
|
||||
this.$toast.success(message)
|
||||
this.$apollo.queries.Post.refetch()
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
},
|
||||
toggleNewCommentForm(showNewCommentForm) {
|
||||
this.showNewCommentForm = showNewCommentForm
|
||||
},
|
||||
|
||||
43
webapp/store/pinnedPosts.js
Normal file
43
webapp/store/pinnedPosts.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { postsPinnedCountsQuery } from '~/graphql/PostQuery'
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
maxPinnedPosts: 0,
|
||||
currentlyPinnedPosts: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
pinPost(state) {
|
||||
state.currentlyPinnedPosts++
|
||||
},
|
||||
unpinPost(state) {
|
||||
state.currentlyPinnedPosts--
|
||||
},
|
||||
setMaxPinnedPosts(state, value) {
|
||||
state.maxPinnedPosts = value
|
||||
},
|
||||
setCurrentlyPinnedPosts(state, value) {
|
||||
state.currentlyPinnedPosts = value
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
maxPinnedPosts(state) {
|
||||
return state.maxPinnedPosts
|
||||
},
|
||||
currentlyPinnedPosts(state) {
|
||||
return state.currentlyPinnedPosts
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async fetch({ commit }) {
|
||||
const client = this.app.apolloProvider.defaultClient
|
||||
const {
|
||||
data: { PostsPinnedCounts },
|
||||
} = await client.query({ query: postsPinnedCountsQuery() })
|
||||
commit('setMaxPinnedPosts', PostsPinnedCounts.maxPinnedPosts)
|
||||
commit('setCurrentlyPinnedPosts', PostsPinnedCounts.currentlyPinnedPosts)
|
||||
},
|
||||
}
|
||||
111
webapp/store/pinnedPosts.spec.js
Normal file
111
webapp/store/pinnedPosts.spec.js
Normal file
@ -0,0 +1,111 @@
|
||||
import { state as initialState, mutations, getters, actions } from './pinnedPosts'
|
||||
import { postsPinnedCountsQuery } from '~/graphql/PostQuery'
|
||||
|
||||
describe('pinned post store', () => {
|
||||
describe('initial state', () => {
|
||||
it('sets all values to 0', () => {
|
||||
expect(initialState()).toEqual({
|
||||
maxPinnedPosts: 0,
|
||||
currentlyPinnedPosts: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mutations', () => {
|
||||
let testMutation
|
||||
const state = {
|
||||
maxPinnedPosts: 0,
|
||||
currentlyPinnedPosts: 0,
|
||||
}
|
||||
|
||||
describe('pinPost', () => {
|
||||
it('increments currentlyPinnedPosts', () => {
|
||||
testMutation = () => {
|
||||
mutations.pinPost(state)
|
||||
return getters.currentlyPinnedPosts(state)
|
||||
}
|
||||
expect(testMutation()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unpinPost', () => {
|
||||
it('decrements currentlyPinnedPosts', () => {
|
||||
state.currentlyPinnedPosts = 2
|
||||
testMutation = () => {
|
||||
mutations.unpinPost(state)
|
||||
return getters.currentlyPinnedPosts(state)
|
||||
}
|
||||
expect(testMutation()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMaxPinnedPosts', () => {
|
||||
it('sets maxPinnedPosts correctly', () => {
|
||||
state.maxPinnedPosts = 3
|
||||
testMutation = () => {
|
||||
mutations.setMaxPinnedPosts(state, 1)
|
||||
return getters.maxPinnedPosts(state)
|
||||
}
|
||||
expect(testMutation()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCurrentlyPinnedPosts', () => {
|
||||
it('sets currentlyPinnedPosts', () => {
|
||||
state.currentlyPinnedPosts = 3
|
||||
testMutation = () => {
|
||||
mutations.setCurrentlyPinnedPosts(state, 1)
|
||||
return getters.currentlyPinnedPosts(state)
|
||||
}
|
||||
expect(testMutation()).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions', () => {
|
||||
const queryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
PostsPinnedCounts: {
|
||||
maxPinnedPosts: 3,
|
||||
currentlyPinnedPosts: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
const commit = jest.fn()
|
||||
|
||||
let action
|
||||
|
||||
beforeEach(() => {
|
||||
const module = {
|
||||
app: {
|
||||
apolloProvider: {
|
||||
defaultClient: {
|
||||
query: queryMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
action = actions.fetch.bind(module)
|
||||
})
|
||||
|
||||
describe('fetch', () => {
|
||||
beforeEach(async () => {
|
||||
await action({ commit })
|
||||
})
|
||||
|
||||
it('calls apollo', () => {
|
||||
expect(queryMock).toBeCalledWith({
|
||||
query: postsPinnedCountsQuery(),
|
||||
})
|
||||
})
|
||||
|
||||
it('commits setMaxPinnedPosts', () => {
|
||||
expect(commit).toBeCalledWith('setMaxPinnedPosts', 3)
|
||||
})
|
||||
|
||||
it('commits setCurrentlyPinnedPosts', () => {
|
||||
expect(commit).toBeCalledWith('setCurrentlyPinnedPosts', 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user