diff --git a/backend/src/db/migrations/20230329150329-article-label-for-posts.js b/backend/src/db/migrations/20230329150329-article-label-for-posts.js
new file mode 100644
index 000000000..3cf435203
--- /dev/null
+++ b/backend/src/db/migrations/20230329150329-article-label-for-posts.js
@@ -0,0 +1,53 @@
+import { getDriver } from '../../db/neo4j'
+
+export const description = 'Add to all existing posts the Article label'
+
+export async function up(next) {
+ const driver = getDriver()
+ const session = driver.session()
+ const transaction = session.beginTransaction()
+
+ try {
+ await transaction.run(`
+ MATCH (post:Post)
+ SET post:Article
+ RETURN post
+ `)
+ await transaction.commit()
+ next()
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.log(error)
+ await transaction.rollback()
+ // eslint-disable-next-line no-console
+ console.log('rolled back')
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+}
+
+export async function down(next) {
+ const driver = getDriver()
+ const session = driver.session()
+ const transaction = session.beginTransaction()
+
+ try {
+ await transaction.run(`
+ MATCH (post:Post)
+ REMOVE post:Article
+ RETURN post
+ `)
+ await transaction.commit()
+ next()
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.log(error)
+ await transaction.rollback()
+ // eslint-disable-next-line no-console
+ console.log('rolled back')
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+}
diff --git a/backend/src/graphql/posts.js b/backend/src/graphql/posts.js
index 2669d6f24..d1dc3ee45 100644
--- a/backend/src/graphql/posts.js
+++ b/backend/src/graphql/posts.js
@@ -11,6 +11,8 @@ export const createPostMutation = () => {
$content: String!
$categoryIds: [ID]
$groupId: ID
+ $postType: PostType
+ $eventInput: _EventInput
) {
CreatePost(
id: $id
@@ -19,11 +21,31 @@ export const createPostMutation = () => {
content: $content
categoryIds: $categoryIds
groupId: $groupId
+ postType: $postType
+ eventInput: $eventInput
) {
id
slug
title
content
+ disabled
+ deleted
+ postType
+ author {
+ name
+ }
+ categories {
+ id
+ }
+ eventStart
+ eventEnd
+ eventLocationName
+ eventVenue
+ eventIsOnline
+ eventLocation {
+ lng
+ lat
+ }
}
}
`
@@ -50,6 +72,7 @@ export const filterPosts = () => {
id
title
content
+ eventStart
}
}
`
diff --git a/backend/src/schema/resolvers/filter-posts.spec.js b/backend/src/schema/resolvers/filter-posts.spec.js
new file mode 100644
index 000000000..0b96e001f
--- /dev/null
+++ b/backend/src/schema/resolvers/filter-posts.spec.js
@@ -0,0 +1,230 @@
+import { createTestClient } from 'apollo-server-testing'
+import Factory, { cleanDatabase } from '../../db/factories'
+import { getNeode, getDriver } from '../../db/neo4j'
+import createServer from '../../server'
+import CONFIG from '../../config'
+import { filterPosts, createPostMutation } from '../../graphql/posts'
+
+CONFIG.CATEGORIES_ACTIVE = false
+
+const driver = getDriver()
+const neode = getNeode()
+
+let query
+let mutate
+let authenticatedUser
+let user
+
+beforeAll(async () => {
+ await cleanDatabase()
+
+ const { server } = createServer({
+ context: () => {
+ return {
+ driver,
+ neode,
+ user: authenticatedUser,
+ }
+ },
+ })
+ query = createTestClient(server).query
+ mutate = createTestClient(server).mutate
+})
+
+afterAll(async () => {
+ await cleanDatabase()
+ driver.close()
+})
+
+describe('Filter Posts', () => {
+ const now = new Date()
+
+ beforeAll(async () => {
+ user = await Factory.build('user', {
+ id: 'user',
+ name: 'User',
+ about: 'I am a user.',
+ })
+ authenticatedUser = await user.toJson()
+ await mutate({
+ mutation: createPostMutation(),
+ variables: {
+ id: 'a1',
+ title: 'I am an article',
+ content: 'I am an article written by user.',
+ },
+ })
+ await mutate({
+ mutation: createPostMutation(),
+ variables: {
+ id: 'a2',
+ title: 'I am anonther article',
+ content: 'I am another article written by user.',
+ },
+ })
+ await mutate({
+ mutation: createPostMutation(),
+ variables: {
+ id: 'e1',
+ title: 'Illegaler Kindergeburtstag',
+ content: 'Elli wird fünf. Wir feiern ihren Geburtstag.',
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventVenue: 'Garten der Familie Maier',
+ },
+ },
+ })
+ await mutate({
+ mutation: createPostMutation(),
+ variables: {
+ id: 'e2',
+ title: 'Räuber-Treffen',
+ content: 'Planung der nächsten Räuberereien',
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(),
+ eventVenue: 'Wirtshaus im Spessart',
+ },
+ },
+ })
+ })
+
+ describe('no filters set', () => {
+ it('finds all posts', async () => {
+ const {
+ data: { Post: result },
+ } = await query({ query: filterPosts() })
+ expect(result).toHaveLength(4)
+ expect(result).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ id: 'a1' }),
+ expect.objectContaining({ id: 'a2' }),
+ expect.objectContaining({ id: 'e1' }),
+ expect.objectContaining({ id: 'e2' }),
+ ]),
+ )
+ })
+ })
+
+ describe('post type filter set to ["Article"]', () => {
+ it('finds the articles', async () => {
+ const {
+ data: { Post: result },
+ } = await query({ query: filterPosts(), variables: { filter: { postType_in: ['Article'] } } })
+ expect(result).toHaveLength(2)
+ expect(result).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ id: 'a1' }),
+ expect.objectContaining({ id: 'a2' }),
+ ]),
+ )
+ })
+ })
+
+ describe('post type filter set to ["Event"]', () => {
+ it('finds the articles', async () => {
+ const {
+ data: { Post: result },
+ } = await query({ query: filterPosts(), variables: { filter: { postType_in: ['Event'] } } })
+ expect(result).toHaveLength(2)
+ expect(result).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ id: 'e1' }),
+ expect.objectContaining({ id: 'e2' }),
+ ]),
+ )
+ })
+ })
+
+ describe('post type filter set to ["Article", "Event"]', () => {
+ it('finds all posts', async () => {
+ const {
+ data: { Post: result },
+ } = await query({
+ query: filterPosts(),
+ variables: { filter: { postType_in: ['Article', 'Event'] } },
+ })
+ expect(result).toHaveLength(4)
+ expect(result).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ id: 'a1' }),
+ expect.objectContaining({ id: 'a2' }),
+ expect.objectContaining({ id: 'e1' }),
+ expect.objectContaining({ id: 'e2' }),
+ ]),
+ )
+ })
+ })
+
+ describe('order events by event start descending', () => {
+ it('finds the events orderd accordingly', async () => {
+ const {
+ data: { Post: result },
+ } = await query({
+ query: filterPosts(),
+ variables: { filter: { postType_in: ['Event'] }, orderBy: ['eventStart_desc'] },
+ })
+ expect(result).toHaveLength(2)
+ expect(result).toEqual([
+ expect.objectContaining({
+ id: 'e1',
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ }),
+ expect.objectContaining({
+ id: 'e2',
+ eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(),
+ }),
+ ])
+ })
+ })
+
+ describe('order events by event start ascending', () => {
+ it('finds the events orderd accordingly', async () => {
+ const {
+ data: { Post: result },
+ } = await query({
+ query: filterPosts(),
+ variables: { filter: { postType_in: ['Event'] }, orderBy: ['eventStart_asc'] },
+ })
+ expect(result).toHaveLength(2)
+ expect(result).toEqual([
+ expect.objectContaining({
+ id: 'e2',
+ eventStart: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString(),
+ }),
+ expect.objectContaining({
+ id: 'e1',
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ }),
+ ])
+ })
+ })
+
+ describe('filter events by event start date', () => {
+ it('finds only events after given date', async () => {
+ const {
+ data: { Post: result },
+ } = await query({
+ query: filterPosts(),
+ variables: {
+ filter: {
+ postType_in: ['Event'],
+ eventStart_gte: new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate() + 2,
+ ).toISOString(),
+ },
+ },
+ })
+ expect(result).toHaveLength(1)
+ expect(result).toEqual([
+ expect.objectContaining({
+ id: 'e1',
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ }),
+ ])
+ })
+ })
+})
diff --git a/backend/src/schema/resolvers/helpers/events.js b/backend/src/schema/resolvers/helpers/events.js
new file mode 100644
index 000000000..84e64299d
--- /dev/null
+++ b/backend/src/schema/resolvers/helpers/events.js
@@ -0,0 +1,47 @@
+import { UserInputError } from 'apollo-server'
+
+export const validateEventParams = (params) => {
+ if (params.postType && params.postType === 'Event') {
+ const { eventInput } = params
+ validateEventDate(eventInput.eventStart)
+ params.eventStart = eventInput.eventStart
+ if (eventInput.eventEnd) {
+ validateEventEnd(eventInput.eventStart, eventInput.eventEnd)
+ params.eventEnd = eventInput.eventEnd
+ }
+ if (eventInput.eventLocationName && !eventInput.eventVenue) {
+ throw new UserInputError('Event venue must be present if event location is given!')
+ }
+ params.eventVenue = eventInput.eventVenue
+ params.eventLocationName = eventInput.eventLocationName
+ params.eventIsOnline = !!eventInput.eventIsOnline
+ }
+ delete params.eventInput
+ let locationName
+ if (params.eventLocationName) {
+ locationName = params.eventLocationName
+ } else {
+ params.eventLocationName = null
+ locationName = null
+ }
+ return locationName
+}
+
+const validateEventDate = (dateString) => {
+ const date = new Date(dateString)
+ if (date.toString() === 'Invalid Date')
+ throw new UserInputError('Event start date must be a valid date!')
+ const now = new Date()
+ if (date.getTime() < now.getTime()) {
+ throw new UserInputError('Event start date must be in the future!')
+ }
+}
+
+const validateEventEnd = (start, end) => {
+ const endDate = new Date(end)
+ if (endDate.toString() === 'Invalid Date')
+ throw new UserInputError('Event end date must be a valid date!')
+ const startDate = new Date(start)
+ if (endDate < startDate)
+ throw new UserInputError('Event end date must be a after event start date!')
+}
diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js
index 117b9b530..c3b882146 100644
--- a/backend/src/schema/resolvers/notifications.js
+++ b/backend/src/schema/resolvers/notifications.js
@@ -51,7 +51,7 @@ export default {
OPTIONAL MATCH (resource)<-[membership:MEMBER_OF]-(relatedUser)
WITH user, notification, resource, membership, relatedUser,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
- [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post {.*, author: properties(author)} ] AS posts
+ [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post {.*, author: properties(author), postType: filter(l IN labels(post) WHERE NOT l = "Post")} ] AS posts
WITH resource, user, notification, authors, posts, relatedUser, membership,
resource {.*,
__typename: labels(resource)[0],
@@ -90,7 +90,7 @@ export default {
SET notification.read = TRUE
WITH user, notification, resource,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
- [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
+ [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author), postType: filter(l IN labels(post) WHERE NOT l = "Post")} ] AS posts
OPTIONAL MATCH (resource)<-[membership:MEMBER_OF]-(user)
WITH resource, user, notification, authors, posts, membership,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0], myRole: membership.role } AS finalResource
@@ -120,7 +120,7 @@ export default {
SET notification.read = TRUE
WITH user, notification, resource,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
- [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
+ [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author), postType: filter(l IN labels(post) WHERE NOT l = "Post")} ] AS posts
OPTIONAL MATCH (resource)<-[membership:MEMBER_OF]-(user)
WITH resource, user, notification, authors, posts, membership,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0], myRole: membership.role} AS finalResource
diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js
index 063b2210d..c7d2eebdb 100644
--- a/backend/src/schema/resolvers/posts.js
+++ b/backend/src/schema/resolvers/posts.js
@@ -7,6 +7,8 @@ import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
+import { validateEventParams } from './helpers/events'
+import { createOrUpdateLocations } from './users/location'
import CONFIG from '../../config'
const maintainPinnedPosts = (params) => {
@@ -81,6 +83,9 @@ export default {
CreatePost: async (_parent, params, context, _resolveInfo) => {
const { categoryIds, groupId } = params
const { image: imageInput } = params
+
+ const locationName = validateEventParams(params)
+
delete params.categoryIds
delete params.image
delete params.groupId
@@ -125,12 +130,13 @@ export default {
SET post.updatedAt = toString(datetime())
SET post.clickedCount = 0
SET post.viewedTeaserCount = 0
+ SET post:${params.postType}
WITH post
MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author)
${categoriesCypher}
${groupCypher}
- RETURN post {.*}
+ RETURN post {.*, postType: filter(l IN labels(post) WHERE NOT l = "Post") }
`,
{ userId: context.user.id, categoryIds, groupId, params },
)
@@ -142,6 +148,9 @@ export default {
})
try {
const post = await writeTxResultPromise
+ if (locationName) {
+ await createOrUpdateLocations('Post', post.id, locationName, session)
+ }
return post
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
@@ -154,6 +163,9 @@ export default {
UpdatePost: async (_parent, params, context, _resolveInfo) => {
const { categoryIds } = params
const { image: imageInput } = params
+
+ const locationName = validateEventParams(params)
+
delete params.categoryIds
delete params.image
const session = context.driver.session()
@@ -183,7 +195,16 @@ export default {
`
}
- updatePostCypher += `RETURN post {.*}`
+ if (params.postType) {
+ updatePostCypher += `
+ REMOVE post:Article
+ REMOVE post:Event
+ SET post:${params.postType}
+ WITH post
+ `
+ }
+
+ updatePostCypher += `RETURN post {.*, postType: filter(l IN labels(post) WHERE NOT l = "Post")}`
const updatePostVariables = { categoryIds, params }
try {
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@@ -196,6 +217,9 @@ export default {
return post
})
const post = await writeTxResultPromise
+ if (locationName) {
+ await createOrUpdateLocations('Post', post.id, locationName, session)
+ }
return post
} finally {
session.close()
@@ -385,7 +409,19 @@ export default {
},
Post: {
...Resolver('Post', {
- undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
+ undefinedToNull: [
+ 'activityId',
+ 'objectId',
+ 'language',
+ 'pinnedAt',
+ 'pinned',
+ 'eventVenue',
+ 'eventLocation',
+ 'eventLocationName',
+ 'eventStart',
+ 'eventEnd',
+ 'eventIsOnline',
+ ],
hasMany: {
tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)',
@@ -398,6 +434,7 @@ export default {
pinnedBy: '<-[:PINNED]-(related:User)',
image: '-[:HERO_IMAGE]->(related:Image)',
group: '-[:IN]->(related:Group)',
+ eventLocation: '-[:IS_IN]->(related:Location)',
},
count: {
commentsCount:
diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js
index 9335c1313..87d09e262 100644
--- a/backend/src/schema/resolvers/posts.spec.js
+++ b/backend/src/schema/resolvers/posts.spec.js
@@ -3,6 +3,10 @@ import Factory, { cleanDatabase } from '../../db/factories'
import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server'
+import { createPostMutation } from '../../graphql/posts'
+import CONFIG from '../../config'
+
+CONFIG.CATEGORIES_ACTIVE = true
const driver = getDriver()
const neode = getNeode()
@@ -15,29 +19,6 @@ let user
const categoryIds = ['cat9', 'cat4', 'cat15']
let variables
-const createPostMutation = gql`
- mutation ($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
- CreatePost(
- id: $id
- title: $title
- content: $content
- language: $language
- categoryIds: $categoryIds
- ) {
- id
- title
- content
- slug
- disabled
- deleted
- language
- author {
- name
- }
- }
- }
-`
-
beforeAll(async () => {
await cleanDatabase()
@@ -281,7 +262,7 @@ describe('CreatePost', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
- const { errors } = await mutate({ mutation: createPostMutation, variables })
+ const { errors } = await mutate({ mutation: createPostMutation(), variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@@ -296,7 +277,7 @@ describe('CreatePost', () => {
data: { CreatePost: { title: 'I am a title', content: 'Some content' } },
errors: undefined,
}
- await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
+ await expect(mutate({ mutation: createPostMutation(), variables })).resolves.toMatchObject(
expected,
)
})
@@ -313,25 +294,327 @@ describe('CreatePost', () => {
},
errors: undefined,
}
- await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
+ await expect(mutate({ mutation: createPostMutation(), variables })).resolves.toMatchObject(
expected,
)
})
it('`disabled` and `deleted` default to `false`', async () => {
const expected = { data: { CreatePost: { disabled: false, deleted: false } } }
- await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
+ await expect(mutate({ mutation: createPostMutation(), variables })).resolves.toMatchObject(
expected,
)
})
+
+ it('has label "Article" as default', async () => {
+ await expect(mutate({ mutation: createPostMutation(), variables })).resolves.toMatchObject({
+ data: { CreatePost: { postType: ['Article'] } },
+ })
+ })
+
+ describe('with invalid post type', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: { ...variables, postType: 'not-valid' },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message:
+ 'Variable "$postType" got invalid value "not-valid"; Expected type PostType.',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with post type "Event"', () => {
+ describe('without event start date', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: "Cannot read properties of undefined (reading 'eventStart')",
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with invalid event start date', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: 'no date',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event start date must be a valid date!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with event start date in the past', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() - 1).toISOString(),
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event start date must be in the future!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with valid start date and invalid end date', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventEnd: 'not-valid',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event end date must be a valid date!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with valid start date and end date before start date', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 2).toISOString(),
+ eventEnd: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event end date must be a after event start date!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with valid start date and valid end date', () => {
+ it('creates the event', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventEnd: new Date(now.getFullYear(), now.getMonth() + 2).toISOString(),
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ CreatePost: {
+ postType: ['Event'],
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventEnd: new Date(now.getFullYear(), now.getMonth() + 2).toISOString(),
+ eventIsOnline: false,
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('with valid start date and event is online', () => {
+ it('creates the event', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventIsOnline: true,
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ CreatePost: {
+ postType: ['Event'],
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventIsOnline: true,
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('event location name is given but event venue is missing', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventLocationName: 'Berlin',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event venue must be present if event location is given!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('valid event input without location', () => {
+ it('has label "Event" set', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ CreatePost: {
+ postType: ['Event'],
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventIsOnline: false,
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('valid event input with location name', () => {
+ it('has label "Event" set', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: createPostMutation(),
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventLocationName: 'Leipzig',
+ eventVenue: 'Connewitzer Kreuz',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ CreatePost: {
+ postType: ['Event'],
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventLocationName: 'Leipzig',
+ eventVenue: 'Connewitzer Kreuz',
+ eventLocation: {
+ lng: 12.374733,
+ lat: 51.340632,
+ },
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+ })
})
})
describe('UpdatePost', () => {
let author, newlyCreatedPost
const updatePostMutation = gql`
- mutation ($id: ID!, $title: String!, $content: String!, $image: ImageInput) {
- UpdatePost(id: $id, title: $title, content: $content, image: $image) {
+ mutation (
+ $id: ID!
+ $title: String!
+ $content: String!
+ $image: ImageInput
+ $categoryIds: [ID]
+ $postType: PostType
+ $eventInput: _EventInput
+ ) {
+ UpdatePost(
+ id: $id
+ title: $title
+ content: $content
+ image: $image
+ categoryIds: $categoryIds
+ postType: $postType
+ eventInput: $eventInput
+ ) {
id
title
content
@@ -341,26 +624,34 @@ describe('UpdatePost', () => {
}
createdAt
updatedAt
+ categories {
+ id
+ }
+ postType
+ eventStart
+ eventLocationName
+ eventVenue
+ eventLocation {
+ lng
+ lat
+ }
}
}
`
beforeEach(async () => {
author = await Factory.build('user', { slug: 'the-author' })
- newlyCreatedPost = await Factory.build(
- 'post',
- {
- id: 'p9876',
+ authenticatedUser = await author.toJson()
+ const { data } = await mutate({
+ mutation: createPostMutation(),
+ variables: {
title: 'Old title',
content: 'Old content',
- },
- {
- author,
categoryIds,
},
- )
-
+ })
+ newlyCreatedPost = data.CreatePost
variables = {
- id: 'p9876',
+ id: newlyCreatedPost.id,
title: 'New title',
content: 'New content',
}
@@ -394,7 +685,7 @@ describe('UpdatePost', () => {
it('updates a post', async () => {
const expected = {
- data: { UpdatePost: { id: 'p9876', content: 'New content' } },
+ data: { UpdatePost: { id: newlyCreatedPost.id, content: 'New content' } },
errors: undefined,
}
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
@@ -405,7 +696,11 @@ describe('UpdatePost', () => {
it('updates a post, but maintains non-updated attributes', async () => {
const expected = {
data: {
- UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) },
+ UpdatePost: {
+ id: newlyCreatedPost.id,
+ content: 'New content',
+ createdAt: expect.any(String),
+ },
},
errors: undefined,
}
@@ -415,23 +710,20 @@ describe('UpdatePost', () => {
})
it('updates the updatedAt attribute', async () => {
- newlyCreatedPost = await newlyCreatedPost.toJson()
const {
data: { UpdatePost },
} = await mutate({ mutation: updatePostMutation, variables })
- expect(newlyCreatedPost.updatedAt).toBeTruthy()
- expect(Date.parse(newlyCreatedPost.updatedAt)).toEqual(expect.any(Number))
expect(UpdatePost.updatedAt).toBeTruthy()
expect(Date.parse(UpdatePost.updatedAt)).toEqual(expect.any(Number))
expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
})
- /* describe('no new category ids provided for update', () => {
+ describe('no new category ids provided for update', () => {
it('resolves and keeps current categories', async () => {
const expected = {
data: {
UpdatePost: {
- id: 'p9876',
+ id: newlyCreatedPost.id,
categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]),
},
},
@@ -441,9 +733,9 @@ describe('UpdatePost', () => {
expected,
)
})
- }) */
+ })
- /* describe('given category ids', () => {
+ describe('given category ids', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: ['cat27'] }
})
@@ -452,7 +744,7 @@ describe('UpdatePost', () => {
const expected = {
data: {
UpdatePost: {
- id: 'p9876',
+ id: newlyCreatedPost.id,
categories: expect.arrayContaining([{ id: 'cat27' }]),
},
},
@@ -462,9 +754,160 @@ describe('UpdatePost', () => {
expected,
)
})
- }) */
+ })
- describe('params.image', () => {
+ describe('change post type to event', () => {
+ describe('with missing event start date', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePostMutation,
+ variables: { ...variables, postType: 'Event' },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: "Cannot read properties of undefined (reading 'eventStart')",
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with invalid event start date', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePostMutation,
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: 'no-date',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event start date must be a valid date!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('with event start date in the past', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: updatePostMutation,
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() - 1).toISOString(),
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event start date must be in the future!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('event location name is given but event venue is missing', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: updatePostMutation,
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventLocationName: 'Berlin',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [
+ {
+ message: 'Event venue must be present if event location is given!',
+ },
+ ],
+ })
+ })
+ })
+
+ describe('valid event input without location name', () => {
+ it('has label "Event" set', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: updatePostMutation,
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ UpdatePost: {
+ postType: ['Event'],
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('valid event input with location name', () => {
+ it('has label "Event" set', async () => {
+ const now = new Date()
+ await expect(
+ mutate({
+ mutation: updatePostMutation,
+ variables: {
+ ...variables,
+ postType: 'Event',
+ eventInput: {
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventLocationName: 'Leipzig',
+ eventVenue: 'Connewitzer Kreuz',
+ },
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ UpdatePost: {
+ postType: ['Event'],
+ eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
+ eventLocationName: 'Leipzig',
+ eventVenue: 'Connewitzer Kreuz',
+ eventLocation: {
+ lng: 12.374733,
+ lat: 51.340632,
+ },
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+ })
+
+ describe.skip('params.image', () => {
describe('is object', () => {
beforeEach(() => {
variables = { ...variables, image: { sensitive: true } }
diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js
index 86a278207..ba9041090 100644
--- a/backend/src/schema/resolvers/postsInGroups.spec.js
+++ b/backend/src/schema/resolvers/postsInGroups.spec.js
@@ -818,11 +818,13 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
]),
},
@@ -846,11 +848,13 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
]),
},
@@ -874,11 +878,13 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
]),
},
@@ -902,11 +908,13 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
]),
},
@@ -930,21 +938,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1319,16 +1331,19 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1361,21 +1376,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1410,16 +1429,19 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1452,11 +1474,13 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
]),
},
@@ -1489,21 +1513,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1534,21 +1562,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1579,21 +1611,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1628,21 +1664,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1675,21 +1715,25 @@ describe('Posts in Groups', () => {
id: 'post-to-public-group',
title: 'A post to a public group',
content: 'I am posting into a public group as a member of the group',
+ eventStart: null,
},
{
id: 'post-without-group',
title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
@@ -1739,11 +1783,13 @@ describe('Posts in Groups', () => {
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',
+ eventStart: null,
},
{
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',
+ eventStart: null,
},
]),
},
diff --git a/backend/src/schema/resolvers/users/location.js b/backend/src/schema/resolvers/users/location.js
index 9d8a11f89..54d73560b 100644
--- a/backend/src/schema/resolvers/users/location.js
+++ b/backend/src/schema/resolvers/users/location.js
@@ -22,7 +22,7 @@ const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru']
const createLocation = async (session, mapboxData) => {
const data = {
- id: mapboxData.id,
+ id: mapboxData.id + (mapboxData.address ? `-${mapboxData.address}` : ''),
nameEN: mapboxData.text_en,
nameDE: mapboxData.text_de,
nameFR: mapboxData.text_fr,
@@ -33,6 +33,7 @@ const createLocation = async (session, mapboxData) => {
namePL: mapboxData.text_pl,
nameRU: mapboxData.text_ru,
type: mapboxData.id.split('.')[0].toLowerCase(),
+ address: mapboxData.address,
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[0] : null,
lat: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
}
@@ -54,6 +55,10 @@ const createLocation = async (session, mapboxData) => {
if (data.lat && data.lng) {
mutation += ', l.lat = $lat, l.lng = $lng'
}
+ if (data.address) {
+ mutation += ', l.address = $address'
+ }
+
mutation += ' RETURN l.id'
await session.writeTransaction((transaction) => {
@@ -72,7 +77,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s
locationName,
)}.json?access_token=${
CONFIG.MAPBOX_TOKEN
- }&types=region,place,country&language=${locales.join(',')}`,
+ }&types=region,place,country,address&language=${locales.join(',')}`,
)
debug(res)
@@ -103,6 +108,10 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s
let parent = data
+ if (parent.address) {
+ parent.id += `-${parent.address}`
+ }
+
if (data.context) {
await asyncForEach(data.context, async (ctx) => {
await createLocation(session, ctx)
diff --git a/backend/src/schema/types/enum/PostType.gql b/backend/src/schema/types/enum/PostType.gql
new file mode 100644
index 000000000..eef80d6ba
--- /dev/null
+++ b/backend/src/schema/types/enum/PostType.gql
@@ -0,0 +1,4 @@
+enum PostType {
+ Article
+ Event
+}
diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql
index 6fc7a3215..5a5d57b9a 100644
--- a/backend/src/schema/types/type/Post.gql
+++ b/backend/src/schema/types/type/Post.gql
@@ -83,6 +83,8 @@ input _PostFilter {
emotions_every: _PostEMOTEDFilter
group: _GroupFilter
postsInMyGroups: Boolean
+ postType_in: [PostType]
+ eventStart_gte: String
}
enum _PostOrdering {
@@ -104,6 +106,8 @@ enum _PostOrdering {
language_desc
pinned_asc
pinned_desc
+ eventStart_asc
+ eventStart_desc
}
@@ -171,12 +175,30 @@ type Post {
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
group: Group @relation(name: "IN", direction: "OUT")
+
+ postType: [PostType]
+ @cypher(statement: "RETURN filter(l IN labels(this) WHERE NOT l = 'Post')")
+
+ eventLocationName: String
+ eventLocation: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
+ eventVenue: String
+ eventStart: String
+ eventEnd: String
+ eventIsOnline: Boolean
}
input _PostInput {
id: ID!
}
+input _EventInput {
+ eventStart: String!
+ eventEnd: String
+ eventVenue: String
+ eventLocationName: String
+ eventIsOnline: Boolean
+}
+
type Mutation {
CreatePost(
id: ID
@@ -189,6 +211,8 @@ type Mutation {
categoryIds: [ID]
contentExcerpt: String
groupId: ID
+ postType: PostType = Article
+ eventInput: _EventInput
): Post
UpdatePost(
id: ID!
@@ -200,6 +224,8 @@ type Mutation {
visibility: Visibility
language: String
categoryIds: [ID]
+ postType: PostType
+ eventInput: _EventInput
): Post
DeletePost(id: ID!): Post
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
diff --git a/webapp/.eslintignore b/webapp/.eslintignore
index be90fc8e3..a90a66efa 100644
--- a/webapp/.eslintignore
+++ b/webapp/.eslintignore
@@ -1,5 +1,5 @@
node_modules
-build
+dist
.nuxt
styleguide/
**/*.min.js
diff --git a/webapp/assets/_new/icons/svgs/book.svg b/webapp/assets/_new/icons/svgs/book.svg
new file mode 100644
index 000000000..305e367ac
--- /dev/null
+++ b/webapp/assets/_new/icons/svgs/book.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/webapp/assets/_new/icons/svgs/calendar.svg b/webapp/assets/_new/icons/svgs/calendar.svg
new file mode 100644
index 000000000..5a67a8299
--- /dev/null
+++ b/webapp/assets/_new/icons/svgs/calendar.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js
index ef3b47c37..6b9db448b 100644
--- a/webapp/components/ContributionForm/ContributionForm.spec.js
+++ b/webapp/components/ContributionForm/ContributionForm.spec.js
@@ -15,6 +15,7 @@ const stubs = {
'client-only': true,
'nuxt-link': true,
'v-popover': true,
+ 'date-picker': true,
}
describe('ContributionForm.vue', () => {
@@ -45,6 +46,7 @@ describe('ContributionForm.vue', () => {
slug: 'this-is-a-title-for-a-post',
content: postContent,
contentExcerpt: postContent,
+ postType: ['Article'],
},
},
}),
@@ -142,6 +144,7 @@ describe('ContributionForm.vue', () => {
id: null,
image: null,
groupId: null,
+ postType: 'Article',
},
}
postTitleInput = wrapper.find('.ds-input')
@@ -268,6 +271,7 @@ describe('ContributionForm.vue', () => {
image: {
sensitive: false,
},
+ postType: 'Article',
},
}
})
diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue
index f740e1f05..997a25341 100644
--- a/webapp/components/ContributionForm/ContributionForm.vue
+++ b/webapp/components/ContributionForm/ContributionForm.vue
@@ -30,6 +30,7 @@