From a6eaea70dcf2bb94a07f8548d1929fff5b860039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 15 Aug 2019 15:47:13 +0200 Subject: [PATCH 01/15] Add UpdateComment support for Mentions in Comment --- backend/src/middleware/index.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index ba5b9d324..c44e19edd 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -1,4 +1,6 @@ -import { applyMiddleware } from 'graphql-middleware' +import { + applyMiddleware +} from 'graphql-middleware' import CONFIG from './../config' import activityPub from './activityPubMiddleware' @@ -12,7 +14,7 @@ import user from './userMiddleware' import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' import validation from './validation/validationMiddleware' -import handleContentData from './handleHtmlContent/handleContentData' +import handleNotifications from './handleNotifications/handleNotifications' import email from './email/emailMiddleware' export default schema => { @@ -23,13 +25,15 @@ export default schema => { validation: validation, sluggify: sluggify, excerpt: excerpt, - handleContentData: handleContentData, + handleNotifications: handleNotifications, xss: xss, softDelete: softDelete, user: user, includedFields: includedFields, orderBy: orderBy, - email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }), + email: email({ + isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT + }), } let order = [ @@ -40,7 +44,7 @@ export default schema => { 'sluggify', 'excerpt', 'email', - 'handleContentData', + 'handleNotifications', 'xss', 'softDelete', 'user', @@ -62,4 +66,4 @@ export default schema => { const appliedMiddlewares = order.map(key => middlewares[key]) return applyMiddleware(schema, ...appliedMiddlewares) -} +} \ No newline at end of file From 5b5489232f13de63fcbe04eee1a7f84eae685fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 15 Aug 2019 15:47:53 +0200 Subject: [PATCH 02/15] Rename files --- .../handleNotifications.js} | 4 ++-- .../handleNotifications.spec.js} | 0 .../hashtags/extractHashtags.js | 0 .../hashtags/extractHashtags.spec.js | 0 .../notifications/extractMentionedUsers.js | 0 .../notifications/extractMentionedUsers.spec.js | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename backend/src/middleware/{handleHtmlContent/handleContentData.js => handleNotifications/handleNotifications.js} (98%) rename backend/src/middleware/{handleHtmlContent/handleContentData.spec.js => handleNotifications/handleNotifications.spec.js} (100%) rename backend/src/middleware/{handleHtmlContent => handleNotifications}/hashtags/extractHashtags.js (100%) rename backend/src/middleware/{handleHtmlContent => handleNotifications}/hashtags/extractHashtags.spec.js (100%) rename backend/src/middleware/{handleHtmlContent => handleNotifications}/notifications/extractMentionedUsers.js (100%) rename backend/src/middleware/{handleHtmlContent => handleNotifications}/notifications/extractMentionedUsers.spec.js (100%) diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.js b/backend/src/middleware/handleNotifications/handleNotifications.js similarity index 98% rename from backend/src/middleware/handleHtmlContent/handleContentData.js rename to backend/src/middleware/handleNotifications/handleNotifications.js index ab0456f9a..d00e6295c 100644 --- a/backend/src/middleware/handleHtmlContent/handleContentData.js +++ b/backend/src/middleware/handleNotifications/handleNotifications.js @@ -86,6 +86,6 @@ export default { CreatePost: handleContentDataOfPost, UpdatePost: handleContentDataOfPost, CreateComment: handleContentDataOfComment, - // UpdateComment: handleContentDataOfComment, + UpdateComment: handleContentDataOfComment, }, -} +} \ No newline at end of file diff --git a/backend/src/middleware/handleHtmlContent/handleContentData.spec.js b/backend/src/middleware/handleNotifications/handleNotifications.spec.js similarity index 100% rename from backend/src/middleware/handleHtmlContent/handleContentData.spec.js rename to backend/src/middleware/handleNotifications/handleNotifications.spec.js diff --git a/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js b/backend/src/middleware/handleNotifications/hashtags/extractHashtags.js similarity index 100% rename from backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.js rename to backend/src/middleware/handleNotifications/hashtags/extractHashtags.js diff --git a/backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js b/backend/src/middleware/handleNotifications/hashtags/extractHashtags.spec.js similarity index 100% rename from backend/src/middleware/handleHtmlContent/hashtags/extractHashtags.spec.js rename to backend/src/middleware/handleNotifications/hashtags/extractHashtags.spec.js diff --git a/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js b/backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.js similarity index 100% rename from backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.js rename to backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.js diff --git a/backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js b/backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.spec.js similarity index 100% rename from backend/src/middleware/handleHtmlContent/notifications/extractMentionedUsers.spec.js rename to backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.spec.js From af70d4f717124940078fd3a9e7bd2bf14db72e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 15 Aug 2019 16:59:42 +0200 Subject: [PATCH 03/15] Get Notification for author for Commenting their Posts --- .../handleNotifications.js | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/src/middleware/handleNotifications/handleNotifications.js b/backend/src/middleware/handleNotifications/handleNotifications.js index d00e6295c..c51bfce25 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.js +++ b/backend/src/middleware/handleNotifications/handleNotifications.js @@ -1,8 +1,8 @@ import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' -const notifyMentions = async (label, id, idsOfMentionedUsers, context) => { - if (!idsOfMentionedUsers.length) return +const notifyUsers = async (label, id, idsOfUsers, context) => { + if (!idsOfUsers.length) return const session = context.driver.session() const createdAt = new Date().toISOString() @@ -11,13 +11,13 @@ const notifyMentions = async (label, id, idsOfMentionedUsers, context) => { WHERE source.id = $id AND $label IN LABELS(source) MATCH (source)<-[:WROTE]-(author: User) MATCH (u: User) - WHERE u.id in $idsOfMentionedUsers + WHERE u.id in $idsOfUsers AND NOT (u)<-[:BLOCKED]-(author) CREATE (n: Notification {id: apoc.create.uuid(), read: false, createdAt: $createdAt }) MERGE (source)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u) ` await session.run(cypher, { - idsOfMentionedUsers, + idsOfUsers, label, createdAt, id, @@ -56,14 +56,14 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => { const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { // extract user ids before xss-middleware removes classes via the following "resolve" call - const idsOfMentionedUsers = extractMentionedUsers(args.content) + const idsOfUsers = extractMentionedUsers(args.content) // extract tag (hashtag) ids before xss-middleware removes classes via the following "resolve" call const hashtags = extractHashtags(args.content) // removes classes from the content const post = await resolve(root, args, context, resolveInfo) - await notifyMentions('Post', post.id, idsOfMentionedUsers, context) + await notifyUsers('Post', post.id, idsOfUsers, context) await updateHashtagsOfPost(post.id, hashtags, context) return post @@ -71,12 +71,35 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => { // extract user ids before xss-middleware removes classes via the following "resolve" call - const idsOfMentionedUsers = extractMentionedUsers(args.content) + const idsOfUsers = extractMentionedUsers(args.content) // removes classes from the content const comment = await resolve(root, args, context, resolveInfo) - await notifyMentions('Comment', comment.id, idsOfMentionedUsers, context) + await notifyUsers('Comment', comment.id, idsOfUsers, context) + + return comment +} + +const handleCreateComment = async (resolve, root, args, context, resolveInfo) => { + // removes classes from the content + const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo) + + const session = context.driver.session() + const cypherFindUser = ` + MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) + RETURN user { .id } + ` + const result = await session.run(cypherFindUser, { + commentId: comment.id, + }) + session.close() + const [userWrotePost] = await result.records.map(record => { + return record.get('user') + }) + if (context.user.id !== userWrotePost.id) { + await notifyUsers('Comment', comment.id, [userWrotePost.id], context) + } return comment } @@ -85,7 +108,7 @@ export default { Mutation: { CreatePost: handleContentDataOfPost, UpdatePost: handleContentDataOfPost, - CreateComment: handleContentDataOfComment, + CreateComment: handleCreateComment, UpdateComment: handleContentDataOfComment, }, } \ No newline at end of file From 8acccc99d0c8ba9d92a0b3ce04e0320ac067a866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 15 Aug 2019 19:29:06 +0200 Subject: [PATCH 04/15] Implemented a `reason` in the Notification Used this for displaying in the mentions menu in frontend. Rewrite backend test. --- .../handleNotifications.js | 24 ++- .../handleNotifications.spec.js | 140 +++++++++++++----- backend/src/middleware/index.js | 8 +- backend/src/models/Notification.js | 24 ++- .../schema/resolvers/notifications.spec.js | 34 +++-- .../src/schema/types/type/Notification.gql | 3 +- backend/src/seed/factories/index.js | 25 +++- .../Notification/Notification.vue | 13 +- .../notifications/NotificationMenu/index.vue | 1 + webapp/locales/de.json | 4 +- webapp/locales/en.json | 4 +- 11 files changed, 207 insertions(+), 73 deletions(-) diff --git a/backend/src/middleware/handleNotifications/handleNotifications.js b/backend/src/middleware/handleNotifications/handleNotifications.js index c51bfce25..6b8368d44 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.js +++ b/backend/src/middleware/handleNotifications/handleNotifications.js @@ -1,9 +1,18 @@ +import { + UserInputError +} from 'apollo-server' import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' -const notifyUsers = async (label, id, idsOfUsers, context) => { +const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return + // Done here, because Neode validation is not working. + const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'] + if (!(reasonsAllowed.includes(reason))) { + throw new UserInputError("Notification reason is not allowed!") + } + const session = context.driver.session() const createdAt = new Date().toISOString() const cypher = ` @@ -13,14 +22,15 @@ const notifyUsers = async (label, id, idsOfUsers, context) => { MATCH (u: User) WHERE u.id in $idsOfUsers AND NOT (u)<-[:BLOCKED]-(author) - CREATE (n: Notification {id: apoc.create.uuid(), read: false, createdAt: $createdAt }) + CREATE (n: Notification { id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) MERGE (source)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u) ` await session.run(cypher, { - idsOfUsers, label, - createdAt, id, + idsOfUsers, + reason, + createdAt, }) session.close() } @@ -63,7 +73,7 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo // removes classes from the content const post = await resolve(root, args, context, resolveInfo) - await notifyUsers('Post', post.id, idsOfUsers, context) + await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context) await updateHashtagsOfPost(post.id, hashtags, context) return post @@ -76,7 +86,7 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI // removes classes from the content const comment = await resolve(root, args, context, resolveInfo) - await notifyUsers('Comment', comment.id, idsOfUsers, context) + await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context) return comment } @@ -98,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => return record.get('user') }) if (context.user.id !== userWrotePost.id) { - await notifyUsers('Comment', comment.id, [userWrotePost.id], context) + await notifyUsers('Comment', comment.id, [userWrotePost.id], 'comment_on_your_post', context) } return comment diff --git a/backend/src/middleware/handleNotifications/handleNotifications.spec.js b/backend/src/middleware/handleNotifications/handleNotifications.spec.js index 40d8a2481..559c1d49a 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.spec.js +++ b/backend/src/middleware/handleNotifications/handleNotifications.spec.js @@ -1,7 +1,14 @@ -import { gql } from '../../jest/helpers' +import { + gql +} from '../../jest/helpers' import Factory from '../../seed/factories' -import { createTestClient } from 'apollo-server-testing' -import { neode, getDriver } from '../../bootstrap/neo4j' +import { + createTestClient +} from 'apollo-server-testing' +import { + neode, + getDriver +} from '../../bootstrap/neo4j' import createServer from '../../server' const factory = Factory() @@ -44,11 +51,12 @@ afterEach(async () => { }) describe('notifications', () => { - const notificationQuery = gql` + const notificationQuery = gql ` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { read + reason post { content } @@ -78,7 +86,7 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' const createPostAction = async () => { - const createPostMutation = gql` + const createPostMutation = gql ` mutation($id: ID, $title: String!, $content: String!) { CreatePost(id: $id, title: $title, content: $content) { id @@ -90,7 +98,11 @@ describe('notifications', () => { authenticatedUser = await author.toJson() await mutate({ mutation: createPostMutation, - variables: { id: 'p47', title, content }, + variables: { + id: 'p47', + title, + content + }, }) authenticatedUser = await user.toJson() } @@ -101,12 +113,27 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' const expected = expect.objectContaining({ data: { - currentUser: { notifications: [{ read: false, post: { content: expectedContent } }] }, + currentUser: { + notifications: [{ + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent + } + }] + }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( - query({ query: notificationQuery, variables: { read: false } }), + query({ + query: notificationQuery, + variables: { + read: false + } + }), ).resolves.toEqual(expected) }) @@ -126,7 +153,7 @@ describe('notifications', () => { @al-capone ` - const updatePostMutation = gql` + const updatePostMutation = gql ` mutation($id: ID!, $title: String!, $content: String!) { UpdatePost(id: $id, content: $content, title: $title) { title @@ -154,15 +181,31 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [ - { read: false, post: { content: expectedContent } }, - { read: false, post: { content: expectedContent } }, + notifications: [{ + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent + } + }, + { + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent + } + }, ], }, }, }) await expect( - query({ query: notificationQuery, variables: { read: false } }), + query({ + query: notificationQuery, + variables: { + read: false + } + }), ).resolves.toEqual(expected) }) }) @@ -175,11 +218,22 @@ describe('notifications', () => { it('sends no notification', async () => { await createPostAction() const expected = expect.objectContaining({ - data: { currentUser: { notifications: [] } }, + data: { + currentUser: { + notifications: [] + } + }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( - query({ query: notificationQuery, variables: { read: false } }), + query({ + query: notificationQuery, + variables: { + read: false + } + }), ).resolves.toEqual(expected) }) }) @@ -193,7 +247,7 @@ describe('Hashtags', () => { const postTitle = 'Two Hashtags' const postContent = '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const postWithHastagsQuery = gql` + const postWithHastagsQuery = gql ` query($id: ID) { Post(id: $id) { tags { @@ -206,7 +260,7 @@ describe('Hashtags', () => { const postWithHastagsVariables = { id: postId, } - const createPostMutation = gql` + const createPostMutation = gql ` mutation($postId: ID, $postTitle: String!, $postContent: String!) { CreatePost(id: $postId, title: $postTitle, content: $postContent) { id @@ -234,20 +288,26 @@ describe('Hashtags', () => { }) it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [ - { id: 'Democracy', name: 'Democracy' }, - { id: 'Liberty', name: 'Liberty' }, + const expected = [{ + id: 'Democracy', + name: 'Democracy' + }, + { + id: 'Liberty', + name: 'Liberty' + }, ] await expect( - query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables + }), ).resolves.toEqual( expect.objectContaining({ data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], + Post: [{ + tags: expect.arrayContaining(expected), + }, ], }, }), ) @@ -257,7 +317,7 @@ describe('Hashtags', () => { // The already existing Hashtag has no class at this point. const updatedPostContent = '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const updatePostMutation = gql` + const updatePostMutation = gql ` mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) { UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) { id @@ -277,16 +337,26 @@ describe('Hashtags', () => { }, }) - const expected = [ - { id: 'Elections', name: 'Elections' }, - { id: 'Liberty', name: 'Liberty' }, + const expected = [{ + id: 'Elections', + name: 'Elections' + }, + { + id: 'Liberty', + name: 'Liberty' + }, ] await expect( - query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables + }), ).resolves.toEqual( expect.objectContaining({ data: { - Post: [{ tags: expect.arrayContaining(expected) }], + Post: [{ + tags: expect.arrayContaining(expected) + }], }, }), ) @@ -294,4 +364,4 @@ describe('Hashtags', () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index c44e19edd..f155f5648 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -1,6 +1,4 @@ -import { - applyMiddleware -} from 'graphql-middleware' +import { applyMiddleware } from 'graphql-middleware' import CONFIG from './../config' import activityPub from './activityPubMiddleware' @@ -32,7 +30,7 @@ export default schema => { includedFields: includedFields, orderBy: orderBy, email: email({ - isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT + isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT, }), } @@ -66,4 +64,4 @@ export default schema => { const appliedMiddlewares = order.map(key => middlewares[key]) return applyMiddleware(schema, ...appliedMiddlewares) -} \ No newline at end of file +} diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js index b8690b8c1..89de27aec 100644 --- a/backend/src/models/Notification.js +++ b/backend/src/models/Notification.js @@ -1,9 +1,25 @@ import uuid from 'uuid/v4' module.exports = { - id: { type: 'uuid', primary: true, default: uuid }, - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - read: { type: 'boolean', default: false }, + id: { + type: 'uuid', + primary: true, + default: uuid, + }, + read: { + type: 'boolean', + default: false, + }, + reason: { + type: 'string', + valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'], + default: 'mentioned_in_post', + }, + createdAt: { + type: 'string', + isoDate: true, + default: () => new Date().toISOString(), + }, user: { type: 'relationship', relationship: 'NOTIFIED', @@ -16,4 +32,4 @@ module.exports = { target: 'Post', direction: 'in', }, -} +} \ No newline at end of file diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 5e7108be3..313376a25 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,6 +1,12 @@ -import { GraphQLClient } from 'graphql-request' +import { + GraphQLClient +} from 'graphql-request' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' +import { + host, + login, + gql +} from '../../jest/helpers' const factory = Factory() let client @@ -19,7 +25,7 @@ afterEach(async () => { }) describe('query for notification', () => { - const notificationQuery = gql` + const notificationQuery = gql ` { Notification { id @@ -61,23 +67,29 @@ describe('currentUser notifications', () => { factory.create('User', neighborParams), factory.create('Notification', { id: 'post-mention-not-for-you', + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'post-mention-already-seen', read: true, + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'post-mention-unseen', + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'comment-mention-not-for-you', + reason: 'mentioned_in_comment', }), factory.create('Notification', { id: 'comment-mention-already-seen', read: true, + reason: 'mentioned_in_comment', }), factory.create('Notification', { id: 'comment-mention-unseen', + reason: 'mentioned_in_comment', }), ]) await factory.authenticateAs(neighborParams) @@ -149,7 +161,7 @@ describe('currentUser notifications', () => { }) describe('filter for read: false', () => { - const queryCurrentUserNotificationsFilterRead = gql` + const queryCurrentUserNotificationsFilterRead = gql ` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -170,8 +182,7 @@ describe('currentUser notifications', () => { it('returns only unread notifications of current user', async () => { const expected = { currentUser: { - notifications: expect.arrayContaining([ - { + notifications: expect.arrayContaining([{ id: 'post-mention-unseen', post: { id: 'p1', @@ -195,7 +206,7 @@ describe('currentUser notifications', () => { }) describe('no filters', () => { - const queryCurrentUserNotifications = gql` + const queryCurrentUserNotifications = gql ` { currentUser { notifications(orderBy: createdAt_desc) { @@ -213,8 +224,7 @@ describe('currentUser notifications', () => { it('returns all notifications of current user', async () => { const expected = { currentUser: { - notifications: expect.arrayContaining([ - { + notifications: expect.arrayContaining([{ id: 'post-mention-unseen', post: { id: 'p1', @@ -255,7 +265,7 @@ describe('currentUser notifications', () => { }) describe('UpdateNotification', () => { - const mutationUpdateNotification = gql` + const mutationUpdateNotification = gql ` mutation($id: ID!, $read: Boolean) { UpdateNotification(id: $id, read: $read) { id @@ -286,9 +296,11 @@ describe('UpdateNotification', () => { factory.create('User', mentionedParams), factory.create('Notification', { id: 'post-mention-to-be-updated', + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'comment-mention-to-be-updated', + reason: 'mentioned_in_comment', }), ]) await factory.authenticateAs(userParams) @@ -390,4 +402,4 @@ describe('UpdateNotification', () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/backend/src/schema/types/type/Notification.gql b/backend/src/schema/types/type/Notification.gql index 0f94c2301..a03b86769 100644 --- a/backend/src/schema/types/type/Notification.gql +++ b/backend/src/schema/types/type/Notification.gql @@ -1,8 +1,9 @@ type Notification { id: ID! read: Boolean + reason: String + createdAt: String user: User @relation(name: "NOTIFIED", direction: "OUT") post: Post @relation(name: "NOTIFIED", direction: "IN") comment: Comment @relation(name: "NOTIFIED", direction: "IN") - createdAt: String } diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index df3886a6c..56518bd06 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -62,15 +62,26 @@ export default function Factory(options = {}) { lastResponse: null, neodeInstance, async authenticateAs({ email, password }) { - const headers = await authenticatedHeaders({ email, password }, seedServerHost) + const headers = await authenticatedHeaders( + { + email, + password, + }, + seedServerHost, + ) this.lastResponse = headers - this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) + this.graphQLClient = new GraphQLClient(seedServerHost, { + headers, + }) return this }, async create(node, args = {}) { const { factory, mutation, variables } = this.factories[node](args) if (factory) { - this.lastResponse = await factory({ args, neodeInstance }) + this.lastResponse = await factory({ + args, + neodeInstance, + }) return this.lastResponse } else { this.lastResponse = await this.graphQLClient.request(mutation, variables) @@ -121,11 +132,15 @@ export default function Factory(options = {}) { }, async invite({ email }) { const mutation = ` mutation($email: String!) { invite( email: $email) } ` - this.lastResponse = await this.graphQLClient.request(mutation, { email }) + this.lastResponse = await this.graphQLClient.request(mutation, { + email, + }) return this }, async cleanDatabase() { - this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) + this.lastResponse = await cleanDatabase({ + driver: this.neo4jDriver, + }) return this }, async emote({ to, data }) { diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index ae1eeddc9..a982e33a9 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -8,9 +8,7 @@ :trunc="35" /> - - {{ $t('notifications.menu.mentioned', { resource: post.id ? 'post' : 'comment' }) }} - + {{ $t(notificationTextIdents[notification.reason]) }} Date: Mon, 19 Aug 2019 22:13:42 +0200 Subject: [PATCH 05/15] Write backend tests Rename files. Co-Authored-By: mattwr18 --- ...ns.js => handleNotificationsMiddleware.js} | 44 ++--- ... => handleNotificationsMiddleware.spec.js} | 167 ++++++++++-------- backend/src/middleware/index.js | 2 +- backend/src/models/Notification.js | 2 +- .../schema/resolvers/notifications.spec.js | 26 ++- 5 files changed, 127 insertions(+), 114 deletions(-) rename backend/src/middleware/handleNotifications/{handleNotifications.js => handleNotificationsMiddleware.js} (80%) rename backend/src/middleware/handleNotifications/{handleNotifications.spec.js => handleNotificationsMiddleware.spec.js} (79%) diff --git a/backend/src/middleware/handleNotifications/handleNotifications.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js similarity index 80% rename from backend/src/middleware/handleNotifications/handleNotifications.js rename to backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js index cbb305fe5..d04eeeece 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js @@ -1,6 +1,4 @@ -import { - UserInputError -} from 'apollo-server' +import { UserInputError } from 'apollo-server' import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' @@ -9,8 +7,8 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { // Done here, because Neode validation is not working. const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'] - if (!(reasonsAllowed.includes(reason))) { - throw new UserInputError("Notification reason is not allowed!") + if (!reasonsAllowed.includes(reason)) { + throw new UserInputError('Notification reason is not allowed!') } const session = context.driver.session() @@ -81,8 +79,10 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo const post = await resolve(root, args, context, resolveInfo) - await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context) - await updateHashtagsOfPost(post.id, hashtags, context) + if (post) { + await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context) + await updateHashtagsOfPost(post.id, hashtags, context) + } return post } @@ -91,7 +91,9 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI const idsOfUsers = extractMentionedUsers(args.content) const comment = await resolve(root, args, context, resolveInfo) - await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context) + if (comment) { + await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context) + } return comment } @@ -100,20 +102,22 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => // removes classes from the content const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo) - const session = context.driver.session() - const cypherFindUser = ` + if (comment) { + const session = context.driver.session() + const cypherFindUser = ` MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) RETURN user { .id } ` - const result = await session.run(cypherFindUser, { - commentId: comment.id, - }) - session.close() - const [userWrotePost] = await result.records.map(record => { - return record.get('user') - }) - if (context.user.id !== userWrotePost.id) { - await notifyUsers('Comment', comment.id, [userWrotePost.id], 'comment_on_your_post', context) + const result = await session.run(cypherFindUser, { + commentId: comment.id, + }) + session.close() + const [postAuthor] = await result.records.map(record => { + return record.get('user') + }) + if (context.user.id !== postAuthor.id) { + await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_your_post', context) + } } return comment @@ -126,4 +130,4 @@ export default { CreateComment: handleCreateComment, UpdateComment: handleContentDataOfComment, }, -} \ No newline at end of file +} diff --git a/backend/src/middleware/handleNotifications/handleNotifications.spec.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js similarity index 79% rename from backend/src/middleware/handleNotifications/handleNotifications.spec.js rename to backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js index 205c1ecb0..cb6aa9844 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.spec.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js @@ -1,14 +1,7 @@ -import { - gql -} from '../../jest/helpers' +import { gql } from '../../jest/helpers' import Factory from '../../seed/factories' -import { - createTestClient -} from 'apollo-server-testing' -import { - neode, - getDriver -} from '../../bootstrap/neo4j' +import { createTestClient } from 'apollo-server-testing' +import { neode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' const factory = Factory() @@ -51,7 +44,7 @@ afterEach(async () => { }) describe('notifications', () => { - const notificationQuery = gql ` + const notificationQuery = gql` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -74,6 +67,30 @@ describe('notifications', () => { }) describe('given another user', () => { + let title + let content + const createPostAction = async () => { + const createPostMutation = gql` + mutation($id: ID, $title: String!, $content: String!) { + CreatePost(id: $id, title: $title, content: $content) { + id + title + content + } + } + ` + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'p47', + title, + content, + }, + }) + authenticatedUser = await user.toJson() + } + let postAuthor beforeEach(async () => { postAuthor = await instance.create('User', { @@ -83,54 +100,42 @@ describe('notifications', () => { }) }) - describe('who mentions me in a post', () => { - const title = 'Mentioning Al Capone' - const content = + describe('comments on my post', () => { + title = 'My post' + content = 'My content' + + // it('sends me a notification', async () => { + // await createPostAction() + // XXX + // }) + }) + + describe('mentions me in a post', () => { + title = 'Mentioning Al Capone' + content = 'Hey @al-capone how do you do?' - const createPostAction = async () => { - const createPostMutation = gql ` - mutation($id: ID, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { - id - title - content - } - } - ` - authenticatedUser = await postAuthor.toJson() - await mutate({ - mutation: createPostMutation, - variables: { - id: 'p47', - title, - content, - }, - }) - authenticatedUser = await user.toJson() - } - - it.only('sends you a notification', async () => { + it('sends you a notification', async () => { await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' const expected = expect.objectContaining({ data: { currentUser: { - notifications: [{ - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, + notifications: [ + { + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent, + }, + comment: null, }, - comment: null, - }, ], + ], }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -157,7 +162,7 @@ describe('notifications', () => { @al-capone ` - const updatePostMutation = gql ` + const updatePostMutation = gql` mutation($id: ID!, $title: String!, $content: String!) { UpdatePost(id: $id, content: $content, title: $title) { title @@ -185,7 +190,8 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [{ + notifications: [ + { read: false, reason: 'mentioned_in_post', post: { @@ -230,9 +236,7 @@ describe('notifications', () => { }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -247,7 +251,7 @@ describe('notifications', () => { describe('but the author of the post blocked me and a mentioner mentions me in a comment', () => { const createCommentOnPostAction = async () => { await createPostAction() - const createCommentMutation = gql ` + const createCommentMutation = gql` mutation($id: ID, $postId: ID!, $commentContent: String!) { CreateComment(id: $id, postId: $postId, content: $commentContent) { id @@ -261,7 +265,8 @@ describe('notifications', () => { variables: { id: 'c47', postId: 'p47', - commentContent: 'One mention of me with .', + commentContent: + 'One mention of me with .', }, }) authenticatedUser = await user.toJson() @@ -288,9 +293,7 @@ describe('notifications', () => { }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -311,7 +314,7 @@ describe('Hashtags', () => { const postTitle = 'Two Hashtags' const postContent = '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const postWithHastagsQuery = gql ` + const postWithHastagsQuery = gql` query($id: ID) { Post(id: $id) { tags { @@ -323,7 +326,7 @@ describe('Hashtags', () => { const postWithHastagsVariables = { id: postId, } - const createPostMutation = gql ` + const createPostMutation = gql` mutation($postId: ID, $postTitle: String!, $postContent: String!) { CreatePost(id: $postId, title: $postTitle, content: $postContent) { id @@ -351,11 +354,14 @@ describe('Hashtags', () => { }) it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [{ - id: 'Democracy' - }, { - id: 'Liberty' - }] + const expected = [ + { + id: 'Democracy', + }, + { + id: 'Liberty', + }, + ] await expect( query({ query: postWithHastagsQuery, @@ -364,9 +370,11 @@ describe('Hashtags', () => { ).resolves.toEqual( expect.objectContaining({ data: { - Post: [{ - tags: expect.arrayContaining(expected), - }, ], + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], }, }), ) @@ -376,7 +384,7 @@ describe('Hashtags', () => { // The already existing Hashtag has no class at this point. const updatedPostContent = '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const updatePostMutation = gql ` + const updatePostMutation = gql` mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) { UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) { id @@ -396,11 +404,14 @@ describe('Hashtags', () => { }, }) - const expected = [{ - id: 'Elections' - }, { - id: 'Liberty' - }] + const expected = [ + { + id: 'Elections', + }, + { + id: 'Liberty', + }, + ] await expect( query({ query: postWithHastagsQuery, @@ -409,9 +420,11 @@ describe('Hashtags', () => { ).resolves.toEqual( expect.objectContaining({ data: { - Post: [{ - tags: expect.arrayContaining(expected), - }, ], + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], }, }), ) @@ -419,4 +432,4 @@ describe('Hashtags', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index f155f5648..9d9ce7e35 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -12,7 +12,7 @@ import user from './userMiddleware' import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' import validation from './validation/validationMiddleware' -import handleNotifications from './handleNotifications/handleNotifications' +import handleNotifications from './handleNotifications/handleNotificationsMiddleware' import email from './email/emailMiddleware' export default schema => { diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js index 89de27aec..93c4cd6cb 100644 --- a/backend/src/models/Notification.js +++ b/backend/src/models/Notification.js @@ -32,4 +32,4 @@ module.exports = { target: 'Post', direction: 'in', }, -} \ No newline at end of file +} diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 313376a25..99dc2fcd3 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,12 +1,6 @@ -import { - GraphQLClient -} from 'graphql-request' +import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { - host, - login, - gql -} from '../../jest/helpers' +import { host, login, gql } from '../../jest/helpers' const factory = Factory() let client @@ -25,7 +19,7 @@ afterEach(async () => { }) describe('query for notification', () => { - const notificationQuery = gql ` + const notificationQuery = gql` { Notification { id @@ -161,7 +155,7 @@ describe('currentUser notifications', () => { }) describe('filter for read: false', () => { - const queryCurrentUserNotificationsFilterRead = gql ` + const queryCurrentUserNotificationsFilterRead = gql` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -182,7 +176,8 @@ describe('currentUser notifications', () => { it('returns only unread notifications of current user', async () => { const expected = { currentUser: { - notifications: expect.arrayContaining([{ + notifications: expect.arrayContaining([ + { id: 'post-mention-unseen', post: { id: 'p1', @@ -206,7 +201,7 @@ describe('currentUser notifications', () => { }) describe('no filters', () => { - const queryCurrentUserNotifications = gql ` + const queryCurrentUserNotifications = gql` { currentUser { notifications(orderBy: createdAt_desc) { @@ -224,7 +219,8 @@ describe('currentUser notifications', () => { it('returns all notifications of current user', async () => { const expected = { currentUser: { - notifications: expect.arrayContaining([{ + notifications: expect.arrayContaining([ + { id: 'post-mention-unseen', post: { id: 'p1', @@ -265,7 +261,7 @@ describe('currentUser notifications', () => { }) describe('UpdateNotification', () => { - const mutationUpdateNotification = gql ` + const mutationUpdateNotification = gql` mutation($id: ID!, $read: Boolean) { UpdateNotification(id: $id, read: $read) { id @@ -402,4 +398,4 @@ describe('UpdateNotification', () => { }) }) }) -}) \ No newline at end of file +}) From 3d2df56141aa67af3172f21fdf28e02c994b825d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 21 Aug 2019 14:27:32 +0200 Subject: [PATCH 06/15] Backend tests --- .../handleNotificationsMiddleware.js | 65 +++-- .../handleNotificationsMiddleware.spec.js | 257 ++++++++++++++---- backend/src/models/Notification.js | 3 +- .../schema/types/enum/ReasonNotification.gql | 5 + .../src/schema/types/type/Notification.gql | 2 +- .../Notification/Notification.vue | 2 +- 6 files changed, 254 insertions(+), 80 deletions(-) create mode 100644 backend/src/schema/types/enum/ReasonNotification.gql diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js index d04eeeece..723dbdc05 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js @@ -6,33 +6,56 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return // Done here, because Neode validation is not working. - const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'] + const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'] if (!reasonsAllowed.includes(reason)) { throw new UserInputError('Notification reason is not allowed!') } + if ( + (label === 'Post' && reason !== 'mentioned_in_post') || + (label === 'Comment' && !['mentioned_in_comment', 'comment_on_post'].includes(reason)) + ) { + throw new UserInputError('Notification fits not to reason!') + } const session = context.driver.session() const createdAt = new Date().toISOString() let cypher - if (label === 'Post') { - cypher = ` - MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) - MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)<-[:BLOCKED]-(author) - CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) - MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) - ` - } else { - cypher = ` - MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) - MATCH (user: User) - WHERE user.id in $idsOfUsers - AND NOT (user)<-[:BLOCKED]-(author) - AND NOT (user)<-[:BLOCKED]-(postAuthor) - CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) - MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) - ` + switch (reason) { + case 'mentioned_in_post': { + cypher = ` + MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) + MATCH (user: User) + WHERE user.id in $idsOfUsers + AND NOT (user)<-[:BLOCKED]-(author) + CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) + MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) + ` + break + } + case 'mentioned_in_comment': { + cypher = ` + MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) + MATCH (user: User) + WHERE user.id in $idsOfUsers + AND NOT (user)<-[:BLOCKED]-(author) + AND NOT (user)<-[:BLOCKED]-(postAuthor) + CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) + MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) + ` + break + } + case 'comment_on_post': { + cypher = ` + MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User) + MATCH (user: User) + WHERE user.id in $idsOfUsers + AND NOT (user)<-[:BLOCKED]-(author) + AND NOT (author)<-[:BLOCKED]-(user) + CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) + MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user) + ` + break + } } await session.run(cypher, { label, @@ -116,7 +139,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => return record.get('user') }) if (context.user.id !== postAuthor.id) { - await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_your_post', context) + await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_post', context) } } diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js index cb6aa9844..f5d2c61c8 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js @@ -63,16 +63,17 @@ describe('notifications', () => { describe('authenticated', () => { beforeEach(async () => { - authenticatedUser = user + authenticatedUser = await user.toJson() }) describe('given another user', () => { - let title - let content + let postTitle + let postContent + let postAuthor const createPostAction = async () => { const createPostMutation = gql` - mutation($id: ID, $title: String!, $content: String!) { - CreatePost(id: $id, title: $title, content: $content) { + mutation($id: ID, $postTitle: String!, $postContent: String!) { + CreatePost(id: $id, title: $postTitle, content: $postContent) { id title content @@ -84,38 +85,154 @@ describe('notifications', () => { mutation: createPostMutation, variables: { id: 'p47', - title, - content, + postTitle, + postContent, }, }) authenticatedUser = await user.toJson() } - let postAuthor - beforeEach(async () => { - postAuthor = await instance.create('User', { - email: 'post-author@example.org', - password: '1234', - id: 'postAuthor', + let commentContent + let commentAuthor + const createCommentOnPostAction = async () => { + await createPostAction() + const createCommentMutation = gql` + mutation($id: ID, $postId: ID!, $commentContent: String!) { + CreateComment(id: $id, postId: $postId, content: $commentContent) { + id + content + } + } + ` + authenticatedUser = await commentAuthor.toJson() + await mutate({ + mutation: createCommentMutation, + variables: { + id: 'c47', + postId: 'p47', + commentContent, + }, + }) + authenticatedUser = await user.toJson() + } + + describe('comments on my post', () => { + beforeEach(async () => { + postTitle = 'My post' + postContent = 'My post content.' + postAuthor = user + }) + + describe('commenter is not me', () => { + beforeEach(async () => { + commentContent = 'Commenters comment.' + commentAuthor = await instance.create('User', { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + email: 'commentauthor@example.org', + password: '1234', + }) + }) + + it('sends me a notification', async () => { + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { + currentUser: { + notifications: [ + { + read: false, + reason: 'comment_on_post', + post: null, + comment: { + content: commentContent, + }, + }, + ], + }, + }, + }) + const { query } = createTestClient(server) + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + + it('sends me no notification if I block the comment author', async () => { + await user.relateTo(commentAuthor, 'blocked') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { + currentUser: { + notifications: [], + }, + }, + }) + const { query } = createTestClient(server) + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + }) + + describe('commenter is me', () => { + beforeEach(async () => { + commentContent = 'My comment.' + commentAuthor = user + }) + + it('sends me no notification', async () => { + await user.relateTo(commentAuthor, 'blocked') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { + currentUser: { + notifications: [], + }, + }, + }) + const { query } = createTestClient(server) + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) }) }) - describe('comments on my post', () => { - title = 'My post' - content = 'My content' - - // it('sends me a notification', async () => { - // await createPostAction() - // XXX - // }) + beforeEach(async () => { + postAuthor = await instance.create('User', { + id: 'postAuthor', + name: 'Mrs Post', + slug: 'mrs-post', + email: 'post-author@example.org', + password: '1234', + }) }) describe('mentions me in a post', () => { - title = 'Mentioning Al Capone' - content = - 'Hey @al-capone how do you do?' + beforeEach(async () => { + postTitle = 'Mentioning Al Capone' + postContent = + 'Hey @al-capone how do you do?' + }) - it('sends you a notification', async () => { + it('sends me a notification', async () => { await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' @@ -163,8 +280,8 @@ describe('notifications', () => { ` const updatePostMutation = gql` - mutation($id: ID!, $title: String!, $content: String!) { - UpdatePost(id: $id, content: $content, title: $title) { + mutation($id: ID!, $postTitle: String!, $postContent: String!) { + UpdatePost(id: $id, content: $postContent, title: $postTitle) { title content } @@ -175,8 +292,8 @@ describe('notifications', () => { mutation: updatePostMutation, variables: { id: 'p47', - title, - content: updatedContent, + postTitle, + postContent: updatedContent, }, }) authenticatedUser = await user.toJson() @@ -247,39 +364,67 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) }) + }) - describe('but the author of the post blocked me and a mentioner mentions me in a comment', () => { - const createCommentOnPostAction = async () => { - await createPostAction() - const createCommentMutation = gql` - mutation($id: ID, $postId: ID!, $commentContent: String!) { - CreateComment(id: $id, postId: $postId, content: $commentContent) { - id - content - } - } - ` - authenticatedUser = await commentMentioner.toJson() - await mutate({ - mutation: createCommentMutation, - variables: { - id: 'c47', - postId: 'p47', - commentContent: - 'One mention of me with .', + describe('mentions me in a comment', () => { + beforeEach(async () => { + postTitle = 'Post where I get mentioned in a comment' + postContent = 'Content of post where I get mentioned in a comment.' + }) + + describe('I am not blocked at all', () => { + beforeEach(async () => { + commentContent = + 'One mention about me with @al-capone.' + commentAuthor = await instance.create('User', { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + email: 'comment-author@example.org', + password: '1234', + }) + }) + + it('sends a notification', async () => { + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { + currentUser: { + notifications: [ + { + read: false, + reason: 'mentioned_in_comment', + post: null, + comment: { + content: commentContent, + }, + }, + ], + }, }, }) - authenticatedUser = await user.toJson() - } - let commentMentioner + const { query } = createTestClient(server) + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + }) + describe('but the author of the post blocked me', () => { beforeEach(async () => { await postAuthor.relateTo(user, 'blocked') - commentMentioner = await instance.create('User', { - id: 'mentioner', - name: 'Mr Mentioner', - slug: 'mr-mentioner', - email: 'mentioner@example.org', + commentContent = + 'One mention about me with @al-capone.' + commentAuthor = await instance.create('User', { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + email: 'comment-author@example.org', password: '1234', }) }) diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js index 93c4cd6cb..b54a99574 100644 --- a/backend/src/models/Notification.js +++ b/backend/src/models/Notification.js @@ -12,7 +12,8 @@ module.exports = { }, reason: { type: 'string', - valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'], + valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'], + invalid: [null], default: 'mentioned_in_post', }, createdAt: { diff --git a/backend/src/schema/types/enum/ReasonNotification.gql b/backend/src/schema/types/enum/ReasonNotification.gql new file mode 100644 index 000000000..a66c446be --- /dev/null +++ b/backend/src/schema/types/enum/ReasonNotification.gql @@ -0,0 +1,5 @@ +enum ReasonNotification { + mentioned_in_post + mentioned_in_comment + comment_on_post +} \ No newline at end of file diff --git a/backend/src/schema/types/type/Notification.gql b/backend/src/schema/types/type/Notification.gql index a03b86769..a3543445f 100644 --- a/backend/src/schema/types/type/Notification.gql +++ b/backend/src/schema/types/type/Notification.gql @@ -1,7 +1,7 @@ type Notification { id: ID! read: Boolean - reason: String + reason: ReasonNotification createdAt: String user: User @relation(name: "NOTIFIED", direction: "OUT") post: Post @relation(name: "NOTIFIED", direction: "IN") diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index 1aa930fc7..8afde5793 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -56,7 +56,7 @@ export default { notificationTextIdents: { mentioned_in_post: 'notifications.menu.mentionedInPost', mentioned_in_comment: 'notifications.menu.mentionedInComment', - comment_on_your_post: 'notifications.menu.commentedOnPost', + comment_on_post: 'notifications.menu.commentedOnPost', }, } }, From 3e91a66673891d75ca71e7efaf465d8a90c3ee72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 22 Aug 2019 08:32:19 +0200 Subject: [PATCH 07/15] Correct wrong merge of master --- .../handleNotifications/handleNotificationsMiddleware.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js index 723dbdc05..6b880cfd7 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js @@ -1,4 +1,6 @@ -import { UserInputError } from 'apollo-server' +import { + UserInputError +} from 'apollo-server' import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' @@ -82,7 +84,7 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => { const cypherCreateNewTagsAndRelations = ` MATCH (p: Post { id: $postId}) UNWIND $hashtags AS tagName - MERGE (t: Tag { id: tagName, name: tagName, disabled: false, deleted: false }) + MERGE (t: Tag { id: tagName, disabled: false, deleted: false }) MERGE (p)-[:TAGGED]->(t) RETURN p, t ` @@ -153,4 +155,4 @@ export default { CreateComment: handleCreateComment, UpdateComment: handleContentDataOfComment, }, -} +} \ No newline at end of file From 793485c0fc5fe9eee1f776874bafc6656c422f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 22 Aug 2019 15:20:40 +0200 Subject: [PATCH 08/15] Write frontend tests --- .../handleNotificationsMiddleware.js | 6 +- .../handleNotificationsMiddleware.spec.js | 126 +++++++-------- .../Notification/Notification.spec.js | 149 ++++++++++++++++-- .../Notification/Notification.vue | 13 +- webapp/graphql/User.js | 2 +- webapp/locales/de.json | 12 +- webapp/locales/en.json | 12 +- 7 files changed, 213 insertions(+), 107 deletions(-) diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js index 6b880cfd7..b56a1384e 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js @@ -1,6 +1,4 @@ -import { - UserInputError -} from 'apollo-server' +import { UserInputError } from 'apollo-server' import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' @@ -155,4 +153,4 @@ export default { CreateComment: handleCreateComment, UpdateComment: handleContentDataOfComment, }, -} \ No newline at end of file +} diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js index d982f318d..3b52d13e0 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js @@ -1,14 +1,7 @@ -import { - gql -} from '../../jest/helpers' +import { gql } from '../../jest/helpers' import Factory from '../../seed/factories' -import { - createTestClient -} from 'apollo-server-testing' -import { - neode, - getDriver -} from '../../bootstrap/neo4j' +import { createTestClient } from 'apollo-server-testing' +import { neode, getDriver } from '../../bootstrap/neo4j' import createServer from '../../server' let server @@ -20,7 +13,7 @@ const factory = Factory() const driver = getDriver() const instance = neode() const categoryIds = ['cat9'] -const createPostMutation = gql ` +const createPostMutation = gql` mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) { id @@ -29,7 +22,7 @@ const createPostMutation = gql ` } } ` -const updatePostMutation = gql ` +const updatePostMutation = gql` mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) { UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) { title @@ -37,7 +30,7 @@ const updatePostMutation = gql ` } } ` -const createCommentMutation = gql ` +const createCommentMutation = gql` mutation($id: ID, $postId: ID!, $commentContent: String!) { CreateComment(id: $id, postId: $postId, content: $commentContent) { id @@ -82,7 +75,7 @@ afterEach(async () => { }) describe('notifications', () => { - const notificationQuery = gql ` + const notificationQuery = gql` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -162,20 +155,20 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [{ - read: false, - reason: 'comment_on_post', - post: null, - comment: { - content: commentContent, + notifications: [ + { + read: false, + reason: 'comment_on_post', + post: null, + comment: { + content: commentContent, + }, }, - }, ], + ], }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -196,9 +189,7 @@ describe('notifications', () => { }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -226,9 +217,7 @@ describe('notifications', () => { }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -265,20 +254,20 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [{ - read: false, - reason: 'mentioned_in_post', - post: { - content: expectedContent, + notifications: [ + { + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent, + }, + comment: null, }, - comment: null, - }, ], + ], }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -326,7 +315,8 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [{ + notifications: [ + { read: false, reason: 'mentioned_in_post', post: { @@ -371,9 +361,7 @@ describe('notifications', () => { }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -410,20 +398,20 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [{ - read: false, - reason: 'mentioned_in_comment', - post: null, - comment: { - content: commentContent, + notifications: [ + { + read: false, + reason: 'mentioned_in_comment', + post: null, + comment: { + content: commentContent, + }, }, - }, ], + ], }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -458,9 +446,7 @@ describe('notifications', () => { }, }, }) - const { - query - } = createTestClient(server) + const { query } = createTestClient(server) await expect( query({ query: notificationQuery, @@ -481,7 +467,7 @@ describe('Hashtags', () => { const title = 'Two Hashtags' const postContent = '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const postWithHastagsQuery = gql ` + const postWithHastagsQuery = gql` query($id: ID) { Post(id: $id) { tags { @@ -513,7 +499,8 @@ describe('Hashtags', () => { }) it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [{ + const expected = [ + { id: 'Democracy', }, { @@ -528,9 +515,11 @@ describe('Hashtags', () => { ).resolves.toEqual( expect.objectContaining({ data: { - Post: [{ - tags: expect.arrayContaining(expected), - }, ], + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], }, }), ) @@ -552,7 +541,8 @@ describe('Hashtags', () => { }, }) - const expected = [{ + const expected = [ + { id: 'Elections', }, { @@ -567,9 +557,11 @@ describe('Hashtags', () => { ).resolves.toEqual( expect.objectContaining({ data: { - Post: [{ - tags: expect.arrayContaining(expected), - }, ], + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], }, }), ) @@ -577,4 +569,4 @@ describe('Hashtags', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/webapp/components/notifications/Notification/Notification.spec.js b/webapp/components/notifications/Notification/Notification.spec.js index 8fbc524fb..8cc5fa8cc 100644 --- a/webapp/components/notifications/Notification/Notification.spec.js +++ b/webapp/components/notifications/Notification/Notification.spec.js @@ -14,10 +14,11 @@ describe('Notification', () => { let stubs let mocks let propsData + let wrapper beforeEach(() => { propsData = {} mocks = { - $t: jest.fn(), + $t: key => key, } stubs = { NuxtLink: RouterLinkStub, @@ -33,37 +34,159 @@ describe('Notification', () => { }) } - describe('given a notification', () => { + describe('given a notification about a comment on a post', () => { beforeEach(() => { propsData.notification = { - post: { - title: "It's a title", - id: 'post-1', - slug: 'its-a-title', - contentExcerpt: '@jenny-rostock is the best', + reason: 'comment_on_post', + post: null, + comment: { + id: 'comment-1', + contentExcerpt: + '@dagobert-duck is the best on this comment.', + post: { + title: "It's a post title", + id: 'post-1', + slug: 'its-a-title', + contentExcerpt: 'Post content.', + }, }, } }) + it('renders reason', () => { + wrapper = Wrapper() + expect(wrapper.find('.reason-text-for-test').text()).toEqual( + 'notifications.menu.comment_on_post', + ) + }) it('renders title', () => { - expect(Wrapper().text()).toContain("It's a title") + wrapper = Wrapper() + expect(wrapper.text()).toContain("It's a post title") + }) + it('renders the "Comment:"', () => { + wrapper = Wrapper() + expect(wrapper.text()).toContain('Comment:') }) - it('renders the contentExcerpt', () => { - expect(Wrapper().text()).toContain('@jenny-rostock is the best') + wrapper = Wrapper() + expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.') }) - it('has no class "read"', () => { - expect(Wrapper().classes()).not.toContain('read') + wrapper = Wrapper() + expect(wrapper.classes()).not.toContain('read') }) describe('that is read', () => { beforeEach(() => { propsData.notification.read = true + wrapper = Wrapper() }) it('has class "read"', () => { - expect(Wrapper().classes()).toContain('read') + expect(wrapper.classes()).toContain('read') + }) + }) + }) + + describe('given a notification about a mention in a post', () => { + beforeEach(() => { + propsData.notification = { + reason: 'mentioned_in_post', + post: { + title: "It's a post title", + id: 'post-1', + slug: 'its-a-title', + contentExcerpt: + '@jenny-rostock is the best on this post.', + }, + comment: null, + } + }) + + it('renders reason', () => { + wrapper = Wrapper() + expect(wrapper.find('.reason-text-for-test').text()).toEqual( + 'notifications.menu.mentioned_in_post', + ) + }) + it('renders title', () => { + wrapper = Wrapper() + expect(wrapper.text()).toContain("It's a post title") + }) + it('renders the contentExcerpt', () => { + wrapper = Wrapper() + expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.') + }) + it('has no class "read"', () => { + wrapper = Wrapper() + expect(wrapper.classes()).not.toContain('read') + }) + + describe('that is read', () => { + beforeEach(() => { + propsData.notification.read = true + wrapper = Wrapper() + }) + + it('has class "read"', () => { + expect(wrapper.classes()).toContain('read') + }) + }) + }) + + describe('given a notification about a mention in a comment', () => { + beforeEach(() => { + propsData.notification = { + reason: 'mentioned_in_comment', + post: null, + comment: { + id: 'comment-1', + contentExcerpt: + '@dagobert-duck is the best on this comment.', + post: { + title: "It's a post title", + id: 'post-1', + slug: 'its-a-title', + contentExcerpt: 'Post content.', + }, + }, + } + }) + + it('renders reason', () => { + wrapper = Wrapper() + expect(wrapper.find('.reason-text-for-test').text()).toEqual( + 'notifications.menu.mentioned_in_comment', + ) + }) + it('renders title', () => { + wrapper = Wrapper() + expect(wrapper.text()).toContain("It's a post title") + }) + + it('renders the "Comment:"', () => { + wrapper = Wrapper() + expect(wrapper.text()).toContain('Comment:') + }) + + it('renders the contentExcerpt', () => { + wrapper = Wrapper() + expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.') + }) + + it('has no class "read"', () => { + wrapper = Wrapper() + expect(wrapper.classes()).not.toContain('read') + }) + + describe('that is read', () => { + beforeEach(() => { + propsData.notification.read = true + wrapper = Wrapper() + }) + + it('has class "read"', () => { + expect(wrapper.classes()).toContain('read') }) }) }) diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index 8afde5793..8c36cb900 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -10,7 +10,9 @@ />
- {{ $t(notificationTextIdents[notification.reason]) }} + + {{ $t(`notifications.menu.${notification.reason}`) }} + { export const currentUserNotificationsQuery = () => { return gql` - { + query { currentUser { id notifications(read: false, orderBy: createdAt_desc) { diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 2328caaf5..e824d565c 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -10,7 +10,7 @@ "all": "Alle" }, "general": { - "header": "Filtern nach..." + "header": "Filtern nach …" }, "followers": { "label": "Benutzern, denen ich folge" @@ -92,7 +92,7 @@ } }, "editor": { - "placeholder": "Schreib etwas Inspirierendes...", + "placeholder": "Schreib etwas Inspirierendes …", "mention": { "noUsersFound": "Keine Benutzer gefunden" }, @@ -124,9 +124,9 @@ }, "notifications": { "menu": { - "mentionedInPost": "Hat dich in einem Beitrag erwähnt …", - "mentionedInComment": "Hat dich in einem Kommentar erwähnt …", - "commentedOnPost": "Hat deinen Beitrag kommentiert …" + "mentioned_in_post": "Hat dich in einem Beitrag erwähnt …", + "mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …", + "comment_on_post": "Hat deinen Beitrag kommentiert …" } }, "search": { @@ -298,7 +298,7 @@ }, "comment": { "content": { - "unavailable-placeholder": "...dieser Kommentar ist nicht mehr verfügbar" + "unavailable-placeholder": "… dieser Kommentar ist nicht mehr verfügbar" }, "menu": { "edit": "Kommentar bearbeiten", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 04f2310f8..edc6e2d28 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -10,7 +10,7 @@ "all": "All" }, "general": { - "header": "Filter by..." + "header": "Filter by …" }, "followers": { "label": "Users I follow" @@ -92,7 +92,7 @@ } }, "editor": { - "placeholder": "Leave your inspirational thoughts...", + "placeholder": "Leave your inspirational thoughts …", "mention": { "noUsersFound": "No users found" }, @@ -124,9 +124,9 @@ }, "notifications": { "menu": { - "mentionedInPost": "Mentioned you in a post …", - "mentionedInComment": "Mentioned you in a comment …", - "commentedOnPost": "Commented on your post …" + "mentioned_in_post": "Mentioned you in a post …", + "mentioned_in_comment": "Mentioned you in a comment …", + "comment_on_post": "Commented on your post …" } }, "search": { @@ -298,7 +298,7 @@ }, "comment": { "content": { - "unavailable-placeholder": "...this comment is not available anymore" + "unavailable-placeholder": "… this comment is not available anymore" }, "menu": { "edit": "Edit Comment", From 2600c69185933525d894f34d89985aa0fd070214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Fri, 23 Aug 2019 15:57:31 +0200 Subject: [PATCH 09/15] Change code after @mattwr18 suggestions Co-Authored-By: mattwr18 --- .../handleNotifications/handleNotificationsMiddleware.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js index b56a1384e..dcd1be1d9 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js +++ b/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js @@ -1,20 +1,19 @@ -import { UserInputError } from 'apollo-server' import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return - // Done here, because Neode validation is not working. + // Checked here, because it does not go through GraphQL checks at all in this file. const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'] if (!reasonsAllowed.includes(reason)) { - throw new UserInputError('Notification reason is not allowed!') + throw new Error('Notification reason is not allowed!') } if ( (label === 'Post' && reason !== 'mentioned_in_post') || (label === 'Comment' && !['mentioned_in_comment', 'comment_on_post'].includes(reason)) ) { - throw new UserInputError('Notification fits not to reason!') + throw new Error('Notification does not fit the reason!') } const session = context.driver.session() From f206d40e86e5847e2ab904d3da10872933faaa31 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 26 Aug 2019 12:15:45 +0200 Subject: [PATCH 10/15] Add memory limits to neo4j deployment I cannot increase the memory above "2G" without getting an error that no node has capacity for it. So I believe we have to change the kubernetes cluster if we want to assign more memory to neo4j. The other settings were suggested to me by neo4j-admin memrec: https://neo4j.com/docs/operations-manual/current/tools/neo4j-admin-memrec/ --- deployment/human-connection/deployment-neo4j.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/deployment/human-connection/deployment-neo4j.yaml b/deployment/human-connection/deployment-neo4j.yaml index 2fcba9061..297f4b551 100644 --- a/deployment/human-connection/deployment-neo4j.yaml +++ b/deployment/human-connection/deployment-neo4j.yaml @@ -25,13 +25,20 @@ - name: nitro-neo4j image: humanconnection/neo4j:latest imagePullPolicy: Always + resources: + requests: + memory: "1G" + limits: + memory: "2G" env: - name: NEO4J_apoc_import_file_enabled value: "true" - name: NEO4J_dbms_memory_pagecache_size - value: 1G + value: "490M" - name: NEO4J_dbms_memory_heap_max__size - value: 1G + value: "500M" + - name: NEO4J_dbms_memory_heap_initial__size + value: "500M" - name: NEO4J_dbms_security_procedures_unrestricted value: "algo.*,apoc.*" envFrom: From af968461b6d5b065386b55fcfd78cc8f942fffba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 26 Aug 2019 16:24:43 +0200 Subject: [PATCH 11/15] Split `handleNotificationsMiddleware` in `notificationsMiddleware` and `hashtagsMiddleware` --- .../hashtags/extractHashtags.js | 0 .../hashtags/extractHashtags.spec.js | 0 .../middleware/hashtags/hashtagsMiddleware.js | 49 +++++ .../hashtags/hashtagsMiddleware.spec.js | 176 ++++++++++++++++++ backend/src/middleware/index.js | 13 +- .../mentions}/extractMentionedUsers.js | 0 .../mentions}/extractMentionedUsers.spec.js | 0 .../notificationsMiddleware.js} | 35 +--- .../notificationsMiddleware.spec.js} | 109 ----------- 9 files changed, 235 insertions(+), 147 deletions(-) rename backend/src/middleware/{handleNotifications => }/hashtags/extractHashtags.js (100%) rename backend/src/middleware/{handleNotifications => }/hashtags/extractHashtags.spec.js (100%) create mode 100644 backend/src/middleware/hashtags/hashtagsMiddleware.js create mode 100644 backend/src/middleware/hashtags/hashtagsMiddleware.spec.js rename backend/src/middleware/{handleNotifications/notifications => notifications/mentions}/extractMentionedUsers.js (100%) rename backend/src/middleware/{handleNotifications/notifications => notifications/mentions}/extractMentionedUsers.spec.js (100%) rename backend/src/middleware/{handleNotifications/handleNotificationsMiddleware.js => notifications/notificationsMiddleware.js} (76%) rename backend/src/middleware/{handleNotifications/handleNotificationsMiddleware.spec.js => notifications/notificationsMiddleware.spec.js} (82%) diff --git a/backend/src/middleware/handleNotifications/hashtags/extractHashtags.js b/backend/src/middleware/hashtags/extractHashtags.js similarity index 100% rename from backend/src/middleware/handleNotifications/hashtags/extractHashtags.js rename to backend/src/middleware/hashtags/extractHashtags.js diff --git a/backend/src/middleware/handleNotifications/hashtags/extractHashtags.spec.js b/backend/src/middleware/hashtags/extractHashtags.spec.js similarity index 100% rename from backend/src/middleware/handleNotifications/hashtags/extractHashtags.spec.js rename to backend/src/middleware/hashtags/extractHashtags.spec.js diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.js b/backend/src/middleware/hashtags/hashtagsMiddleware.js new file mode 100644 index 000000000..c9156398d --- /dev/null +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.js @@ -0,0 +1,49 @@ +import extractHashtags from '../hashtags/extractHashtags' + +const updateHashtagsOfPost = async (postId, hashtags, context) => { + if (!hashtags.length) return + + const session = context.driver.session() + // We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement + // functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted + // and no new Hashtags and relations will be created. + const cypherDeletePreviousRelations = ` + MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag) + DELETE previousRelations + RETURN p, t + ` + const cypherCreateNewTagsAndRelations = ` + MATCH (p: Post { id: $postId}) + UNWIND $hashtags AS tagName + MERGE (t: Tag { id: tagName, disabled: false, deleted: false }) + MERGE (p)-[:TAGGED]->(t) + RETURN p, t + ` + await session.run(cypherDeletePreviousRelations, { + postId, + }) + await session.run(cypherCreateNewTagsAndRelations, { + postId, + hashtags, + }) + session.close() +} + +const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { + const hashtags = extractHashtags(args.content) + + const post = await resolve(root, args, context, resolveInfo) + + if (post) { + await updateHashtagsOfPost(post.id, hashtags, context) + } + + return post +} + +export default { + Mutation: { + CreatePost: handleContentDataOfPost, + UpdatePost: handleContentDataOfPost, + }, +} diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js new file mode 100644 index 000000000..3f101f778 --- /dev/null +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js @@ -0,0 +1,176 @@ +import { gql } from '../../jest/helpers' +import Factory from '../../seed/factories' +import { createTestClient } from 'apollo-server-testing' +import { neode, getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' + +let server +let query +let mutate +let hashtagingUser +let authenticatedUser +const factory = Factory() +const driver = getDriver() +const instance = neode() +const categoryIds = ['cat9'] +const createPostMutation = gql` + mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { + CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) { + id + title + content + } + } +` +const updatePostMutation = gql` + mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) { + UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) { + title + content + } + } +` + +beforeAll(() => { + const createServerResult = createServer({ + context: () => { + return { + user: authenticatedUser, + neode: instance, + driver, + } + }, + }) + server = createServerResult.server + const createTestClientResult = createTestClient(server) + query = createTestClientResult.query + mutate = createTestClientResult.mutate +}) + +beforeEach(async () => { + hashtagingUser = await instance.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + email: 'test@example.org', + password: '1234', + }) + await instance.create('Category', { + id: 'cat9', + name: 'Democracy & Politics', + icon: 'university', + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('hashtags', () => { + const id = 'p135' + const title = 'Two Hashtags' + const postContent = + '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + const postWithHastagsQuery = gql` + query($id: ID) { + Post(id: $id) { + tags { + id + } + } + } + ` + const postWithHastagsVariables = { + id, + } + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await hashtagingUser.toJson() + }) + + describe('create a Post with Hashtags', () => { + beforeEach(async () => { + await mutate({ + mutation: createPostMutation, + variables: { + id, + title, + postContent, + categoryIds, + }, + }) + }) + + it('both hashtags are created with the "id" set to their "name"', async () => { + const expected = [ + { + id: 'Democracy', + }, + { + id: 'Liberty', + }, + ] + await expect( + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], + }, + }), + ) + }) + + describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { + // The already existing Hashtag has no class at this point. + const postContent = + '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' + + it('only one previous Hashtag and the new Hashtag exists', async () => { + await mutate({ + mutation: updatePostMutation, + variables: { + id, + title, + postContent, + categoryIds, + }, + }) + + const expected = [ + { + id: 'Elections', + }, + { + id: 'Liberty', + }, + ] + await expect( + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + Post: [ + { + tags: expect.arrayContaining(expected), + }, + ], + }, + }), + ) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 7b81204df..7774ccc15 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -12,7 +12,8 @@ import user from './userMiddleware' import includedFields from './includedFieldsMiddleware' import orderBy from './orderByMiddleware' import validation from './validation/validationMiddleware' -import handleNotifications from './handleNotifications/handleNotificationsMiddleware' +import notifications from './notifications/notificationsMiddleware' +import hashtags from './hashtags/hashtagsMiddleware' import email from './email/emailMiddleware' import sentry from './sentryMiddleware' @@ -25,13 +26,16 @@ export default schema => { validation, sluggify, excerpt, - handleNotifications, + notifications, + hashtags, xss, softDelete, user, includedFields, orderBy, - email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }), + email: email({ + isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT, + }), } let order = [ @@ -43,7 +47,8 @@ export default schema => { 'sluggify', 'excerpt', 'email', - 'handleNotifications', + 'notifications', + 'hashtags', 'xss', 'softDelete', 'user', diff --git a/backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.js b/backend/src/middleware/notifications/mentions/extractMentionedUsers.js similarity index 100% rename from backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.js rename to backend/src/middleware/notifications/mentions/extractMentionedUsers.js diff --git a/backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.spec.js b/backend/src/middleware/notifications/mentions/extractMentionedUsers.spec.js similarity index 100% rename from backend/src/middleware/handleNotifications/notifications/extractMentionedUsers.spec.js rename to backend/src/middleware/notifications/mentions/extractMentionedUsers.spec.js diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js similarity index 76% rename from backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js rename to backend/src/middleware/notifications/notificationsMiddleware.js index dcd1be1d9..c9dfe406c 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -1,5 +1,4 @@ -import extractMentionedUsers from './notifications/extractMentionedUsers' -import extractHashtags from './hashtags/extractHashtags' +import extractMentionedUsers from './mentions/extractMentionedUsers' const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return @@ -66,44 +65,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => { session.close() } -const updateHashtagsOfPost = async (postId, hashtags, context) => { - if (!hashtags.length) return - - const session = context.driver.session() - // We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement - // functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted - // and no new Hashtags and relations will be created. - const cypherDeletePreviousRelations = ` - MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag) - DELETE previousRelations - RETURN p, t - ` - const cypherCreateNewTagsAndRelations = ` - MATCH (p: Post { id: $postId}) - UNWIND $hashtags AS tagName - MERGE (t: Tag { id: tagName, disabled: false, deleted: false }) - MERGE (p)-[:TAGGED]->(t) - RETURN p, t - ` - await session.run(cypherDeletePreviousRelations, { - postId, - }) - await session.run(cypherCreateNewTagsAndRelations, { - postId, - hashtags, - }) - session.close() -} - const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { const idsOfUsers = extractMentionedUsers(args.content) - const hashtags = extractHashtags(args.content) const post = await resolve(root, args, context, resolveInfo) if (post) { await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context) - await updateHashtagsOfPost(post.id, hashtags, context) } return post @@ -121,7 +89,6 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI } const handleCreateComment = async (resolve, root, args, context, resolveInfo) => { - // removes classes from the content const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo) if (comment) { diff --git a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js similarity index 82% rename from backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js rename to backend/src/middleware/notifications/notificationsMiddleware.spec.js index 3b52d13e0..624cedddc 100644 --- a/backend/src/middleware/handleNotifications/handleNotificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -461,112 +461,3 @@ describe('notifications', () => { }) }) }) - -describe('Hashtags', () => { - const id = 'p135' - const title = 'Two Hashtags' - const postContent = - '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const postWithHastagsQuery = gql` - query($id: ID) { - Post(id: $id) { - tags { - id - } - } - } - ` - const postWithHastagsVariables = { - id, - } - - describe('authenticated', () => { - beforeEach(async () => { - authenticatedUser = await notifiedUser.toJson() - }) - - describe('create a Post with Hashtags', () => { - beforeEach(async () => { - await mutate({ - mutation: createPostMutation, - variables: { - id, - title, - postContent, - categoryIds, - }, - }) - }) - - it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [ - { - id: 'Democracy', - }, - { - id: 'Liberty', - }, - ] - await expect( - query({ - query: postWithHastagsQuery, - variables: postWithHastagsVariables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], - }, - }), - ) - }) - - describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => { - // The already existing Hashtag has no class at this point. - const postContent = - '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - - it('only one previous Hashtag and the new Hashtag exists', async () => { - await mutate({ - mutation: updatePostMutation, - variables: { - id, - title, - postContent, - categoryIds, - }, - }) - - const expected = [ - { - id: 'Elections', - }, - { - id: 'Liberty', - }, - ] - await expect( - query({ - query: postWithHastagsQuery, - variables: postWithHastagsVariables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], - }, - }), - ) - }) - }) - }) - }) -}) From 2af41519185476f0a82a5ba1f961afaf432bcda1 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Mon, 26 Aug 2019 17:09:24 +0200 Subject: [PATCH 12/15] Update to use instead of --- webapp/components/Comment.spec.js | 2 +- webapp/components/Comment.vue | 4 +- .../CommentList/CommentList.spec.js | 2 +- .../ContributionForm/ContributionForm.spec.js | 2 +- .../ContributionForm/ContributionForm.vue | 4 +- webapp/components/CountTo.vue | 4 +- .../EditCommentForm/EditCommentForm.vue | 2 +- webapp/components/PostCard/index.spec.js | 2 +- webapp/components/PostCard/index.vue | 8 ++-- webapp/components/User/index.vue | 4 +- .../Notification/Notification.spec.js | 2 +- .../Notification/Notification.vue | 4 +- .../NotificationList/NotificationList.spec.js | 2 +- webapp/layouts/default.vue | 20 +++++----- webapp/pages/admin/index.vue | 40 +++++++++---------- webapp/pages/index.spec.js | 2 +- webapp/pages/index.vue | 4 +- webapp/pages/login.vue | 4 +- webapp/pages/post/_id/_slug/index.spec.js | 2 +- webapp/pages/post/_id/_slug/index.vue | 4 +- webapp/pages/profile/_id/_slug.spec.js | 2 +- webapp/pages/profile/_id/_slug.vue | 32 +++++++-------- webapp/storybook/config.js | 2 +- webapp/yarn.lock | 6 +-- 24 files changed, 80 insertions(+), 80 deletions(-) diff --git a/webapp/components/Comment.spec.js b/webapp/components/Comment.spec.js index 4fdc48bbd..b9be448e4 100644 --- a/webapp/components/Comment.spec.js +++ b/webapp/components/Comment.spec.js @@ -8,7 +8,7 @@ const localVue = createLocalVue() localVue.use(Vuex) localVue.use(Styleguide) -config.stubs['no-ssr'] = '' +config.stubs['client-only'] = '' describe('Comment.vue', () => { let propsData diff --git a/webapp/components/Comment.vue b/webapp/components/Comment.vue index 6d3b05eff..16d717430 100644 --- a/webapp/components/Comment.vue +++ b/webapp/components/Comment.vue @@ -15,7 +15,7 @@
- + - +
diff --git a/webapp/components/CommentList/CommentList.spec.js b/webapp/components/CommentList/CommentList.spec.js index e1090475a..5551227a1 100644 --- a/webapp/components/CommentList/CommentList.spec.js +++ b/webapp/components/CommentList/CommentList.spec.js @@ -14,7 +14,7 @@ localVue.filter('truncate', string => string) config.stubs['v-popover'] = '' config.stubs['nuxt-link'] = '' -config.stubs['no-ssr'] = '' +config.stubs['client-only'] = '' describe('CommentList.vue', () => { let mocks diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index e1f293c77..90ed226ee 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -16,7 +16,7 @@ localVue.use(Vuex) localVue.use(Styleguide) localVue.use(Filters) -config.stubs['no-ssr'] = '' +config.stubs['client-only'] = '' config.stubs['nuxt-link'] = '' config.stubs['v-popover'] = '' diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index e3daf753d..b54cd4f37 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -14,7 +14,7 @@ {{ form.title.length }}/{{ formSchema.title.max }} - + {{ form.contentLength }}/{{ contentMax }} - + - + - + diff --git a/webapp/components/EditCommentForm/EditCommentForm.vue b/webapp/components/EditCommentForm/EditCommentForm.vue index e647131b8..0a5cd7d98 100644 --- a/webapp/components/EditCommentForm/EditCommentForm.vue +++ b/webapp/components/EditCommentForm/EditCommentForm.vue @@ -2,7 +2,7 @@ diff --git a/webapp/components/User/index.vue b/webapp/components/User/index.vue index 0fefe4eb3..684220f38 100644 --- a/webapp/components/User/index.vue +++ b/webapp/components/User/index.vue @@ -25,9 +25,9 @@
- + - +
diff --git a/webapp/components/notifications/Notification/Notification.spec.js b/webapp/components/notifications/Notification/Notification.spec.js index 8fbc524fb..fce2a94b2 100644 --- a/webapp/components/notifications/Notification/Notification.spec.js +++ b/webapp/components/notifications/Notification/Notification.spec.js @@ -8,7 +8,7 @@ const localVue = createLocalVue() localVue.use(Styleguide) localVue.use(Filters) -config.stubs['no-ssr'] = '' +config.stubs['client-only'] = '' describe('Notification', () => { let stubs diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index 6aa4a5eeb..686938a85 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -1,6 +1,6 @@ diff --git a/webapp/pages/admin/index.vue b/webapp/pages/admin/index.vue index ca8ce4df7..f764238e3 100644 --- a/webapp/pages/admin/index.vue +++ b/webapp/pages/admin/index.vue @@ -1,23 +1,23 @@ diff --git a/webapp/pages/index.spec.js b/webapp/pages/index.spec.js index 6dacd6069..3a97e3709 100644 --- a/webapp/pages/index.spec.js +++ b/webapp/pages/index.spec.js @@ -15,7 +15,7 @@ localVue.use(Filters) localVue.use(VTooltip) localVue.use(InfiniteScroll) -config.stubs['no-ssr'] = '' +config.stubs['client-only'] = '' config.stubs['router-link'] = '' config.stubs['nuxt-link'] = '' diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index 910fa7a2f..1224f6f48 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -36,7 +36,7 @@ - + - +