mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into post-needs-to-have-category#1222
This commit is contained in:
commit
a000719663
@ -90,7 +90,7 @@
|
||||
"minimatch": "^3.0.4",
|
||||
"neo4j-driver": "~1.7.5",
|
||||
"neo4j-graphql-js": "^2.7.1",
|
||||
"neode": "^0.3.1",
|
||||
"neode": "^0.3.2",
|
||||
"node-fetch": "~2.6.0",
|
||||
"nodemailer": "^6.3.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
@ -98,7 +98,7 @@
|
||||
"sanitize-html": "~1.20.1",
|
||||
"slug": "~1.1.0",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~3.3.2",
|
||||
"uuid": "~3.3.3",
|
||||
"wait-on": "~3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -115,14 +115,14 @@
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~5.1.0",
|
||||
"eslint": "~6.2.0",
|
||||
"eslint-config-prettier": "~6.0.0",
|
||||
"eslint-config-prettier": "~6.1.0",
|
||||
"eslint-config-standard": "~13.0.1",
|
||||
"eslint-plugin-import": "~2.18.2",
|
||||
"eslint-plugin-jest": "~22.15.1",
|
||||
"eslint-plugin-node": "~9.1.0",
|
||||
"eslint-plugin-prettier": "~3.1.0",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
"eslint-plugin-standard": "~4.0.0",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"graphql-request": "~1.8.2",
|
||||
"jest": "~24.9.0",
|
||||
"nodemon": "~1.19.1",
|
||||
|
||||
@ -1,39 +1,57 @@
|
||||
import extractMentionedUsers from './notifications/extractMentionedUsers'
|
||||
import extractHashtags from './hashtags/extractHashtags'
|
||||
|
||||
const notify = async (postId, idsOfMentionedUsers, context) => {
|
||||
const notifyMentions = async (label, id, idsOfMentionedUsers, context) => {
|
||||
if (!idsOfMentionedUsers.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
const createdAt = new Date().toISOString()
|
||||
const cypher = `
|
||||
MATCH(p:Post {id: $postId})<-[:WROTE]-(author:User)
|
||||
MATCH(u:User)
|
||||
WHERE u.id in $idsOfMentionedUsers
|
||||
AND NOT (u)<-[:BLOCKED]-(author)
|
||||
CREATE(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
|
||||
MERGE (p)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u)
|
||||
let cypher
|
||||
if (label === 'Post') {
|
||||
cypher = `
|
||||
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfMentionedUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, 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 $idsOfMentionedUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, createdAt: $createdAt })
|
||||
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
||||
`
|
||||
}
|
||||
await session.run(cypher, {
|
||||
idsOfMentionedUsers,
|
||||
label,
|
||||
createdAt,
|
||||
postId,
|
||||
id,
|
||||
})
|
||||
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)
|
||||
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p:Post { id: $postId})
|
||||
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, name: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
@ -47,24 +65,32 @@ const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
session.close()
|
||||
}
|
||||
|
||||
const handleContentData = async (resolve, root, args, context, resolveInfo) => {
|
||||
// extract user ids before xss-middleware removes classes via the following "resolve" call
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfMentionedUsers = 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 notify(post.id, idsOfMentionedUsers, context)
|
||||
await notifyMentions('Post', post.id, idsOfMentionedUsers, context)
|
||||
await updateHashtagsOfPost(post.id, hashtags, context)
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfMentionedUsers = extractMentionedUsers(args.content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
await notifyMentions('Comment', comment.id, idsOfMentionedUsers, context)
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentData,
|
||||
UpdatePost: handleContentData,
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
CreateComment: handleContentDataOfComment,
|
||||
UpdateComment: handleContentDataOfComment,
|
||||
},
|
||||
}
|
||||
|
||||
@ -75,6 +75,9 @@ describe('notifications', () => {
|
||||
post {
|
||||
content
|
||||
}
|
||||
comment {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,12 +89,12 @@ describe('notifications', () => {
|
||||
})
|
||||
|
||||
describe('given another user', () => {
|
||||
let author
|
||||
let postAuthor
|
||||
beforeEach(async () => {
|
||||
author = await instance.create('User', {
|
||||
email: 'author@example.org',
|
||||
postAuthor = await instance.create('User', {
|
||||
email: 'post-author@example.org',
|
||||
password: '1234',
|
||||
id: 'author',
|
||||
id: 'postAuthor',
|
||||
})
|
||||
})
|
||||
|
||||
@ -101,7 +104,7 @@ describe('notifications', () => {
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
|
||||
const createPostAction = async () => {
|
||||
authenticatedUser = await author.toJson()
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: { id: 'p47', title, content, categoryIds },
|
||||
@ -115,12 +118,27 @@ describe('notifications', () => {
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: { notifications: [{ read: false, post: { content: expectedContent } }] },
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({ query: notificationQuery, variables: { read: false } }),
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
@ -140,7 +158,7 @@ describe('notifications', () => {
|
||||
@al-capone
|
||||
</a>
|
||||
`
|
||||
authenticatedUser = await author.toJson()
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
@ -162,31 +180,114 @@ describe('notifications', () => {
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{ read: false, post: { content: expectedContent } },
|
||||
{ read: false, post: { content: expectedContent } },
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
query({ query: notificationQuery, variables: { read: false } }),
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('but the author of the post blocked me', () => {
|
||||
beforeEach(async () => {
|
||||
await author.relateTo(user, 'blocked')
|
||||
await postAuthor.relateTo(user, 'blocked')
|
||||
})
|
||||
|
||||
it('sends no notification', async () => {
|
||||
await createPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: { currentUser: { notifications: [] } },
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({ query: notificationQuery, variables: { read: false } }),
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).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 <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">.',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
}
|
||||
let commentMentioner
|
||||
|
||||
beforeEach(async () => {
|
||||
await postAuthor.relateTo(user, 'blocked')
|
||||
commentMentioner = await instance.create('User', {
|
||||
id: 'mentioner',
|
||||
name: 'Mr Mentioner',
|
||||
slug: 'mr-mentioner',
|
||||
email: 'mentioner@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends no notification', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
@ -234,7 +335,10 @@ describe('Hashtags', () => {
|
||||
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 }),
|
||||
query({
|
||||
query: postWithHastagsQuery,
|
||||
variables: postWithHastagsVariables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
@ -266,11 +370,18 @@ describe('Hashtags', () => {
|
||||
|
||||
const expected = [{ id: 'Elections' }, { id: '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),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('Notification', () => {
|
||||
const query = gql`
|
||||
const notificationQuery = gql`
|
||||
query {
|
||||
Notification {
|
||||
id
|
||||
@ -38,19 +38,24 @@ describe('Notification', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(query)).rejects.toThrow('Not Authorised')
|
||||
await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('currentUser { notifications }', () => {
|
||||
describe('currentUser notifications', () => {
|
||||
const variables = {}
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
describe('given some notifications', () => {
|
||||
@ -62,24 +67,92 @@ describe('currentUser { notifications }', () => {
|
||||
}
|
||||
await Promise.all([
|
||||
factory.create('User', neighborParams),
|
||||
factory.create('Notification', { id: 'not-for-you' }),
|
||||
factory.create('Notification', { id: 'already-seen', read: true }),
|
||||
factory.create('Notification', {
|
||||
id: 'post-mention-not-for-you',
|
||||
}),
|
||||
factory.create('Notification', {
|
||||
id: 'post-mention-already-seen',
|
||||
read: true,
|
||||
}),
|
||||
factory.create('Notification', {
|
||||
id: 'post-mention-unseen',
|
||||
}),
|
||||
factory.create('Notification', {
|
||||
id: 'comment-mention-not-for-you',
|
||||
}),
|
||||
factory.create('Notification', {
|
||||
id: 'comment-mention-already-seen',
|
||||
read: true,
|
||||
}),
|
||||
factory.create('Notification', {
|
||||
id: 'comment-mention-unseen',
|
||||
}),
|
||||
])
|
||||
await factory.create('Notification', { id: 'unseen' })
|
||||
await factory.authenticateAs(neighborParams)
|
||||
await factory.create('Post', { id: 'p1', categoryIds })
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor' }),
|
||||
factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you', categoryIds }),
|
||||
factory.relate('Notification', 'User', { from: 'unseen', to: 'you' }),
|
||||
factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen', categoryIds }),
|
||||
factory.relate('Notification', 'User', { from: 'already-seen', to: 'you' }),
|
||||
factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen', categoryIds }),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-not-for-you',
|
||||
to: 'neighbor',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-not-for-you',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-unseen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-unseen',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-already-seen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-already-seen',
|
||||
}),
|
||||
])
|
||||
// Comment and its notifications
|
||||
await Promise.all([
|
||||
factory.create('Comment', {
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-not-for-you',
|
||||
to: 'neighbor',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'c1',
|
||||
to: 'comment-mention-not-for-you',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-unseen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'c1',
|
||||
to: 'comment-mention-unseen',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-already-seen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'c1',
|
||||
to: 'comment-mention-already-seen',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe('filter for read: false', () => {
|
||||
const query = gql`
|
||||
const queryCurrentUserNotificationsFilterRead = gql`
|
||||
query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
@ -87,6 +160,9 @@ describe('currentUser { notifications }', () => {
|
||||
post {
|
||||
id
|
||||
}
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,15 +171,32 @@ describe('currentUser { notifications }', () => {
|
||||
it('returns only unread notifications of current user', async () => {
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [{ id: 'unseen', post: { id: 'p1' } }],
|
||||
notifications: expect.arrayContaining([
|
||||
{
|
||||
id: 'post-mention-unseen',
|
||||
post: {
|
||||
id: 'p1',
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
id: 'comment-mention-unseen',
|
||||
post: null,
|
||||
comment: {
|
||||
id: 'c1',
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
await expect(client.request(query, variables)).resolves.toEqual(expected)
|
||||
await expect(
|
||||
client.request(queryCurrentUserNotificationsFilterRead, variables),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('no filters', () => {
|
||||
const query = gql`
|
||||
const queryCurrentUserNotifications = gql`
|
||||
query {
|
||||
currentUser {
|
||||
notifications(orderBy: createdAt_desc) {
|
||||
@ -111,6 +204,9 @@ describe('currentUser { notifications }', () => {
|
||||
post {
|
||||
id
|
||||
}
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,13 +214,41 @@ describe('currentUser { notifications }', () => {
|
||||
it('returns all notifications of current user', async () => {
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{ id: 'unseen', post: { id: 'p1' } },
|
||||
{ id: 'already-seen', post: { id: 'p1' } },
|
||||
],
|
||||
notifications: expect.arrayContaining([
|
||||
{
|
||||
id: 'post-mention-unseen',
|
||||
post: {
|
||||
id: 'p1',
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
id: 'post-mention-already-seen',
|
||||
post: {
|
||||
id: 'p1',
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
id: 'comment-mention-unseen',
|
||||
comment: {
|
||||
id: 'c1',
|
||||
},
|
||||
post: null,
|
||||
},
|
||||
{
|
||||
id: 'comment-mention-already-seen',
|
||||
comment: {
|
||||
id: 'c1',
|
||||
},
|
||||
post: null,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
await expect(client.request(query, variables)).resolves.toEqual(expected)
|
||||
await expect(client.request(queryCurrentUserNotifications, variables)).resolves.toEqual(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -132,7 +256,7 @@ describe('currentUser { notifications }', () => {
|
||||
})
|
||||
|
||||
describe('UpdateNotification', () => {
|
||||
const mutation = gql`
|
||||
const mutationUpdateNotification = gql`
|
||||
mutation($id: ID!, $read: Boolean) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
@ -140,9 +264,16 @@ describe('UpdateNotification', () => {
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = { id: 'to-be-updated', read: true }
|
||||
const variablesPostUpdateNotification = {
|
||||
id: 'post-mention-to-be-updated',
|
||||
read: true,
|
||||
}
|
||||
const variablesCommentUpdateNotification = {
|
||||
id: 'comment-mention-to-be-updated',
|
||||
read: true,
|
||||
}
|
||||
|
||||
describe('given a notifications', () => {
|
||||
describe('given some notifications', () => {
|
||||
let headers
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -152,42 +283,105 @@ describe('UpdateNotification', () => {
|
||||
password: '1234',
|
||||
slug: 'mentioned',
|
||||
}
|
||||
await factory.create('User', mentionedParams)
|
||||
await factory.create('Notification', { id: 'to-be-updated' })
|
||||
await Promise.all([
|
||||
factory.create('User', mentionedParams),
|
||||
factory.create('Notification', {
|
||||
id: 'post-mention-to-be-updated',
|
||||
}),
|
||||
factory.create('Notification', {
|
||||
id: 'comment-mention-to-be-updated',
|
||||
}),
|
||||
])
|
||||
await factory.authenticateAs(userParams)
|
||||
await factory.create('Post', { id: 'p1', categoryIds })
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', { from: 'to-be-updated', to: 'mentioned-1' }),
|
||||
factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-to-be-updated',
|
||||
to: 'mentioned-1',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-to-be-updated',
|
||||
}),
|
||||
])
|
||||
// Comment and its notifications
|
||||
await Promise.all([
|
||||
factory.create('Comment', {
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-to-be-updated',
|
||||
to: 'mentioned-1',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'p1',
|
||||
to: 'comment-mention-to-be-updated',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('and owner', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'mentioned@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
headers = await login({
|
||||
email: 'mentioned@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
it('updates notification', async () => {
|
||||
const expected = { UpdateNotification: { id: 'to-be-updated', read: true } }
|
||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||
it('updates post notification', async () => {
|
||||
const expected = {
|
||||
UpdateNotification: {
|
||||
id: 'post-mention-to-be-updated',
|
||||
read: true,
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('updates comment notification', async () => {
|
||||
const expected = {
|
||||
UpdateNotification: {
|
||||
id: 'comment-mention-to-be-updated',
|
||||
read: true,
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesCommentUpdateNotification),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -123,4 +123,3 @@ type SharedInboxEndpoint {
|
||||
id: ID!
|
||||
uri: String
|
||||
}
|
||||
|
||||
|
||||
@ -3,5 +3,6 @@ type Notification {
|
||||
read: Boolean
|
||||
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
||||
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
||||
comment: Comment @relation(name: "NOTIFIED", direction: "IN")
|
||||
createdAt: String
|
||||
}
|
||||
|
||||
@ -44,42 +44,49 @@ import Factory from './factories'
|
||||
f.create('User', {
|
||||
id: 'u1',
|
||||
name: 'Peter Lustig',
|
||||
slug: 'peter-lustig',
|
||||
role: 'admin',
|
||||
email: 'admin@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u2',
|
||||
name: 'Bob der Baumeister',
|
||||
slug: 'bob-der-baumeister',
|
||||
role: 'moderator',
|
||||
email: 'moderator@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u3',
|
||||
name: 'Jenny Rostock',
|
||||
slug: 'jenny-rostock',
|
||||
role: 'user',
|
||||
email: 'user@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u4',
|
||||
name: 'Tick',
|
||||
name: 'Huey (Tick)',
|
||||
slug: 'huey-tick',
|
||||
role: 'user',
|
||||
email: 'tick@example.org',
|
||||
email: 'huey@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u5',
|
||||
name: 'Trick',
|
||||
name: 'Dewey (Trick)',
|
||||
slug: 'dewey-trick',
|
||||
role: 'user',
|
||||
email: 'trick@example.org',
|
||||
email: 'dewey@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u6',
|
||||
name: 'Track',
|
||||
name: 'Louie (Track)',
|
||||
slug: 'louie-track',
|
||||
role: 'user',
|
||||
email: 'track@example.org',
|
||||
email: 'louie@example.org',
|
||||
}),
|
||||
f.create('User', {
|
||||
id: 'u7',
|
||||
name: 'Dagobert',
|
||||
slug: 'dagobert',
|
||||
role: 'user',
|
||||
email: 'dagobert@example.org',
|
||||
}),
|
||||
@ -99,15 +106,15 @@ import Factory from './factories'
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'tick@example.org',
|
||||
email: 'huey@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'trick@example.org',
|
||||
email: 'dewey@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
Factory().authenticateAs({
|
||||
email: 'track@example.org',
|
||||
email: 'louie@example.org',
|
||||
password: '1234',
|
||||
}),
|
||||
])
|
||||
@ -260,6 +267,10 @@ import Factory from './factories'
|
||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||
const mention2 =
|
||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
||||
const hashtag1 =
|
||||
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||
const hashtagAndMention1 =
|
||||
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u3" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
|
||||
await Promise.all([
|
||||
asAdmin.create('Post', {
|
||||
@ -272,6 +283,8 @@ import Factory from './factories'
|
||||
}),
|
||||
asUser.create('Post', {
|
||||
id: 'p2',
|
||||
title: `Nature Philosophy Yoga`,
|
||||
content: `${hashtag1}`,
|
||||
}),
|
||||
asTick.create('Post', {
|
||||
id: 'p3',
|
||||
@ -293,6 +306,8 @@ import Factory from './factories'
|
||||
asUser.create('Post', {
|
||||
id: 'p8',
|
||||
image: faker.image.unsplash.nature(),
|
||||
title: `Quantum Flow Theory explains Quantum Gravity`,
|
||||
content: `${hashtagAndMention1}`,
|
||||
}),
|
||||
asTick.create('Post', {
|
||||
id: 'p9',
|
||||
@ -639,6 +654,11 @@ import Factory from './factories'
|
||||
}),
|
||||
])
|
||||
|
||||
const mentionInComment1 =
|
||||
'I heard <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, practice it since 3 years now.'
|
||||
const mentionInComment2 =
|
||||
'Did <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> told you?'
|
||||
|
||||
await Promise.all([
|
||||
asUser.create('Comment', {
|
||||
id: 'c1',
|
||||
@ -655,6 +675,12 @@ import Factory from './factories'
|
||||
asTrick.create('Comment', {
|
||||
id: 'c4',
|
||||
postId: 'p2',
|
||||
content: `${mentionInComment1}`,
|
||||
}),
|
||||
asUser.create('Comment', {
|
||||
id: 'c4-1',
|
||||
postId: 'p2',
|
||||
content: `${mentionInComment2}`,
|
||||
}),
|
||||
asModerator.create('Comment', {
|
||||
id: 'c5',
|
||||
|
||||
@ -3267,10 +3267,10 @@ escodegen@^1.9.1:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-prettier@~6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz#f429a53bde9fc7660e6353910fd996d6284d3c25"
|
||||
integrity sha512-vDrcCFE3+2ixNT5H83g28bO/uYAwibJxerXPj+E7op4qzBCsAV36QfvdAyVOoNxKAH2Os/e01T/2x++V0LPukA==
|
||||
eslint-config-prettier@~6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.1.0.tgz#e6f678ba367fbd1273998d5510f76f004e9dce7b"
|
||||
integrity sha512-k9fny9sPjIBQ2ftFTesJV21Rg4R/7a7t7LCtZVrYQiHEp8Nnuk3EGaDmsKSAnsPj0BYcgB2zxzHa2NTkIxcOLg==
|
||||
dependencies:
|
||||
get-stdin "^6.0.0"
|
||||
|
||||
@ -3351,10 +3351,10 @@ eslint-plugin-promise@~4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
|
||||
integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
|
||||
|
||||
eslint-plugin-standard@~4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz#f845b45109c99cd90e77796940a344546c8f6b5c"
|
||||
integrity sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA==
|
||||
eslint-plugin-standard@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4"
|
||||
integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==
|
||||
|
||||
eslint-scope@3.7.1:
|
||||
version "3.7.1"
|
||||
@ -6163,7 +6163,7 @@ neo-async@^2.6.0:
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
|
||||
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
|
||||
|
||||
neo4j-driver@^1.6.3, neo4j-driver@^1.7.3, neo4j-driver@~1.7.5:
|
||||
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.5:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.5.tgz#c3fe3677f69c12f26944563d45e7e7d818a685e4"
|
||||
integrity sha512-xCD2F5+tp/SD9r5avX5bSoY8u8RH2o793xJ9Ikjz1s5qQy7cFxFbbj2c52uz3BVGhRAx/NmB57VjOquYmmxGtw==
|
||||
@ -6184,14 +6184,14 @@ neo4j-graphql-js@^2.7.1:
|
||||
lodash "^4.17.15"
|
||||
neo4j-driver "^1.7.3"
|
||||
|
||||
neode@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.1.tgz#d40147bf20d6951b69c9d392fbdd322aeca07816"
|
||||
integrity sha512-SdaJmdjQ3PWOH6W1H8Xgd2CLyJs+BPPXPt0jOVNs7naeQH8nWPP6ixDqI6NWDCxwecTdNl//fpAicB9I6hCwEw==
|
||||
neode@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.2.tgz#ced277e1daba26a77c48f5857c30af054f11c7df"
|
||||
integrity sha512-Bm4GBXdXunv8cqUUkJtksIGHDnYdBJf4UHwzFgXbJiDKBAdqfjhzwAPAhf1PrvlFmR4vJva2Bh/XvIghYOiKrA==
|
||||
dependencies:
|
||||
"@hapi/joi" "^15.1.0"
|
||||
dotenv "^4.0.0"
|
||||
neo4j-driver "^1.6.3"
|
||||
neo4j-driver "^1.7.5"
|
||||
uuid "^3.3.2"
|
||||
|
||||
next-tick@^1.0.0:
|
||||
@ -8552,10 +8552,10 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
uuid@^3.1.0, uuid@^3.3.2, uuid@~3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
uuid@^3.1.0, uuid@^3.3.2, uuid@~3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
|
||||
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.0.3"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||
import { getLangByName } from "../../support/helpers";
|
||||
import slugify from 'slug'
|
||||
import slugify from "slug";
|
||||
|
||||
/* global cy */
|
||||
|
||||
@ -12,7 +12,7 @@ let loginCredentials = {
|
||||
};
|
||||
const narratorParams = {
|
||||
name: "Peter Pan",
|
||||
slug: 'peter-pan',
|
||||
slug: "peter-pan",
|
||||
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/nerrsoft/128.jpg",
|
||||
...loginCredentials
|
||||
};
|
||||
@ -174,10 +174,10 @@ When("I press {string}", label => {
|
||||
|
||||
Given("we have the following posts in our database:", table => {
|
||||
table.hashes().forEach(({ Author, ...postAttributes }, i) => {
|
||||
Author = Author || `author-${i}`
|
||||
Author = Author || `author-${i}`;
|
||||
const userAttributes = {
|
||||
name: Author,
|
||||
email: `${slugify(Author, {lower: true})}@example.org`,
|
||||
email: `${slugify(Author, { lower: true })}@example.org`,
|
||||
password: "1234"
|
||||
};
|
||||
postAttributes.deleted = Boolean(postAttributes.deleted);
|
||||
@ -363,95 +363,106 @@ Then("there are no notifications in the top menu", () => {
|
||||
cy.get(".notifications-menu").should("contain", "0");
|
||||
});
|
||||
|
||||
Given("there is an annoying user called {string}", (name) => {
|
||||
Given("there is an annoying user called {string}", name => {
|
||||
const annoyingParams = {
|
||||
email: 'spammy-spammer@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
cy.factory().create('User', {
|
||||
email: "spammy-spammer@example.org",
|
||||
password: "1234"
|
||||
};
|
||||
cy.factory().create("User", {
|
||||
...annoyingParams,
|
||||
id: 'annoying-user',
|
||||
id: "annoying-user",
|
||||
name
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
Given("I am on the profile page of the annoying user", (name) => {
|
||||
cy.openPage('/profile/annoying-user/spammy-spammer');
|
||||
})
|
||||
Given("I am on the profile page of the annoying user", name => {
|
||||
cy.openPage("/profile/annoying-user/spammy-spammer");
|
||||
});
|
||||
|
||||
When("I visit the profile page of the annoying user", (name) => {
|
||||
cy.openPage('/profile/annoying-user');
|
||||
})
|
||||
When("I visit the profile page of the annoying user", name => {
|
||||
cy.openPage("/profile/annoying-user");
|
||||
});
|
||||
|
||||
When("I ", (name) => {
|
||||
cy.openPage('/profile/annoying-user');
|
||||
})
|
||||
When("I ", name => {
|
||||
cy.openPage("/profile/annoying-user");
|
||||
});
|
||||
|
||||
When("I click on {string} from the content menu in the user info box", (button) => {
|
||||
cy.get('.user-content-menu .content-menu-trigger')
|
||||
.click()
|
||||
cy.get('.popover .ds-menu-item-link')
|
||||
.contains(button)
|
||||
.click()
|
||||
})
|
||||
When(
|
||||
"I click on {string} from the content menu in the user info box",
|
||||
button => {
|
||||
cy.get(".user-content-menu .content-menu-trigger").click();
|
||||
cy.get(".popover .ds-menu-item-link")
|
||||
.contains(button)
|
||||
.click({ force: true });
|
||||
}
|
||||
);
|
||||
|
||||
When ("I navigate to my {string} settings page", (settingsPage) => {
|
||||
When("I navigate to my {string} settings page", settingsPage => {
|
||||
cy.get(".avatar-menu").click();
|
||||
cy.get(".avatar-menu-popover")
|
||||
.find('a[href]').contains("Settings").click()
|
||||
cy.contains('.ds-menu-item-link', settingsPage).click()
|
||||
})
|
||||
.find("a[href]")
|
||||
.contains("Settings")
|
||||
.click();
|
||||
cy.contains(".ds-menu-item-link", settingsPage).click();
|
||||
});
|
||||
|
||||
Given("I follow the user {string}", (name) => {
|
||||
Given("I follow the user {string}", name => {
|
||||
cy.neode()
|
||||
.first('User', { name }).then((followed) => {
|
||||
.first("User", { name })
|
||||
.then(followed => {
|
||||
cy.neode()
|
||||
.first('User', {name: narratorParams.name})
|
||||
.relateTo(followed, 'following')
|
||||
})
|
||||
})
|
||||
.first("User", { name: narratorParams.name })
|
||||
.relateTo(followed, "following");
|
||||
});
|
||||
});
|
||||
|
||||
Given("\"Spammy Spammer\" wrote a post {string}", (title) => {
|
||||
Given('"Spammy Spammer" wrote a post {string}', title => {
|
||||
cy.factory()
|
||||
.authenticateAs({
|
||||
email: 'spammy-spammer@example.org',
|
||||
password: '1234',
|
||||
email: "spammy-spammer@example.org",
|
||||
password: "1234"
|
||||
})
|
||||
.create("Post", { title })
|
||||
})
|
||||
.create("Post", { title });
|
||||
});
|
||||
|
||||
Then("the list of posts of this user is empty", () => {
|
||||
cy.get('.ds-card-content').not('.post-link')
|
||||
cy.get('.main-container').find('.ds-space.hc-empty')
|
||||
})
|
||||
cy.get(".ds-card-content").not(".post-link");
|
||||
cy.get(".main-container").find(".ds-space.hc-empty");
|
||||
});
|
||||
|
||||
Then("nobody is following the user profile anymore", () => {
|
||||
cy.get('.ds-card-content').not('.post-link')
|
||||
cy.get('.main-container').contains('.ds-card-content', 'is not followed by anyone')
|
||||
})
|
||||
cy.get(".ds-card-content").not(".post-link");
|
||||
cy.get(".main-container").contains(
|
||||
".ds-card-content",
|
||||
"is not followed by anyone"
|
||||
);
|
||||
});
|
||||
|
||||
Given("I wrote a post {string}", (title) => {
|
||||
Given("I wrote a post {string}", title => {
|
||||
cy.factory()
|
||||
.authenticateAs(loginCredentials)
|
||||
.create("Post", { title })
|
||||
})
|
||||
.create("Post", { title });
|
||||
});
|
||||
|
||||
When("I block the user {string}", (name) => {
|
||||
When("I block the user {string}", name => {
|
||||
cy.neode()
|
||||
.first('User', { name }).then((blocked) => {
|
||||
.first("User", { name })
|
||||
.then(blocked => {
|
||||
cy.neode()
|
||||
.first('User', {name: narratorParams.name})
|
||||
.relateTo(blocked, 'blocked')
|
||||
})
|
||||
})
|
||||
.first("User", { name: narratorParams.name })
|
||||
.relateTo(blocked, "blocked");
|
||||
});
|
||||
});
|
||||
|
||||
When("I log in with:", (table) => {
|
||||
const [firstRow] = table.hashes()
|
||||
const { Email, Password } = firstRow
|
||||
cy.login({email: Email, password: Password})
|
||||
})
|
||||
When("I log in with:", table => {
|
||||
const [firstRow] = table.hashes();
|
||||
const { Email, Password } = firstRow;
|
||||
cy.login({ email: Email, password: Password });
|
||||
});
|
||||
|
||||
Then("I see only one post with the title {string}", (title) => {
|
||||
cy.get('.main-container').find('.post-link').should('have.length', 1)
|
||||
cy.get('.main-container').contains('.post-link', title)
|
||||
})
|
||||
Then("I see only one post with the title {string}", title => {
|
||||
cy.get(".main-container")
|
||||
.find(".post-link")
|
||||
.should("have.length", 1);
|
||||
cy.get(".main-container").contains(".post-link", title);
|
||||
});
|
||||
|
||||
@ -32,6 +32,8 @@
|
||||
value: 1G
|
||||
- name: NEO4J_dbms_memory_heap_max__size
|
||||
value: 1G
|
||||
- name: NEO4J_dbms_security_procedures_unrestricted
|
||||
value: "algo.*,apoc.*"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: configmap
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
</ds-card>
|
||||
</div>
|
||||
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
|
||||
<ds-card>
|
||||
<ds-card :id="`commentId-${comment.id}`">
|
||||
<ds-space margin-bottom="small">
|
||||
<hc-user :user="author" :date-time="comment.createdAt" />
|
||||
</ds-space>
|
||||
|
||||
@ -24,9 +24,15 @@ describe('CommentForm.vue', () => {
|
||||
mutate: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: { CreateComment: { contentExcerpt: 'this is a comment' } },
|
||||
data: {
|
||||
CreateComment: {
|
||||
contentExcerpt: 'this is a comment',
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockRejectedValue({ message: 'Ouch!' }),
|
||||
.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
}),
|
||||
},
|
||||
$toast: {
|
||||
error: jest.fn(),
|
||||
@ -34,7 +40,9 @@ describe('CommentForm.vue', () => {
|
||||
},
|
||||
}
|
||||
propsData = {
|
||||
post: { id: 1 },
|
||||
post: {
|
||||
id: 1,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -49,7 +57,12 @@ describe('CommentForm.vue', () => {
|
||||
getters,
|
||||
})
|
||||
const Wrapper = () => {
|
||||
return mount(CommentForm, { mocks, localVue, propsData, store })
|
||||
return mount(CommentForm, {
|
||||
mocks,
|
||||
localVue,
|
||||
propsData,
|
||||
store,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -2,7 +2,13 @@
|
||||
<ds-form v-show="!editPending" v-model="form" @submit="handleSubmit">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card>
|
||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||
<hc-editor
|
||||
ref="editor"
|
||||
:users="users"
|
||||
:hashtags="null"
|
||||
:value="form.content"
|
||||
@input="updateEditorContent"
|
||||
/>
|
||||
<ds-space />
|
||||
<ds-flex :gutter="{ base: 'small', md: 'small', sm: 'x-large', xs: 'x-large' }">
|
||||
<ds-flex-item :width="{ base: '0%', md: '50%', sm: '0%', xs: '0%' }" />
|
||||
|
||||
@ -16,6 +16,22 @@ describe('Editor.vue', () => {
|
||||
let mocks
|
||||
let getters
|
||||
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return (wrapper = mount(Editor, {
|
||||
mocks,
|
||||
propsData,
|
||||
localVue,
|
||||
sync: false,
|
||||
stubs: {
|
||||
transition: false,
|
||||
},
|
||||
store,
|
||||
}))
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
mocks = {
|
||||
@ -26,25 +42,10 @@ describe('Editor.vue', () => {
|
||||
return 'some cool placeholder'
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
let Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return (wrapper = mount(Editor, {
|
||||
mocks,
|
||||
propsData,
|
||||
localVue,
|
||||
sync: false,
|
||||
stubs: {
|
||||
transition: false,
|
||||
},
|
||||
store,
|
||||
}))
|
||||
}
|
||||
|
||||
it('renders', () => {
|
||||
expect(Wrapper().is('div')).toBe(true)
|
||||
})
|
||||
@ -67,5 +68,43 @@ describe('Editor.vue', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('optional extensions', () => {
|
||||
it('sets the Mention items to the users', () => {
|
||||
propsData.users = [
|
||||
{
|
||||
id: 'u345',
|
||||
},
|
||||
]
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.vm.editor.extensions.options.mention.items()).toEqual(propsData.users)
|
||||
})
|
||||
|
||||
it('mentions is not an option when there are no users', () => {
|
||||
expect(wrapper.vm.editor.extensions.options).toEqual(
|
||||
expect.not.objectContaining({
|
||||
mention: expect.anything(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('sets the Hashtag items to the hashtags', () => {
|
||||
propsData.hashtags = [
|
||||
{
|
||||
id: 'Frieden',
|
||||
},
|
||||
]
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.vm.editor.extensions.options.hashtag.items()).toEqual(propsData.hashtags)
|
||||
})
|
||||
|
||||
it('hashtags is not an option when there are no hashtags', () => {
|
||||
expect(wrapper.vm.editor.extensions.options).toEqual(
|
||||
expect.not.objectContaining({
|
||||
hashtag: expect.anything(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -200,12 +200,173 @@ export default {
|
||||
EditorMenuBubble,
|
||||
},
|
||||
props: {
|
||||
users: { type: Array, default: () => [] },
|
||||
hashtags: { type: Array, default: () => [] },
|
||||
users: { type: Array, default: () => null }, // If 'null', than the Mention extention is not assigned.
|
||||
hashtags: { type: Array, default: () => null }, // If 'null', than the Hashtag extention is not assigned.
|
||||
value: { type: String, default: '' },
|
||||
doc: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
// Set array of optional extensions by analysing the props.
|
||||
let optionalExtensions = []
|
||||
// Don't change the following line. The functionallity is in danger!
|
||||
if (this.users) {
|
||||
optionalExtensions.push(
|
||||
new Mention({
|
||||
// a list of all suggested items
|
||||
items: () => {
|
||||
return this.users
|
||||
},
|
||||
// is called when a suggestion starts
|
||||
onEnter: ({ items, query, range, command, virtualNode }) => {
|
||||
this.suggestionType = this.mentionSuggestionType
|
||||
|
||||
this.query = query
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.renderPopup(virtualNode)
|
||||
// we save the command for inserting a selected mention
|
||||
// this allows us to call it inside of our custom popup
|
||||
// via keyboard navigation and on click
|
||||
this.insertMentionOrHashtag = command
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
onChange: ({ items, query, range, virtualNode }) => {
|
||||
this.query = query
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.navigatedItemIndex = 0
|
||||
this.renderPopup(virtualNode)
|
||||
},
|
||||
// is called when a suggestion is cancelled
|
||||
onExit: () => {
|
||||
this.suggestionType = this.nullSuggestionType
|
||||
|
||||
// reset all saved values
|
||||
this.query = null
|
||||
this.filteredItems = []
|
||||
this.suggestionRange = null
|
||||
this.navigatedItemIndex = 0
|
||||
this.destroyPopup()
|
||||
},
|
||||
// is called on every keyDown event while a suggestion is active
|
||||
onKeyDown: ({ event }) => {
|
||||
// pressing up arrow
|
||||
if (event.keyCode === 38) {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
// pressing down arrow
|
||||
if (event.keyCode === 40) {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
// pressing enter
|
||||
if (event.keyCode === 13) {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
// this function is optional because there is basic filtering built-in
|
||||
// you can overwrite it if you prefer your own filtering
|
||||
// in this example we use fuse.js with support for fuzzy search
|
||||
onFilter: (items, query) => {
|
||||
if (!query) {
|
||||
return items
|
||||
}
|
||||
const fuse = new Fuse(items, {
|
||||
threshold: 0.2,
|
||||
keys: ['slug'],
|
||||
})
|
||||
return fuse.search(query)
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
// Don't change the following line. The functionallity is in danger!
|
||||
if (this.hashtags) {
|
||||
optionalExtensions.push(
|
||||
new Hashtag({
|
||||
// a list of all suggested items
|
||||
items: () => {
|
||||
return this.hashtags
|
||||
},
|
||||
// is called when a suggestion starts
|
||||
onEnter: ({ items, query, range, command, virtualNode }) => {
|
||||
this.suggestionType = this.hashtagSuggestionType
|
||||
|
||||
this.query = this.sanitizedQuery(query)
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.renderPopup(virtualNode)
|
||||
// we save the command for inserting a selected mention
|
||||
// this allows us to call it inside of our custom popup
|
||||
// via keyboard navigation and on click
|
||||
this.insertMentionOrHashtag = command
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
onChange: ({ items, query, range, virtualNode }) => {
|
||||
this.query = this.sanitizedQuery(query)
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.navigatedItemIndex = 0
|
||||
this.renderPopup(virtualNode)
|
||||
},
|
||||
// is called when a suggestion is cancelled
|
||||
onExit: () => {
|
||||
this.suggestionType = this.nullSuggestionType
|
||||
|
||||
// reset all saved values
|
||||
this.query = null
|
||||
this.filteredItems = []
|
||||
this.suggestionRange = null
|
||||
this.navigatedItemIndex = 0
|
||||
this.destroyPopup()
|
||||
},
|
||||
// is called on every keyDown event while a suggestion is active
|
||||
onKeyDown: ({ event }) => {
|
||||
// pressing up arrow
|
||||
if (event.keyCode === 38) {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
// pressing down arrow
|
||||
if (event.keyCode === 40) {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
// pressing enter
|
||||
if (event.keyCode === 13) {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
// pressing space
|
||||
if (event.keyCode === 32) {
|
||||
this.spaceHandler()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
// this function is optional because there is basic filtering built-in
|
||||
// you can overwrite it if you prefer your own filtering
|
||||
// in this example we use fuse.js with support for fuzzy search
|
||||
onFilter: (items, query) => {
|
||||
query = this.sanitizedQuery(query)
|
||||
if (!query) {
|
||||
return items
|
||||
}
|
||||
return items.filter(item =>
|
||||
JSON.stringify(item)
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase()),
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
lastValueHash: null,
|
||||
editor: new Editor({
|
||||
@ -215,154 +376,7 @@ export default {
|
||||
...defaultExtensions(this),
|
||||
new EventHandler(),
|
||||
new History(),
|
||||
new Mention({
|
||||
// a list of all suggested items
|
||||
items: () => {
|
||||
return this.users
|
||||
},
|
||||
// is called when a suggestion starts
|
||||
onEnter: ({ items, query, range, command, virtualNode }) => {
|
||||
this.suggestionType = this.mentionSuggestionType
|
||||
|
||||
this.query = query
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.renderPopup(virtualNode)
|
||||
// we save the command for inserting a selected mention
|
||||
// this allows us to call it inside of our custom popup
|
||||
// via keyboard navigation and on click
|
||||
this.insertMentionOrHashtag = command
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
onChange: ({ items, query, range, virtualNode }) => {
|
||||
this.query = query
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.navigatedItemIndex = 0
|
||||
this.renderPopup(virtualNode)
|
||||
},
|
||||
// is called when a suggestion is cancelled
|
||||
onExit: () => {
|
||||
this.suggestionType = this.nullSuggestionType
|
||||
|
||||
// reset all saved values
|
||||
this.query = null
|
||||
this.filteredItems = []
|
||||
this.suggestionRange = null
|
||||
this.navigatedItemIndex = 0
|
||||
this.destroyPopup()
|
||||
},
|
||||
// is called on every keyDown event while a suggestion is active
|
||||
onKeyDown: ({ event }) => {
|
||||
// pressing up arrow
|
||||
if (event.keyCode === 38) {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
// pressing down arrow
|
||||
if (event.keyCode === 40) {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
// pressing enter
|
||||
if (event.keyCode === 13) {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
// this function is optional because there is basic filtering built-in
|
||||
// you can overwrite it if you prefer your own filtering
|
||||
// in this example we use fuse.js with support for fuzzy search
|
||||
onFilter: (items, query) => {
|
||||
if (!query) {
|
||||
return items
|
||||
}
|
||||
const fuse = new Fuse(items, {
|
||||
threshold: 0.2,
|
||||
keys: ['slug'],
|
||||
})
|
||||
return fuse.search(query)
|
||||
},
|
||||
}),
|
||||
new Hashtag({
|
||||
// a list of all suggested items
|
||||
items: () => {
|
||||
return this.hashtags
|
||||
},
|
||||
// is called when a suggestion starts
|
||||
onEnter: ({ items, query, range, command, virtualNode }) => {
|
||||
this.suggestionType = this.hashtagSuggestionType
|
||||
|
||||
this.query = this.sanitizedQuery(query)
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.renderPopup(virtualNode)
|
||||
// we save the command for inserting a selected mention
|
||||
// this allows us to call it inside of our custom popup
|
||||
// via keyboard navigation and on click
|
||||
this.insertMentionOrHashtag = command
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
onChange: ({ items, query, range, virtualNode }) => {
|
||||
this.query = this.sanitizedQuery(query)
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
this.navigatedItemIndex = 0
|
||||
this.renderPopup(virtualNode)
|
||||
},
|
||||
// is called when a suggestion is cancelled
|
||||
onExit: () => {
|
||||
this.suggestionType = this.nullSuggestionType
|
||||
|
||||
// reset all saved values
|
||||
this.query = null
|
||||
this.filteredItems = []
|
||||
this.suggestionRange = null
|
||||
this.navigatedItemIndex = 0
|
||||
this.destroyPopup()
|
||||
},
|
||||
// is called on every keyDown event while a suggestion is active
|
||||
onKeyDown: ({ event }) => {
|
||||
// pressing up arrow
|
||||
if (event.keyCode === 38) {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
// pressing down arrow
|
||||
if (event.keyCode === 40) {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
// pressing enter
|
||||
if (event.keyCode === 13) {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
// pressing space
|
||||
if (event.keyCode === 32) {
|
||||
this.spaceHandler()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
// is called when a suggestion has changed
|
||||
// this function is optional because there is basic filtering built-in
|
||||
// you can overwrite it if you prefer your own filtering
|
||||
// in this example we use fuse.js with support for fuzzy search
|
||||
onFilter: (items, query) => {
|
||||
query = this.sanitizedQuery(query)
|
||||
if (!query) {
|
||||
return items
|
||||
}
|
||||
return items.filter(item =>
|
||||
JSON.stringify(item)
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase()),
|
||||
)
|
||||
},
|
||||
}),
|
||||
...optionalExtensions,
|
||||
],
|
||||
onUpdate: e => {
|
||||
clearTimeout(throttleInputEvent)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
|
||||
import Notification from '.'
|
||||
import Notification from './Notification'
|
||||
import Styleguide from '@human-connection/styleguide'
|
||||
import Filters from '~/plugins/vue-filters'
|
||||
|
||||
@ -38,6 +38,9 @@ describe('Notification', () => {
|
||||
propsData.notification = {
|
||||
post: {
|
||||
title: "It's a title",
|
||||
id: 'post-1',
|
||||
slug: 'its-a-title',
|
||||
contentExcerpt: '<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best',
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -46,6 +49,10 @@ describe('Notification', () => {
|
||||
expect(Wrapper().text()).toContain("It's a title")
|
||||
})
|
||||
|
||||
it('renders the contentExcerpt', () => {
|
||||
expect(Wrapper().text()).toContain('@jenny-rostock is the best')
|
||||
})
|
||||
|
||||
it('has no class "read"', () => {
|
||||
expect(Wrapper().classes()).not.toContain('read')
|
||||
})
|
||||
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<ds-space :class="[{ read: notification.read }, notification]" margin-bottom="x-small">
|
||||
<no-ssr>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<hc-user
|
||||
v-if="resourceType == 'Post'"
|
||||
:user="post.author"
|
||||
:date-time="post.createdAt"
|
||||
:trunc="35"
|
||||
/>
|
||||
<hc-user v-else :user="comment.author" :date-time="comment.createdAt" :trunc="35" />
|
||||
</ds-space>
|
||||
<ds-text color="soft">
|
||||
{{ $t('notifications.menu.mentioned', { resource: resourceType }) }}
|
||||
</ds-text>
|
||||
</no-ssr>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<nuxt-link
|
||||
class="notification-mention-post"
|
||||
:to="{ name: 'post-id-slug', params, ...hashParam }"
|
||||
@click.native="$emit('read')"
|
||||
>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<ds-card
|
||||
:header="post.title || comment.post.title"
|
||||
hover
|
||||
space="x-small"
|
||||
class="notifications-card"
|
||||
>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<div v-if="resourceType == 'Post'">{{ post.contentExcerpt | removeHtml }}</div>
|
||||
<div v-else>
|
||||
<span class="comment-notification-header">Comment:</span>
|
||||
{{ comment.contentExcerpt | removeHtml }}
|
||||
</div>
|
||||
</ds-card>
|
||||
</ds-space>
|
||||
</nuxt-link>
|
||||
</ds-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HcUser from '~/components/User'
|
||||
|
||||
export default {
|
||||
name: 'Notification',
|
||||
components: {
|
||||
HcUser,
|
||||
},
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
resourceType() {
|
||||
return this.post.id ? 'Post' : 'Comment'
|
||||
},
|
||||
post() {
|
||||
return this.notification.post || {}
|
||||
},
|
||||
comment() {
|
||||
return this.notification.comment || {}
|
||||
},
|
||||
params() {
|
||||
return {
|
||||
id: this.post.id || this.comment.post.id,
|
||||
slug: this.post.slug || this.comment.post.slug,
|
||||
}
|
||||
},
|
||||
hashParam() {
|
||||
return this.post.id ? {} : { hash: `#commentId-${this.comment.id}` }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.notification.read {
|
||||
opacity: 0.6; /* Real browsers */
|
||||
filter: alpha(opacity = 60); /* MSIE */
|
||||
}
|
||||
.notifications-card {
|
||||
min-width: 500px;
|
||||
}
|
||||
.comment-notification-header {
|
||||
font-weight: 700;
|
||||
margin-right: 0.1rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<ds-space :class="{ notification: true, read: notification.read }" margin-bottom="x-small">
|
||||
<no-ssr>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<hc-user :user="post.author" :date-time="post.createdAt" :trunc="35" />
|
||||
</ds-space>
|
||||
<ds-text color="soft">
|
||||
{{ $t('notifications.menu.mentioned') }}
|
||||
</ds-text>
|
||||
</no-ssr>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<nuxt-link
|
||||
class="notification-mention-post"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
@click.native="$emit('read')"
|
||||
>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<ds-card :header="post.title" :image="post.image" hover space="x-small">
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div v-html="excerpt" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</ds-card>
|
||||
</ds-space>
|
||||
</nuxt-link>
|
||||
</ds-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HcUser from '~/components/User'
|
||||
|
||||
export default {
|
||||
name: 'Notification',
|
||||
components: {
|
||||
HcUser,
|
||||
},
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
excerpt() {
|
||||
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||
},
|
||||
post() {
|
||||
return this.notification.post || {}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.notification.read {
|
||||
opacity: 0.6; /* Real browsers */
|
||||
filter: alpha(opacity = 60); /* MSIE */
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,6 @@
|
||||
import { config, shallowMount, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
|
||||
import NotificationList from '.'
|
||||
import Notification from '../Notification'
|
||||
import NotificationList from './NotificationList'
|
||||
import Notification from '../Notification/Notification'
|
||||
import Vuex from 'vuex'
|
||||
import Filters from '~/plugins/vue-filters'
|
||||
|
||||
@ -45,6 +45,7 @@ describe('NotificationList.vue', () => {
|
||||
post: {
|
||||
id: 'post-1',
|
||||
title: 'some post title',
|
||||
slug: 'some-post-title',
|
||||
contentExcerpt: 'this is a post content',
|
||||
author: {
|
||||
id: 'john-1',
|
||||
@ -59,6 +60,7 @@ describe('NotificationList.vue', () => {
|
||||
post: {
|
||||
id: 'post-2',
|
||||
title: 'another post title',
|
||||
slug: 'another-post-title',
|
||||
contentExcerpt: 'this is yet another post content',
|
||||
author: {
|
||||
id: 'john-1',
|
||||
@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Notification from '../Notification'
|
||||
import Notification from '../Notification/Notification'
|
||||
|
||||
export default {
|
||||
name: 'NotificationList',
|
||||
@ -1,5 +1,5 @@
|
||||
import { config, shallowMount, createLocalVue } from '@vue/test-utils'
|
||||
import NotificationMenu from '.'
|
||||
import NotificationMenu from './NotificationMenu'
|
||||
|
||||
import Styleguide from '@human-connection/styleguide'
|
||||
import Filters from '~/plugins/vue-filters'
|
||||
@ -17,30 +17,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotificationList from '../NotificationList'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const MARK_AS_READ = gql(`
|
||||
mutation($id: ID!, $read: Boolean!) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}`)
|
||||
|
||||
const NOTIFICATIONS = gql(`{
|
||||
currentUser {
|
||||
id
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id read createdAt
|
||||
post {
|
||||
id createdAt disabled deleted title contentExcerpt slug
|
||||
author { id slug name disabled deleted }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
import { currentUserNotificationsQuery, updateNotificationMutation } from '~/graphql/User'
|
||||
import NotificationList from '../NotificationList/NotificationList'
|
||||
|
||||
export default {
|
||||
name: 'NotificationMenu',
|
||||
@ -61,7 +40,7 @@ export default {
|
||||
const variables = { id: notificationId, read: true }
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: MARK_AS_READ,
|
||||
mutation: updateNotificationMutation(),
|
||||
variables,
|
||||
})
|
||||
} catch (err) {
|
||||
@ -71,7 +50,7 @@ export default {
|
||||
},
|
||||
apollo: {
|
||||
notifications: {
|
||||
query: NOTIFICATIONS,
|
||||
query: currentUserNotificationsQuery(),
|
||||
update: data => {
|
||||
const {
|
||||
currentUser: { notifications },
|
||||
@ -74,3 +74,78 @@ export default i18n => {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const currentUserNotificationsQuery = () => {
|
||||
return gql`
|
||||
{
|
||||
currentUser {
|
||||
id
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
}
|
||||
comment {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
contentExcerpt
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
post {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const updateNotificationMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $read: Boolean!) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ import { mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
import SearchInput from '~/components/SearchInput.vue'
|
||||
import Modal from '~/components/Modal'
|
||||
import NotificationMenu from '~/components/notifications/NotificationMenu'
|
||||
import NotificationMenu from '~/components/notifications/NotificationMenu/NotificationMenu'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||
import seo from '~/mixins/seo'
|
||||
|
||||
@ -124,7 +124,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"menu": {
|
||||
"mentioned": "hat dich in einem Beitrag erwähnt"
|
||||
"mentioned": "hat dich in einem {resource} erwähnt"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -124,7 +124,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"menu": {
|
||||
"mentioned": "mentioned you in a post"
|
||||
"mentioned": "mentioned you in a {resource}"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -57,11 +57,26 @@ module.exports = {
|
||||
title: 'Human Connection',
|
||||
titleTemplate: '%s - Human Connection',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: pkg.description },
|
||||
{
|
||||
charset: 'utf-8',
|
||||
},
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: pkg.description,
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon.ico',
|
||||
},
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||
},
|
||||
|
||||
/*
|
||||
@ -107,8 +122,79 @@ module.exports = {
|
||||
middleware: ['authenticated'],
|
||||
linkActiveClass: 'router-link-active',
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
scrollBehavior: () => {
|
||||
return { x: 0, y: 0 }
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
let position = false
|
||||
// if no children detected and scrollToTop is not explicitly disabled
|
||||
if (
|
||||
to.matched.length < 2 &&
|
||||
to.matched.every(r => r.components.default.options.scrollToTop !== false)
|
||||
) {
|
||||
// scroll to the top of the page
|
||||
position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
|
||||
// if one of the children has scrollToTop option set to true
|
||||
position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// savedPosition is only available for popstate navigations (back button)
|
||||
if (savedPosition) {
|
||||
position = savedPosition
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
// wait for the out transition to complete (if necessary)
|
||||
window.$nuxt.$once('triggerScroll', () => {
|
||||
let processInterval = null
|
||||
let processTime = 0
|
||||
const callInterval = 100
|
||||
const callIntervalLimit = 2000
|
||||
|
||||
// coords will be used if no selector is provided,
|
||||
// or if the selector didn't match any element.
|
||||
if (to.hash) {
|
||||
let hash = to.hash
|
||||
// CSS.escape() is not supported with IE and Edge.
|
||||
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
|
||||
hash = '#' + window.CSS.escape(hash.substr(1))
|
||||
}
|
||||
try {
|
||||
processInterval = setInterval(() => {
|
||||
const hashIsFound = document.querySelector(hash)
|
||||
|
||||
if (hashIsFound) {
|
||||
position = {
|
||||
selector: hash,
|
||||
offset: { x: 0, y: -500 },
|
||||
}
|
||||
}
|
||||
processTime += callInterval
|
||||
if (hashIsFound || processTime >= callIntervalLimit) {
|
||||
clearInterval(processInterval)
|
||||
processInterval = null
|
||||
}
|
||||
}, callInterval)
|
||||
} catch (e) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.warn(
|
||||
'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let resolveInterval = setInterval(() => {
|
||||
if (!processInterval) {
|
||||
clearInterval(resolveInterval)
|
||||
resolve(position)
|
||||
}
|
||||
}, callInterval)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@ -116,8 +202,18 @@ module.exports = {
|
||||
** Nuxt.js modules
|
||||
*/
|
||||
modules: [
|
||||
['@nuxtjs/dotenv', { only: envWhitelist }],
|
||||
['nuxt-env', { keys: envWhitelist }],
|
||||
[
|
||||
'@nuxtjs/dotenv',
|
||||
{
|
||||
only: envWhitelist,
|
||||
},
|
||||
],
|
||||
[
|
||||
'nuxt-env',
|
||||
{
|
||||
keys: envWhitelist,
|
||||
},
|
||||
],
|
||||
'cookie-universal-nuxt',
|
||||
'@nuxtjs/apollo',
|
||||
'@nuxtjs/axios',
|
||||
@ -155,7 +251,9 @@ module.exports = {
|
||||
'/api': {
|
||||
// make this configurable (nuxt-dotenv)
|
||||
target: process.env.GRAPHQL_URI || 'http://localhost:4000',
|
||||
pathRewrite: { '^/api': '' },
|
||||
pathRewrite: {
|
||||
'^/api': '',
|
||||
},
|
||||
toProxy: true, // cloudflare needs that
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
"core-js": "~2.6.9",
|
||||
"css-loader": "~2.1.1",
|
||||
"eslint": "~5.16.0",
|
||||
"eslint-config-prettier": "~6.0.0",
|
||||
"eslint-config-prettier": "~6.1.0",
|
||||
"eslint-config-standard": "~12.0.0",
|
||||
"eslint-loader": "~2.2.1",
|
||||
"eslint-plugin-import": "~2.18.2",
|
||||
@ -109,7 +109,7 @@
|
||||
"eslint-plugin-node": "~9.1.0",
|
||||
"eslint-plugin-prettier": "~3.1.0",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
"eslint-plugin-standard": "~4.0.0",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"eslint-plugin-vue": "~5.2.3",
|
||||
"flush-promises": "^1.0.2",
|
||||
"fuse.js": "^3.4.5",
|
||||
|
||||
@ -83,6 +83,15 @@ export default ({ app = {} }) => {
|
||||
|
||||
return excerpt
|
||||
},
|
||||
removeHtml: content => {
|
||||
if (!content) return ''
|
||||
// replace linebreaks with spaces first
|
||||
let contentExcerpt = content.replace(/<br>/gim, ' ').trim()
|
||||
// remove the rest of the HTML
|
||||
contentExcerpt = contentExcerpt.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
|
||||
return contentExcerpt
|
||||
},
|
||||
proxyApiUrl: url => {
|
||||
if (!url) return url
|
||||
return url.startsWith('/') ? url.replace('/', '/api/') : url
|
||||
|
||||
@ -69,41 +69,43 @@ export const actions = {
|
||||
const {
|
||||
data: { currentUser },
|
||||
} = await client.query({
|
||||
query: gql(`{
|
||||
currentUser {
|
||||
id
|
||||
name
|
||||
slug
|
||||
email
|
||||
avatar
|
||||
role
|
||||
about
|
||||
locationName
|
||||
contributionsCount
|
||||
commentedCount
|
||||
socialMedia {
|
||||
query: gql`
|
||||
query {
|
||||
currentUser {
|
||||
id
|
||||
url
|
||||
}
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
author {
|
||||
id
|
||||
name
|
||||
slug
|
||||
email
|
||||
avatar
|
||||
role
|
||||
about
|
||||
locationName
|
||||
contributionsCount
|
||||
commentedCount
|
||||
socialMedia {
|
||||
id
|
||||
url
|
||||
}
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
`,
|
||||
})
|
||||
if (!currentUser) return dispatch('logout')
|
||||
commit('SET_USER', currentUser)
|
||||
@ -122,7 +124,10 @@ export const actions = {
|
||||
login(email: $email, password: $password)
|
||||
}
|
||||
`),
|
||||
variables: { email, password },
|
||||
variables: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
})
|
||||
await this.app.$apolloHelpers.onLogin(login)
|
||||
commit('SET_TOKEN', login)
|
||||
|
||||
@ -19,7 +19,10 @@ export const mutations = {
|
||||
const toBeUpdated = notifications.find(n => {
|
||||
return n.id === notification.id
|
||||
})
|
||||
state.notifications = { ...toBeUpdated, ...notification }
|
||||
state.notifications = {
|
||||
...toBeUpdated,
|
||||
...notification,
|
||||
}
|
||||
},
|
||||
}
|
||||
export const getters = {
|
||||
@ -38,28 +41,30 @@ export const actions = {
|
||||
const {
|
||||
data: { currentUser },
|
||||
} = await client.query({
|
||||
query: gql(`{
|
||||
currentUser {
|
||||
id
|
||||
notifications(orderBy: createdAt_desc) {
|
||||
query: gql`
|
||||
{
|
||||
currentUser {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
author {
|
||||
id
|
||||
notifications(orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
`,
|
||||
})
|
||||
notifications = currentUser.notifications
|
||||
commit('SET_NOTIFICATIONS', notifications)
|
||||
@ -71,18 +76,24 @@ export const actions = {
|
||||
|
||||
async markAsRead({ commit, rootGetters }, notificationId) {
|
||||
const client = this.app.apolloProvider.defaultClient
|
||||
const mutation = gql(`
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $read: Boolean!) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
`)
|
||||
const variables = { id: notificationId, read: true }
|
||||
`
|
||||
const variables = {
|
||||
id: notificationId,
|
||||
read: true,
|
||||
}
|
||||
const {
|
||||
data: { UpdateNotification },
|
||||
} = await client.mutate({ mutation, variables })
|
||||
} = await client.mutate({
|
||||
mutation,
|
||||
variables,
|
||||
})
|
||||
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
|
||||
},
|
||||
}
|
||||
|
||||
@ -6168,10 +6168,10 @@ escodegen@^1.9.1:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-prettier@^6.0.0, eslint-config-prettier@~6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz#f429a53bde9fc7660e6353910fd996d6284d3c25"
|
||||
integrity sha512-vDrcCFE3+2ixNT5H83g28bO/uYAwibJxerXPj+E7op4qzBCsAV36QfvdAyVOoNxKAH2Os/e01T/2x++V0LPukA==
|
||||
eslint-config-prettier@^6.0.0, eslint-config-prettier@~6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.1.0.tgz#e6f678ba367fbd1273998d5510f76f004e9dce7b"
|
||||
integrity sha512-k9fny9sPjIBQ2ftFTesJV21Rg4R/7a7t7LCtZVrYQiHEp8Nnuk3EGaDmsKSAnsPj0BYcgB2zxzHa2NTkIxcOLg==
|
||||
dependencies:
|
||||
get-stdin "^6.0.0"
|
||||
|
||||
@ -6263,10 +6263,10 @@ eslint-plugin-promise@~4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
|
||||
integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
|
||||
|
||||
eslint-plugin-standard@~4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz#f845b45109c99cd90e77796940a344546c8f6b5c"
|
||||
integrity sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA==
|
||||
eslint-plugin-standard@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4"
|
||||
integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==
|
||||
|
||||
eslint-plugin-vue@~5.2.3:
|
||||
version "5.2.3"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user