mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #6199 from Ocelot-Social-Community/event-master
feat(other): 🍰 epic events – master
This commit is contained in:
commit
3790f3d010
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
230
backend/src/schema/resolvers/filter-posts.spec.js
Normal file
230
backend/src/schema/resolvers/filter-posts.spec.js
Normal file
@ -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(),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
47
backend/src/schema/resolvers/helpers/events.js
Normal file
47
backend/src/schema/resolvers/helpers/events.js
Normal file
@ -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!')
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 } }
|
||||
|
||||
@ -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,
|
||||
},
|
||||
]),
|
||||
},
|
||||
|
||||
@ -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)
|
||||
|
||||
4
backend/src/schema/types/enum/PostType.gql
Normal file
4
backend/src/schema/types/enum/PostType.gql
Normal file
@ -0,0 +1,4 @@
|
||||
enum PostType {
|
||||
Article
|
||||
Event
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
.nuxt
|
||||
styleguide/
|
||||
**/*.min.js
|
||||
|
||||
5
webapp/assets/_new/icons/svgs/book.svg
Normal file
5
webapp/assets/_new/icons/svgs/book.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>book</title>
|
||||
<path d="M10 5c2.92 0 5.482 0.981 6 1.188 0.518-0.206 3.080-1.188 6-1.188 3.227 0 6.375 1.313 6.375 1.313l0.625 0.281v20.406h-11.281c-0.346 0.597-0.979 1-1.719 1s-1.373-0.403-1.719-1h-11.281v-20.406l0.625-0.281s3.148-1.313 6.375-1.313zM10 7c-2.199 0-4.232 0.69-5 0.969v16.125c1.188-0.392 2.897-0.875 5-0.875 2.057 0 3.888 0.506 5 0.875v-16.125c-1-0.343-3.067-0.969-5-0.969zM22 7c-1.933 0-4 0.626-5 0.969v16.125c1.112-0.369 2.943-0.875 5-0.875 2.103 0 3.813 0.483 5 0.875v-16.125c-0.768-0.279-2.801-0.969-5-0.969z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
5
webapp/assets/_new/icons/svgs/calendar.svg
Normal file
5
webapp/assets/_new/icons/svgs/calendar.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>calendar</title>
|
||||
<path d="M9 4h2v1h10v-1h2v1h4v22h-22v-22h4v-1zM7 7v2h18v-2h-2v1h-2v-1h-10v1h-2v-1h-2zM7 11v14h18v-14h-18zM13 13h2v2h-2v-2zM17 13h2v2h-2v-2zM21 13h2v2h-2v-2zM9 17h2v2h-2v-2zM13 17h2v2h-2v-2zM17 17h2v2h-2v-2zM21 17h2v2h-2v-2zM9 21h2v2h-2v-2zM13 21h2v2h-2v-2zM17 21h2v2h-2v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@ -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',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
<base-icon name="question-circle" />
|
||||
</page-params-link>
|
||||
</div>
|
||||
<ds-space margin-top="base" />
|
||||
<ds-input
|
||||
model="title"
|
||||
:placeholder="$t('contribution.title')"
|
||||
@ -51,6 +52,93 @@
|
||||
{{ contentLength }}
|
||||
<base-icon v-if="errors && errors.content" name="warning" />
|
||||
</ds-chip>
|
||||
|
||||
<!-- Eventdata -->
|
||||
<div v-if="creatEvent" class="eventDatas">
|
||||
<hr />
|
||||
<ds-space margin-top="x-small" />
|
||||
<ds-grid>
|
||||
<ds-grid-item style="grid-row-end: span 3">
|
||||
<!-- <label>Beginn</label> -->
|
||||
<div style="z-index: 20">
|
||||
<date-picker
|
||||
name="eventStart"
|
||||
v-model="formData.eventStart"
|
||||
type="datetime"
|
||||
value-type="format"
|
||||
:minute-step="15"
|
||||
Xformat="DD-MM-YYYY HH:mm"
|
||||
style="z-index: 20"
|
||||
:placeholder="$t('post.viewEvent.eventStart')"
|
||||
:disabled-date="notBeforeToday"
|
||||
:disabled-time="notBeforeNow"
|
||||
:show-second="false"
|
||||
></date-picker>
|
||||
</div>
|
||||
<div class="chipbox" style="margin-top: 10px">
|
||||
<ds-chip size="base" :color="errors && errors.eventStart && 'danger'">
|
||||
<base-icon v-if="errors && errors.eventStart" name="warning" />
|
||||
</ds-chip>
|
||||
</div>
|
||||
</ds-grid-item>
|
||||
<ds-grid-item style="grid-row-end: span 3">
|
||||
<!-- <label>Ende (optional)</label> -->
|
||||
|
||||
<date-picker
|
||||
v-model="formData.eventEnd"
|
||||
type="datetime"
|
||||
value-type="format"
|
||||
:minute-step="15"
|
||||
:seconds-step="0"
|
||||
Xformat="DD-MM-YYYY HH:mm"
|
||||
:placeholder="$t('post.viewEvent.eventEnd')"
|
||||
style="font-size: larger"
|
||||
:disabled-date="notBeforeEventDay"
|
||||
:disabled-time="notBeforeEvent"
|
||||
:show-second="false"
|
||||
></date-picker>
|
||||
</ds-grid-item>
|
||||
</ds-grid>
|
||||
<ds-grid>
|
||||
<ds-grid-item style="grid-row-end: span 3">
|
||||
<ds-input
|
||||
model="eventVenue"
|
||||
name="location"
|
||||
:placeholder="$t('post.viewEvent.eventVenue')"
|
||||
/>
|
||||
<div class="chipbox">
|
||||
<ds-chip size="base" :color="errors && errors.eventVenue && 'danger'">
|
||||
{{ formData.eventVenue.length }}/{{ formSchema.eventVenue.max }}
|
||||
<base-icon v-if="errors && errors.eventVenue" name="warning" />
|
||||
</ds-chip>
|
||||
</div>
|
||||
</ds-grid-item>
|
||||
<ds-grid-item style="grid-row-end: span 3">
|
||||
<ds-input
|
||||
model="eventLocationName"
|
||||
name="venue"
|
||||
:placeholder="$t('post.viewEvent.eventLocationName')"
|
||||
/>
|
||||
<div class="chipbox">
|
||||
<ds-chip size="base" :color="errors && errors.eventLocationName && 'danger'">
|
||||
{{ formData.eventLocationName.length }}/{{ formSchema.eventLocationName.max }}
|
||||
<base-icon v-if="errors && errors.eventLocationName" name="warning" />
|
||||
</ds-chip>
|
||||
</div>
|
||||
</ds-grid-item>
|
||||
</ds-grid>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
model="formData.eventIsOnline"
|
||||
name="eventIsOnline"
|
||||
style="font-size: larger"
|
||||
/>
|
||||
{{ $t('post.viewEvent.eventIsOnline') }}
|
||||
</div>
|
||||
</div>
|
||||
<ds-space margin-top="x-small" />
|
||||
<categories-select
|
||||
v-if="categoriesActive"
|
||||
model="categoryIds"
|
||||
@ -67,6 +155,7 @@
|
||||
<ds-flex class="buttons-footer" gutter="xxx-small">
|
||||
<ds-flex-item width="3.5" style="margin-right: 16px; margin-bottom: 6px">
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<!-- TODO => remove v-html! only text ! no html! security first! -->
|
||||
<ds-text
|
||||
v-if="showGroupHint"
|
||||
v-html="$t('contribution.visibleOnlyForMembersOfGroup', { name: groupName })"
|
||||
@ -92,7 +181,6 @@
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { mapGetters } from 'vuex'
|
||||
@ -102,6 +190,8 @@ import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
import ImageUploader from '~/components/Uploader/ImageUploader'
|
||||
import links from '~/constants/links.js'
|
||||
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
|
||||
import DatePicker from 'vue2-datepicker'
|
||||
import 'vue2-datepicker/scss/index.scss'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -109,6 +199,7 @@ export default {
|
||||
ImageUploader,
|
||||
PageParamsLink,
|
||||
CategoriesSelect,
|
||||
DatePicker,
|
||||
},
|
||||
props: {
|
||||
contribution: {
|
||||
@ -119,15 +210,30 @@ export default {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
creatEvent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const { title, content, image, categories } = this.contribution
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
categories,
|
||||
eventStart,
|
||||
eventEnd,
|
||||
eventLocationName,
|
||||
eventVenue,
|
||||
eventIsOnline,
|
||||
eventLocation,
|
||||
} = this.contribution
|
||||
const {
|
||||
sensitive: imageBlurred = false,
|
||||
aspectRatio: imageAspectRatio = null,
|
||||
type: imageType = null,
|
||||
} = image || {}
|
||||
|
||||
return {
|
||||
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
||||
links,
|
||||
@ -139,6 +245,12 @@ export default {
|
||||
imageType,
|
||||
imageBlurred,
|
||||
categoryIds: categories ? categories.map((category) => category.id) : [],
|
||||
eventStart: eventStart || null,
|
||||
eventEnd: eventEnd || null,
|
||||
eventLocation: eventLocation || '',
|
||||
eventLocationName: eventLocationName || '',
|
||||
eventVenue: eventVenue || '',
|
||||
eventIsOnline: eventIsOnline || false,
|
||||
},
|
||||
formSchema: {
|
||||
title: { required: true, min: 3, max: 100 },
|
||||
@ -154,6 +266,9 @@ export default {
|
||||
return []
|
||||
},
|
||||
},
|
||||
eventStart: { required: !!this.creatEvent },
|
||||
eventVenue: { required: !!this.creatEvent, min: 3, max: 100 },
|
||||
eventLocationName: { required: !!this.creatEvent, min: 3, max: 100 },
|
||||
},
|
||||
loading: false,
|
||||
users: [],
|
||||
@ -161,10 +276,25 @@ export default {
|
||||
imageUpload: null,
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await import(`vue2-datepicker/locale/${this.currentUser.locale}`)
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
eventInput() {
|
||||
if (this.creatEvent) {
|
||||
return {
|
||||
eventStart: this.formData.eventStart,
|
||||
eventVenue: this.formData.eventVenue,
|
||||
eventEnd: this.formData.eventEnd,
|
||||
eventIsOnline: this.formData.eventIsOnline,
|
||||
eventLocationName: this.formData.eventLocationName,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
contentLength() {
|
||||
return this.$filters.removeHtml(this.formData.content).length
|
||||
},
|
||||
@ -188,8 +318,21 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
notBeforeToday(date) {
|
||||
return date < new Date().setHours(0, 0, 0, 0)
|
||||
},
|
||||
notBeforeNow(date) {
|
||||
return date < new Date()
|
||||
},
|
||||
notBeforeEventDay(date) {
|
||||
return date < new Date(this.formData.eventStart).setHours(0, 0, 0, 0)
|
||||
},
|
||||
notBeforeEvent(date) {
|
||||
return date <= new Date(this.formData.eventStart)
|
||||
},
|
||||
submit() {
|
||||
let image = null
|
||||
|
||||
const { title, content, categoryIds } = this.formData
|
||||
if (this.formData.image) {
|
||||
image = {
|
||||
@ -202,6 +345,7 @@ export default {
|
||||
}
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: this.contribution.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||
@ -212,6 +356,8 @@ export default {
|
||||
id: this.contribution.id || null,
|
||||
image,
|
||||
groupId: this.groupId,
|
||||
postType: !this.creatEvent ? 'Article' : 'Event',
|
||||
eventInput: this.eventInput,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
@ -288,6 +434,17 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.eventDatas {
|
||||
.chipbox {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
> .ds-chip {
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contribution-form > .base-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -369,5 +526,27 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
.mx-datepicker input {
|
||||
font-size: 1rem;
|
||||
height: calc(1.625rem + 18px);
|
||||
padding: 8px 8px;
|
||||
background-color: #faf9fa;
|
||||
border-color: #c8c8c8;
|
||||
color: #4b4554;
|
||||
}
|
||||
.mx-datepicker input:hover {
|
||||
border-color: #c8c8c8;
|
||||
}
|
||||
.mx-datepicker input:focus {
|
||||
border-color: #17b53f;
|
||||
background-color: #fff;
|
||||
}
|
||||
.mx-datepicker-error {
|
||||
border-color: #cf2619;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -72,9 +72,7 @@ describe('CategoriesFilter.vue', () => {
|
||||
|
||||
describe('click on an "catetories-buttons" button', () => {
|
||||
it('calls TOGGLE_CATEGORY when clicked', () => {
|
||||
environmentAndNatureButton = wrapper
|
||||
.findAll('.categories-filter .item-category .base-button')
|
||||
.at(0)
|
||||
environmentAndNatureButton = wrapper.findAll('.category-filter-list .base-button').at(0)
|
||||
environmentAndNatureButton.trigger('click')
|
||||
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
|
||||
})
|
||||
|
||||
@ -17,18 +17,22 @@
|
||||
</template>
|
||||
|
||||
<template #filter-list>
|
||||
<li v-for="category in categories" :key="category.id" class="item item-category">
|
||||
<labeled-button
|
||||
:icon="category.icon"
|
||||
:filled="filteredCategoryIds.includes(category.id)"
|
||||
:label="$t(`contribution.category.name.${category.slug}`)"
|
||||
<div class="category-filter-list">
|
||||
<base-button
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
@click="toggleCategory(category.id)"
|
||||
:filled="filteredCategoryIds.includes(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.description.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
</li>
|
||||
>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</div>
|
||||
</template>
|
||||
</filter-menu-section>
|
||||
</template>
|
||||
@ -95,3 +99,13 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.category-filter-list {
|
||||
margin-left: $space-xx-large;
|
||||
|
||||
> .base-button {
|
||||
margin-right: $space-xx-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -20,6 +20,7 @@ describe('FilterMenu.vue', () => {
|
||||
|
||||
const stubs = {
|
||||
FollowingFilter: true,
|
||||
PostTypeFilter: true,
|
||||
CategoriesFilter: true,
|
||||
EmotionsFilter: true,
|
||||
LanguagesFilter: true,
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
<div class="filter-menu-options">
|
||||
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
||||
<following-filter />
|
||||
<post-type-filter />
|
||||
<categories-filter v-if="categoriesActive" @showFilterMenu="$emit('showFilterMenu')" />
|
||||
</div>
|
||||
<div class="filter-menu-options">
|
||||
@ -13,6 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PostTypeFilter from './PostTypeFilter'
|
||||
import FollowingFilter from './FollowingFilter'
|
||||
import OrderByFilter from './OrderByFilter'
|
||||
import CategoriesFilter from './CategoriesFilter'
|
||||
@ -22,6 +24,7 @@ export default {
|
||||
FollowingFilter,
|
||||
OrderByFilter,
|
||||
CategoriesFilter,
|
||||
PostTypeFilter,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
59
webapp/components/FilterMenu/PostTypeFilter.vue
Normal file
59
webapp/components/FilterMenu/PostTypeFilter.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<filter-menu-section
|
||||
:title="$t('filter-menu.post-type')"
|
||||
:divider="false"
|
||||
class="following-filter"
|
||||
>
|
||||
<template #filter-follower>
|
||||
<li class="item article-item">
|
||||
<labeled-button
|
||||
icon="book"
|
||||
:label="$t('filter-menu.article')"
|
||||
:filled="articleSet"
|
||||
:title="$t('filter-menu.article')"
|
||||
@click="toggleFilterPostType('Article')"
|
||||
/>
|
||||
</li>
|
||||
<li class="item event-item">
|
||||
<labeled-button
|
||||
icon="calendar"
|
||||
:label="$t('filter-menu.events')"
|
||||
:filled="eventSet"
|
||||
:title="$t('filter-menu.events')"
|
||||
@click="toggleFilterPostType('Event')"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</filter-menu-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||
import LabeledButton from '~/components/_new/generic/LabeledButton/LabeledButton'
|
||||
|
||||
export default {
|
||||
name: 'PostTypeFilter',
|
||||
components: {
|
||||
FilterMenuSection,
|
||||
LabeledButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
filteredPostTypes: 'posts/filteredPostTypes',
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
articleSet() {
|
||||
return this.filteredPostTypes.includes('Article')
|
||||
},
|
||||
eventSet() {
|
||||
return this.filteredPostTypes.includes('Event')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
toggleFilterPostType: 'posts/TOGGLE_POST_TYPE',
|
||||
}),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -28,6 +28,7 @@ describe('PostTeaser', () => {
|
||||
author: {
|
||||
id: 'u1',
|
||||
},
|
||||
postType: ['Article'],
|
||||
},
|
||||
}
|
||||
stubs = {
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
}"
|
||||
:highlight="isPinned"
|
||||
>
|
||||
<!-- {{ post }} -->
|
||||
<template v-if="post.image" #heroImage>
|
||||
<img :src="post.image | proxyApiUrl" class="image" />
|
||||
</template>
|
||||
@ -19,11 +20,37 @@
|
||||
<user-teaser :user="post.author" :group="post.group" :date-time="post.createdAt" />
|
||||
<hc-ribbon
|
||||
:class="[isPinned ? '--pinned' : '', post.image ? 'post-ribbon-w-img' : 'post-ribbon']"
|
||||
:text="isPinned ? $t('post.pinned') : $t('post.name')"
|
||||
:text="ribbonText"
|
||||
:typ="post.postType[0]"
|
||||
/>
|
||||
</div>
|
||||
</client-only>
|
||||
<h2 class="title hyphenate-text">{{ post.title }}</h2>
|
||||
<ds-space
|
||||
v-if="post && post.postType[0] === 'Event'"
|
||||
margin-bottom="small"
|
||||
style="padding: 5px"
|
||||
>
|
||||
<ds-flex>
|
||||
<ds-flex-item>
|
||||
<ds-text align="left" size="small" color="soft" class="event-info">
|
||||
<base-icon name="map-marker" data-test="map-marker" />
|
||||
<span v-if="post.eventIsOnline">
|
||||
{{ $t('post.viewEvent.eventIsOnline') }}
|
||||
</span>
|
||||
<span v-else-if="post.eventLocationName">
|
||||
{{ post.eventLocationName }}
|
||||
</span>
|
||||
</ds-text>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<ds-text align="left" color="soft" size="small" class="event-info">
|
||||
<base-icon name="calendar" data-test="calendar" />
|
||||
<span>{{ getEventDateString }}</span>
|
||||
</ds-text>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-space>
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="content hyphenate-text" v-html="excerpt" />
|
||||
@ -91,6 +118,7 @@ import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import { mapGetters } from 'vuex'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export default {
|
||||
name: 'PostTeaser',
|
||||
@ -152,6 +180,20 @@ export default {
|
||||
isPinned() {
|
||||
return this.post && this.post.pinned
|
||||
},
|
||||
ribbonText() {
|
||||
if (this.post.pinned) return this.$t('post.pinned')
|
||||
if (this.post.postType[0] === 'Event') return this.$t('post.event')
|
||||
return this.$t('post.name')
|
||||
},
|
||||
getEventDateString() {
|
||||
if (this.post.eventEnd) {
|
||||
const eventStart = format(new Date(this.post.eventStart), 'dd.MM.')
|
||||
const eventEnd = format(new Date(this.post.eventEnd), 'dd.MM.yyyy')
|
||||
return `${eventStart} - ${eventEnd}`
|
||||
} else {
|
||||
return format(new Date(this.post.eventStart), 'dd.MM.yyyy')
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async deletePostCallback() {
|
||||
@ -236,6 +278,12 @@ export default {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
& .event-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
> .footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="ribbon">
|
||||
<aside class="ribbon" :class="typ === 'Event' ? 'eventBg' : ''">
|
||||
<p>{{ text }}</p>
|
||||
</aside>
|
||||
</template>
|
||||
@ -12,6 +12,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
typ: {
|
||||
type: String,
|
||||
default: 'blue',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -43,4 +47,11 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
.eventBg {
|
||||
background-color: $color-success-active;
|
||||
|
||||
&::before {
|
||||
border-color: $color-success-active transparent transparent $color-success-active;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,21 +4,30 @@ export default () => {
|
||||
return {
|
||||
CreatePost: gql`
|
||||
mutation (
|
||||
$id: ID
|
||||
$title: String!
|
||||
$slug: String
|
||||
$content: String!
|
||||
$categoryIds: [ID]
|
||||
$image: ImageInput
|
||||
$groupId: ID
|
||||
$postType: PostType
|
||||
$eventInput: _EventInput
|
||||
) {
|
||||
CreatePost(
|
||||
id: $id
|
||||
title: $title
|
||||
slug: $slug
|
||||
content: $content
|
||||
categoryIds: $categoryIds
|
||||
image: $image
|
||||
groupId: $groupId
|
||||
postType: $postType
|
||||
eventInput: $eventInput
|
||||
) {
|
||||
title
|
||||
id
|
||||
slug
|
||||
title
|
||||
content
|
||||
contentExcerpt
|
||||
language
|
||||
@ -26,6 +35,22 @@ export default () => {
|
||||
url
|
||||
sensitive
|
||||
}
|
||||
disabled
|
||||
deleted
|
||||
postType
|
||||
author {
|
||||
name
|
||||
}
|
||||
categories {
|
||||
id
|
||||
}
|
||||
eventStart
|
||||
eventVenue
|
||||
eventLocationName
|
||||
eventLocation {
|
||||
lng
|
||||
lat
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@ -24,6 +24,12 @@ export default (i18n) => {
|
||||
|
||||
query Post($id: ID!) {
|
||||
Post(id: $id) {
|
||||
postType
|
||||
eventStart
|
||||
eventEnd
|
||||
eventVenue
|
||||
eventLocationName
|
||||
eventIsOnline
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
@ -66,6 +72,12 @@ export const filterPosts = (i18n) => {
|
||||
|
||||
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
postType
|
||||
eventStart
|
||||
eventEnd
|
||||
eventVenue
|
||||
eventLocationName
|
||||
eventIsOnline
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
@ -103,6 +115,10 @@ export const profilePagePosts = (i18n) => {
|
||||
$orderBy: [_PostOrdering]
|
||||
) {
|
||||
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
postType
|
||||
eventStart
|
||||
eventVenue
|
||||
eventLocationName
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
|
||||
@ -29,8 +29,9 @@ module.exports = {
|
||||
modulePathIgnorePatterns: ['<rootDir>/dist/'],
|
||||
moduleNameMapper: {
|
||||
'\\.(svg)$': '<rootDir>/test/fileMock.js',
|
||||
'\\.(css|less)$': 'identity-obj-proxy',
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'@mapbox/mapbox-gl-geocoder': 'identity-obj-proxy',
|
||||
'vue2-datepicker/locale/undefined': 'vue2-datepicker/locale/en',
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^~/(.*)$': '<rootDir>/$1',
|
||||
},
|
||||
|
||||
@ -278,6 +278,7 @@
|
||||
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
|
||||
"languageSelectLabel": "Sprache Deines Beitrags",
|
||||
"languageSelectText": "Sprache wählen",
|
||||
"newEvent": "Erstelle einen neue Veranstaltung",
|
||||
"newPost": "Erstelle einen neuen Beitrag",
|
||||
"success": "Gespeichert!",
|
||||
"teaserImage": {
|
||||
@ -377,9 +378,11 @@
|
||||
},
|
||||
"filter-menu": {
|
||||
"all": "Alle",
|
||||
"article": "Artikel",
|
||||
"categories": "Themen",
|
||||
"deleteFilter": "Filter löschen",
|
||||
"emotions": "Emotionen",
|
||||
"events": "Veranstaltungen",
|
||||
"filter-by": "Filtern nach ...",
|
||||
"following": "Nutzer denen ich folge",
|
||||
"languages": "Sprachen",
|
||||
@ -395,6 +398,7 @@
|
||||
}
|
||||
},
|
||||
"order-by": "Sortieren nach ...",
|
||||
"post-type": "Beitrags-Typ",
|
||||
"save": {
|
||||
"error": "Themen konnten nicht gespeichert werden!",
|
||||
"success": "Themen gespeichert!"
|
||||
@ -684,6 +688,12 @@
|
||||
"submitted": "Kommentar gesendet",
|
||||
"updated": "Änderungen gespeichert"
|
||||
},
|
||||
"createNewEvent": {
|
||||
"forGroup": {
|
||||
"title": "Für die Gruppe „{name}“"
|
||||
},
|
||||
"title": "Erstelle ein neues Event"
|
||||
},
|
||||
"createNewPost": {
|
||||
"forGroup": {
|
||||
"title": "Für die Gruppe „{name}“"
|
||||
@ -697,6 +707,7 @@
|
||||
},
|
||||
"title": "Bearbeite deinen Beitrag"
|
||||
},
|
||||
"event": "Veranstaltung",
|
||||
"menu": {
|
||||
"delete": "Beitrag löschen",
|
||||
"edit": "Beitrag bearbeiten",
|
||||
@ -710,6 +721,14 @@
|
||||
"takeAction": {
|
||||
"name": "Aktiv werden"
|
||||
},
|
||||
"viewEvent": {
|
||||
"eventEnd": "Ende",
|
||||
"eventIsOnline": "Online Veranstaltung",
|
||||
"eventLocationName": "Stadt",
|
||||
"eventStart": "Beginn",
|
||||
"eventVenue": "Veranstaltungsort",
|
||||
"title": "Veranstaltung"
|
||||
},
|
||||
"viewPost": {
|
||||
"forGroup": {
|
||||
"title": "In der Gruppe „{name}“"
|
||||
|
||||
@ -278,6 +278,7 @@
|
||||
"inappropriatePicture": "This image may be inappropriate for some people.",
|
||||
"languageSelectLabel": "Language of your contribution",
|
||||
"languageSelectText": "Select Language",
|
||||
"newEvent": "Create a new Event",
|
||||
"newPost": "Create a new Post",
|
||||
"success": "Saved!",
|
||||
"teaserImage": {
|
||||
@ -377,9 +378,11 @@
|
||||
},
|
||||
"filter-menu": {
|
||||
"all": "All",
|
||||
"article": "Article",
|
||||
"categories": "Topics",
|
||||
"deleteFilter": "Delete filter",
|
||||
"emotions": "Emotions",
|
||||
"events": "Events",
|
||||
"filter-by": "Filter by ...",
|
||||
"following": "Users I follow",
|
||||
"languages": "Languages",
|
||||
@ -395,6 +398,7 @@
|
||||
}
|
||||
},
|
||||
"order-by": "Order by ...",
|
||||
"post-type": "Post type",
|
||||
"save": {
|
||||
"error": "Failed saving topic settings!",
|
||||
"success": "Topics saved!"
|
||||
@ -684,6 +688,12 @@
|
||||
"submitted": "Comment submitted!",
|
||||
"updated": "Changes saved!"
|
||||
},
|
||||
"createNewEvent": {
|
||||
"forGroup": {
|
||||
"title": "For The Group “{name}”"
|
||||
},
|
||||
"title": "Create A New Event"
|
||||
},
|
||||
"createNewPost": {
|
||||
"forGroup": {
|
||||
"title": "For The Group “{name}”"
|
||||
@ -697,6 +707,7 @@
|
||||
},
|
||||
"title": "Edit Your Post"
|
||||
},
|
||||
"event": "Event",
|
||||
"menu": {
|
||||
"delete": "Delete post",
|
||||
"edit": "Edit post",
|
||||
@ -710,6 +721,14 @@
|
||||
"takeAction": {
|
||||
"name": "Take action"
|
||||
},
|
||||
"viewEvent": {
|
||||
"eventEnd": "End",
|
||||
"eventIsOnline": "Online Event",
|
||||
"eventLocationName": "City",
|
||||
"eventStart": "Start",
|
||||
"eventVenue": "Venue",
|
||||
"title": "Event"
|
||||
},
|
||||
"viewPost": {
|
||||
"forGroup": {
|
||||
"title": "In The Group “{name}”"
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-sweetalert-icons": "~4.3.1",
|
||||
"vue2-datepicker": "^3.11.1",
|
||||
"vuex-i18n": "~1.13.1",
|
||||
"xregexp": "^4.3.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
|
||||
@ -19,6 +19,7 @@ describe('PostSlug', () => {
|
||||
post: {
|
||||
id: '1',
|
||||
author,
|
||||
postType: ['Article'],
|
||||
comments: [
|
||||
{
|
||||
id: 'comment134',
|
||||
@ -111,6 +112,7 @@ describe('PostSlug', () => {
|
||||
id: '1',
|
||||
author: null,
|
||||
comments: [],
|
||||
postType: ['Article'],
|
||||
},
|
||||
ready: true,
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<transition name="fade" appear>
|
||||
<div>
|
||||
<ds-space margin="small">
|
||||
<ds-heading tag="h1">{{ $t('post.viewPost.title') }}</ds-heading>
|
||||
<ds-heading tag="h1">{{ heading }}</ds-heading>
|
||||
<ds-heading v-if="post && post.group" tag="h2">
|
||||
{{ $t('post.viewPost.forGroup.title', { name: post.group.name }) }}
|
||||
</ds-heading>
|
||||
@ -54,6 +54,33 @@
|
||||
</section>
|
||||
<ds-space margin-bottom="small" />
|
||||
<h2 class="title hyphenate-text">{{ post.title }}</h2>
|
||||
<!-- Eventdata -->
|
||||
<ds-space
|
||||
v-if="post && post.postType[0] === 'Event'"
|
||||
margin-bottom="small"
|
||||
style="padding: 10px"
|
||||
>
|
||||
<ds-text align="left" color="soft">
|
||||
<base-icon name="map-marker" data-test="map-marker" />
|
||||
<span v-if="post.eventVenue">{{ post.eventVenue }}</span>
|
||||
<span v-if="!post.eventIsOnline">
|
||||
<span v-if="post.eventVenue">-</span>
|
||||
{{ post.eventLocationName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-if="post.eventVenue">-</span>
|
||||
{{ $t('post.viewEvent.eventIsOnline') }}
|
||||
</span>
|
||||
</ds-text>
|
||||
<ds-text align="left" color="soft" class="event-info">
|
||||
<base-icon name="calendar" data-test="calendar" />
|
||||
<span>{{ getEventDateString }}</span>
|
||||
</ds-text>
|
||||
<ds-text v-if="getEventTimeString" align="left" color="soft" class="event-info">
|
||||
<base-icon name="clock" data-test="calendar" />
|
||||
<span>{{ getEventTimeString }}</span>
|
||||
</ds-text>
|
||||
</ds-space>
|
||||
<ds-space margin-bottom="small" />
|
||||
<content-viewer class="content hyphenate-text" :content="post.content" />
|
||||
<!-- Categories -->
|
||||
@ -147,6 +174,7 @@ import { groupQuery } from '~/graphql/groups'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import links from '~/constants/links.js'
|
||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export default {
|
||||
name: 'PostSlug',
|
||||
@ -218,6 +246,10 @@ export default {
|
||||
},
|
||||
]
|
||||
},
|
||||
heading() {
|
||||
if (this.post?.postType[0] === 'Event') return this.$t('post.viewEvent.title')
|
||||
return this.$t('post.viewPost.title')
|
||||
},
|
||||
menuModalsData() {
|
||||
return postMenuModalsData(
|
||||
// "this.post" may not always be defined at the beginning …
|
||||
@ -256,6 +288,27 @@ export default {
|
||||
!this.post.group || (this.group && ['usual', 'admin', 'owner'].includes(this.group.myRole))
|
||||
)
|
||||
},
|
||||
getEventDateString() {
|
||||
if (this.post.eventEnd) {
|
||||
const eventStart = format(new Date(this.post.eventStart), 'dd.MM.')
|
||||
const eventEnd = format(new Date(this.post.eventEnd), 'dd.MM.yyyy')
|
||||
return `${eventStart} - ${eventEnd}`
|
||||
} else {
|
||||
return format(new Date(this.post.eventStart), 'dd.MM.yyyy')
|
||||
}
|
||||
},
|
||||
getEventTimeString() {
|
||||
if (this.post.eventEnd) {
|
||||
const eventStartTime = format(new Date(this.post.eventStart), 'HH:mm')
|
||||
const eventEndTime = format(new Date(this.post.eventEnd), 'HH:mm')
|
||||
/* assumption that if e.g. 00:00 == 00:00 is saved,
|
||||
it's not realistic because they are the default values, so don't show the time info.
|
||||
*/
|
||||
return eventStartTime !== eventEndTime ? `${eventStartTime} - ${eventEndTime}` : ''
|
||||
} else {
|
||||
return format(new Date(this.post.eventStart), 'HH:mm')
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reply(message) {
|
||||
@ -374,6 +427,12 @@ export default {
|
||||
filter: blur($blur-radius);
|
||||
}
|
||||
|
||||
& .event-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.blur-toggle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
@ -1,15 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-space margin="small">
|
||||
<ds-heading tag="h1">{{ $t('post.createNewPost.title') }}</ds-heading>
|
||||
<ds-heading v-if="group" tag="h2">
|
||||
{{ $t('post.createNewPost.forGroup.title', { name: group.name }) }}
|
||||
</ds-heading>
|
||||
</ds-space>
|
||||
<ds-space margin="large" />
|
||||
<ds-flex :width="{ base: '100%' }">
|
||||
<ds-flex-item :width="{ base: '100%', md: 5 }">
|
||||
<ds-flex gutter="base" :width="{ base: '100%', sm: 1 }">
|
||||
<ds-flex-item>
|
||||
<ds-card :primary="!creatEvent" centered>
|
||||
<div>
|
||||
<ds-button
|
||||
v-if="!creatEvent"
|
||||
ghost
|
||||
fullwidth
|
||||
size="x-large"
|
||||
class="inactive-tab-button"
|
||||
>
|
||||
{{ $t('post.createNewPost.title') }}
|
||||
</ds-button>
|
||||
<ds-button v-else ghost fullwidth size="x-large" @click="creatEvent = !creatEvent">
|
||||
{{ $t('post.createNewPost.title') }}
|
||||
</ds-button>
|
||||
</div>
|
||||
</ds-card>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<ds-card :primary="!!creatEvent" centered>
|
||||
<div>
|
||||
<ds-button
|
||||
ghost
|
||||
fullwidth
|
||||
size="x-large"
|
||||
v-if="creatEvent"
|
||||
hover
|
||||
class="inactive-tab-button"
|
||||
>
|
||||
{{ $t('post.createNewEvent.title') }}
|
||||
</ds-button>
|
||||
<ds-button ghost fullwidth size="x-large" v-else @click="creatEvent = !creatEvent">
|
||||
{{ $t('post.createNewEvent.title') }}
|
||||
</ds-button>
|
||||
</div>
|
||||
</ds-card>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<div v-if="group" class="group-create-title">
|
||||
{{ $t('post.createNewPost.forGroup.title', { name: group.name }) }}
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', md: 1 }"> </ds-flex-item>
|
||||
</ds-flex>
|
||||
|
||||
<ds-flex :width="{ base: '100%' }" gutter="base">
|
||||
<ds-flex-item :width="{ base: '100%', md: 5 }">
|
||||
<contribution-form :group="group" />
|
||||
<contribution-form :group="group" :creatEvent="creatEvent" />
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', md: 1 }"> </ds-flex-item>
|
||||
</ds-flex>
|
||||
@ -28,6 +69,7 @@ export default {
|
||||
const { groupId = null } = this.$route.query
|
||||
return {
|
||||
groupId,
|
||||
creatEvent: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -58,3 +100,13 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.inactive-tab-button {
|
||||
background-color: #ff000000 !important;
|
||||
color: 'whitesmoke' !important;
|
||||
}
|
||||
.group-create-title {
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -63,6 +63,12 @@ export const mutations = {
|
||||
if (isEmpty(get(filter, 'categories_some.id_in'))) delete filter.categories_some
|
||||
state.filter = filter
|
||||
},
|
||||
TOGGLE_POST_TYPE(state, postType) {
|
||||
const filter = clone(state.filter)
|
||||
update(filter, 'postType_in', (postTypes) => xor(postTypes, [postType]))
|
||||
if (isEmpty(get(filter, 'postType_in'))) delete filter.postType_in
|
||||
state.filter = filter
|
||||
},
|
||||
TOGGLE_LANGUAGE(state, languageCode) {
|
||||
const filter = clone(state.filter)
|
||||
update(filter, 'language_in', (languageCodes) => xor(languageCodes, [languageCode]))
|
||||
@ -90,6 +96,9 @@ export const getters = {
|
||||
filteredCategoryIds(state) {
|
||||
return get(state.filter, 'categories_some.id_in') || []
|
||||
},
|
||||
filteredPostTypes(state) {
|
||||
return get(state.filter, 'postType_in') || []
|
||||
},
|
||||
filteredLanguageCodes(state) {
|
||||
return get(state.filter, 'language_in') || []
|
||||
},
|
||||
|
||||
@ -25,6 +25,18 @@ describe('getters', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('filteredPostTypes', () => {
|
||||
it('returns post types if filter is set', () => {
|
||||
state = { filter: { postType_in: ['Article', 'Event'] } }
|
||||
expect(getters.filteredPostTypes(state)).toEqual(['Article', 'Event'])
|
||||
})
|
||||
|
||||
it('returns empty array if post type filter is not set', () => {
|
||||
state = { filter: { author: { followedBy_some: { id: 7 } } } }
|
||||
expect(getters.filteredPostTypes(state)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('filteredLanguageCodes', () => {
|
||||
it('returns category ids if filter is set', () => {
|
||||
state = { filter: { language_in: ['en', 'de', 'pt'] } }
|
||||
@ -213,6 +225,46 @@ describe('mutations', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('TOGGLE_POST_TYPE', () => {
|
||||
beforeEach(() => {
|
||||
testMutation = (postType) => {
|
||||
mutations.TOGGLE_POST_TYPE(state, postType)
|
||||
return getters.filter(state)
|
||||
}
|
||||
})
|
||||
|
||||
it('creates post type filter if empty', () => {
|
||||
state = { filter: {} }
|
||||
expect(testMutation('Event')).toEqual({ postType_in: ['Event'] })
|
||||
})
|
||||
|
||||
it('adds post type not present', () => {
|
||||
state = { filter: { postType_in: ['Event'] } }
|
||||
expect(testMutation('Article')).toEqual({ postType_in: ['Event', 'Article'] })
|
||||
})
|
||||
|
||||
it('removes category id if present', () => {
|
||||
state = { filter: { postType_in: ['Event', 'Article'] } }
|
||||
const result = testMutation('Event')
|
||||
expect(result).toEqual({ postType_in: ['Article'] })
|
||||
})
|
||||
|
||||
it('removes category filter if empty', () => {
|
||||
state = { filter: { postType_in: ['Event'] } }
|
||||
expect(testMutation('Event')).toEqual({})
|
||||
})
|
||||
|
||||
it('does not get in the way of other filters', () => {
|
||||
state = {
|
||||
filter: {
|
||||
author: { followedBy_some: { id: 7 } },
|
||||
postType_in: ['Event'],
|
||||
},
|
||||
}
|
||||
expect(testMutation('Event')).toEqual({ author: { followedBy_some: { id: 7 } } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('TOGGLE_FILTER_BY_FOLLOWED', () => {
|
||||
beforeEach(() => {
|
||||
testMutation = (userId) => {
|
||||
|
||||
@ -82,6 +82,7 @@ const helpers = {
|
||||
commentsCount: faker.random.number(),
|
||||
clickedCount: faker.random.number(),
|
||||
viewedTeaserCount: faker.random.number(),
|
||||
postType: ['Article'],
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@ -9776,6 +9776,11 @@ date-fns@^1.27.2:
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
||||
|
||||
date-format-parse@^0.2.7:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/date-format-parse/-/date-format-parse-0.2.7.tgz#a2f78bca857a821785b48abedd4426c65aa7b918"
|
||||
integrity sha512-/+lyMUKoRogMuTeOVii6lUwjbVlesN9YRYLzZT/g3TEZ3uD9QnpjResujeEqUW+OSNbT7T1+SYdyEkTcRv+KDQ==
|
||||
|
||||
date-now@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||
@ -22091,6 +22096,13 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue2-datepicker@^3.11.1:
|
||||
version "3.11.1"
|
||||
resolved "https://registry.yarnpkg.com/vue2-datepicker/-/vue2-datepicker-3.11.1.tgz#b2124e15f694d0fd43a92558f6929ec29338d241"
|
||||
integrity sha512-6PU/+pnp2mgZAfnSXmbdwj9516XsEvTiw61Q5SNrvvdy8W/FCxk1GAe9UZn/m9YfS5A47yK6XkcjMHbp7aFApA==
|
||||
dependencies:
|
||||
date-format-parse "^0.2.7"
|
||||
|
||||
vue2-dropzone@3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/vue2-dropzone/-/vue2-dropzone-3.6.0.tgz#b4bb4b64de1cbbb3b88f04b24878e06780a51546"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user