Merge pull request #6199 from Ocelot-Social-Community/event-master

feat(other): 🍰 epic events – master
This commit is contained in:
Ulf Gebhardt 2023-05-30 16:28:29 +02:00 committed by GitHub
commit 3790f3d010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1600 additions and 86 deletions

View File

@ -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()
}
}

View File

@ -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
}
}
`

View 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(),
}),
])
})
})
})

View 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!')
}

View File

@ -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

View File

@ -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:

View File

@ -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 } }

View File

@ -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,
},
]),
},

View File

@ -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)

View File

@ -0,0 +1,4 @@
enum PostType {
Article
Event
}

View File

@ -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

View File

@ -1,5 +1,5 @@
node_modules
build
dist
.nuxt
styleguide/
**/*.min.js

View 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

View 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

View File

@ -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',
},
}
})

View File

@ -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>

View File

@ -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')
})

View File

@ -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>

View File

@ -20,6 +20,7 @@ describe('FilterMenu.vue', () => {
const stubs = {
FollowingFilter: true,
PostTypeFilter: true,
CategoriesFilter: true,
EmotionsFilter: true,
LanguagesFilter: true,

View File

@ -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 {

View 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>

View File

@ -28,6 +28,7 @@ describe('PostTeaser', () => {
author: {
id: 'u1',
},
postType: ['Article'],
},
}
stubs = {

View File

@ -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;

View File

@ -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>

View File

@ -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
}
}
}
`,

View File

@ -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

View File

@ -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',
},

View File

@ -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}“"

View File

@ -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}”"

View File

@ -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"

View File

@ -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,
}

View File

@ -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;

View File

@ -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 }">&nbsp;</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 }">&nbsp;</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>

View File

@ -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') || []
},

View File

@ -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) => {

View File

@ -82,6 +82,7 @@ const helpers = {
commentsCount: faker.random.number(),
clickedCount: faker.random.number(),
viewedTeaserCount: faker.random.number(),
postType: ['Article'],
}
})
},

View File

@ -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"