Merge branch 'master' into C-1187-terms-and-conditions-confirmed-function

This commit is contained in:
Alexander Friedland 2019-09-03 09:51:12 +02:00 committed by GitHub
commit 41767cc27e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1557 additions and 1596 deletions

View File

@ -49,7 +49,7 @@
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.9.1",
"apollo-server": "~2.9.3",
"apollo-server-express": "^2.9.0",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
@ -61,7 +61,7 @@
"dotenv": "~8.1.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.5.3",
"graphql": "^14.5.4",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.5",
@ -90,7 +90,7 @@
"metascraper-video": "^5.6.5",
"metascraper-youtube": "^5.6.3",
"minimatch": "^3.0.4",
"neo4j-driver": "~1.7.5",
"neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.7.2",
"neode": "^0.3.2",
"node-fetch": "~2.6.0",
@ -116,12 +116,12 @@
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~6.2.2",
"eslint": "~6.3.0",
"eslint-config-prettier": "~6.1.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.2",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-jest": "~22.16.0",
"eslint-plugin-node": "~9.2.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",

View File

@ -12,11 +12,9 @@ export default {
CreatePost: setCreatedAt,
CreateComment: setCreatedAt,
CreateOrganization: setCreatedAt,
CreateNotification: setCreatedAt,
UpdateUser: setUpdatedAt,
UpdatePost: setUpdatedAt,
UpdateComment: setUpdatedAt,
UpdateOrganization: setUpdatedAt,
UpdateNotification: setUpdatedAt,
},
}

View File

@ -4,13 +4,13 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
if (!idsOfUsers.length) return
// 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']
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
if (!reasonsAllowed.includes(reason)) {
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))
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
) {
throw new Error('Notification does not fit the reason!')
}
@ -25,8 +25,9 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
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)
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE
SET notification.createdAt = $createdAt
`
break
}
@ -37,20 +38,22 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
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)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE
SET notification.createdAt = $createdAt
`
break
}
case 'comment_on_post': {
case 'commented_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)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE
SET notification.createdAt = $createdAt
`
break
}
@ -105,7 +108,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_post', context)
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
}
}

View File

@ -77,14 +77,18 @@ afterEach(async () => {
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
read
reason
post {
notifications(read: $read, orderBy: createdAt_desc) {
read
reason
createdAt
from {
__typename
... on Post {
id
content
}
comment {
... on Comment {
id
content
}
}
@ -154,18 +158,18 @@ describe('notifications', () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'comment_on_post',
post: null,
comment: {
content: commentContent,
},
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
],
},
},
],
},
})
const { query } = createTestClient(server)
@ -183,11 +187,7 @@ describe('notifications', () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(
@ -211,11 +211,7 @@ describe('notifications', () => {
await notifiedUser.relateTo(commentAuthor, 'blocked')
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(
@ -253,18 +249,18 @@ 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,
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
id: 'p47',
content: expectedContent,
},
],
},
},
],
},
})
const { query } = createTestClient(server)
@ -278,7 +274,7 @@ describe('notifications', () => {
).resolves.toEqual(expected)
})
describe('many times', () => {
describe('updates the post and mentions me again', () => {
const updatePostAction = async () => {
const updatedContent = `
One more mention to
@ -307,33 +303,25 @@ describe('notifications', () => {
authenticatedUser = await notifiedUser.toJson()
}
it('creates exactly one more notification', async () => {
it('creates no duplicate notification for the same resource', async () => {
const expectedUpdatedContent =
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
await createPostAction()
await updatePostAction()
const expectedContent =
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
id: 'p47',
content: expectedUpdatedContent,
},
{
read: false,
reason: 'mentioned_in_post',
post: {
content: expectedContent,
},
comment: null,
},
],
},
},
],
},
})
await expect(
@ -345,6 +333,68 @@ describe('notifications', () => {
}),
).resolves.toEqual(expected)
})
describe('if the notification was marked as read earlier', () => {
const markAsReadAction = async () => {
const mutation = gql`
mutation($id: ID!) {
markAsRead(id: $id) {
read
}
}
`
await mutate({ mutation, variables: { id: 'p47' } })
}
describe('but the next mention happens after the notification was marked as read', () => {
it('sets the `read` attribute to false again', async () => {
await createPostAction()
await markAsReadAction()
const {
data: {
notifications: [{ read: readBefore }],
},
} = await query({
query: notificationQuery,
})
await updatePostAction()
const {
data: {
notifications: [{ read: readAfter }],
},
} = await query({
query: notificationQuery,
})
expect(readBefore).toEqual(true)
expect(readAfter).toEqual(false)
})
it('updates the `createdAt` attribute', async () => {
await createPostAction()
await markAsReadAction()
const {
data: {
notifications: [{ createdAt: createdAtBefore }],
},
} = await query({
query: notificationQuery,
})
await updatePostAction()
const {
data: {
notifications: [{ createdAt: createdAtAfter }],
},
} = await query({
query: notificationQuery,
})
expect(createdAtBefore).toBeTruthy()
expect(Date.parse(createdAtBefore)).toEqual(expect.any(Number))
expect(createdAtAfter).toBeTruthy()
expect(Date.parse(createdAtAfter)).toEqual(expect.any(Number))
expect(createdAtBefore).not.toEqual(createdAtAfter)
})
})
})
})
describe('but the author of the post blocked me', () => {
@ -355,11 +405,7 @@ describe('notifications', () => {
it('sends no notification', async () => {
await createPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(
@ -397,18 +443,18 @@ describe('notifications', () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{
read: false,
reason: 'mentioned_in_comment',
post: null,
comment: {
content: commentContent,
},
notifications: [
{
read: false,
createdAt: expect.any(String),
reason: 'mentioned_in_comment',
from: {
__typename: 'Comment',
id: 'c47',
content: commentContent,
},
],
},
},
],
},
})
const { query } = createTestClient(server)
@ -440,11 +486,7 @@ describe('notifications', () => {
it('sends no notification', async () => {
await createCommentOnPostAction()
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [],
},
},
data: { notifications: [] },
})
const { query } = createTestClient(server)
await expect(

View File

@ -41,32 +41,6 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id
})
const belongsToMe = rule({
cache: 'no_cache',
})(async (_, args, context) => {
const {
driver,
user: { id: userId },
} = context
const { id: notificationId } = args
const session = driver.session()
const result = await session.run(
`
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
RETURN n
`,
{
userId,
notificationId,
},
)
const [notification] = result.records.map(record => {
return record.get('n')
})
session.close()
return Boolean(notification)
})
/* TODO: decide if we want to remove this check: the check
* `onlyEnabledContent` throws authorization errors only if you have
* arguments for `disabled` or `deleted` assuming these are filter
@ -149,7 +123,6 @@ const permissions = shield(
Category: allow,
Tag: allow,
Report: isModerator,
Notification: isAdmin,
statistics: allow,
currentUser: allow,
Post: or(onlyEnabledContent, isModerator),
@ -160,6 +133,7 @@ const permissions = shield(
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: allow,
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
},
Mutation: {
'*': deny,
@ -168,7 +142,6 @@ const permissions = shield(
Signup: isAdmin,
SignupVerification: allow,
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
UpdateNotification: belongsToMe,
UpdateUser: onlyYourself,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,
@ -198,6 +171,7 @@ const permissions = shield(
RemovePostEmotions: isAuthenticated,
block: isAuthenticated,
unblock: isAuthenticated,
markAsRead: isAuthenticated,
},
User: {
email: isMyOwn,

View File

@ -1,36 +0,0 @@
import uuid from 'uuid/v4'
module.exports = {
id: {
type: 'uuid',
primary: true,
default: uuid,
},
read: {
type: 'boolean',
default: false,
},
reason: {
type: 'string',
valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post'],
invalid: [null],
default: 'mentioned_in_post',
},
createdAt: {
type: 'string',
isoDate: true,
default: () => new Date().toISOString(),
},
user: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'User',
direction: 'out',
},
post: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'Post',
direction: 'in',
},
}

View File

@ -7,6 +7,5 @@ export default {
EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Notification: require('./Notification.js'),
Category: require('./Category.js'),
}

View File

@ -20,6 +20,7 @@ export default applyScalars(
'Statistics',
'LoggedInUser',
'SocialMedia',
'NOTIFIED',
],
// add 'User' here as soon as possible
},
@ -32,6 +33,7 @@ export default applyScalars(
'Statistics',
'LoggedInUser',
'SocialMedia',
'NOTIFIED',
],
// add 'User' here as soon as possible
},

View File

@ -1,4 +1,5 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import Resolver from './helpers/Resolver'
export default {
Mutation: {
@ -52,4 +53,13 @@ export default {
return comment
},
},
Comment: {
...Resolver('Comment', {
hasOne: {
author: '<-[:WROTE]-(related:User)',
post: '-[:COMMENTS]->(related:Post)',
disabledBy: '<-[:DISABLED]-(related:User)',
},
}),
},
}

View File

@ -61,7 +61,6 @@ export default function Resolver(type, options = {}) {
const id = parent[idAttribute]
const statement = `
MATCH(u:${type} {${idAttribute}: {id}})${connection}
WHERE NOT related.deleted = true AND NOT related.disabled = true
RETURN COUNT(DISTINCT(related)) as count
`
const result = await instance.cypher(statement, { id })

View File

@ -1,14 +1,80 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
const resourceTypes = ['Post', 'Comment']
const transformReturnType = record => {
return {
...record.get('notification').properties,
from: {
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
...record.get('resource').properties,
},
to: {
...record.get('user').properties,
},
}
}
export default {
Query: {
Notification: (object, params, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false)
notifications: async (parent, args, context, resolveInfo) => {
const { user: currentUser } = context
const session = context.driver.session()
let notifications
let whereClause
let orderByClause
switch (args.read) {
case true:
whereClause = 'WHERE notification.read = TRUE'
break
case false:
whereClause = 'WHERE notification.read = FALSE'
break
default:
whereClause = ''
}
switch (args.orderBy) {
case 'createdAt_asc':
orderByClause = 'ORDER BY notification.createdAt ASC'
break
case 'createdAt_desc':
orderByClause = 'ORDER BY notification.createdAt DESC'
break
default:
orderByClause = ''
}
try {
const cypher = `
MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id})
${whereClause}
RETURN resource, notification, user
${orderByClause}
`
const result = await session.run(cypher, { id: currentUser.id })
notifications = await result.records.map(transformReturnType)
} finally {
session.close()
}
return notifications
},
},
Mutation: {
UpdateNotification: (object, params, context, resolveInfo) => {
return neo4jgraphql(object, params, context, resolveInfo, false)
markAsRead: async (parent, args, context, resolveInfo) => {
const { user: currentUser } = context
const session = context.driver.session()
let notification
try {
const cypher = `
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
SET notification.read = TRUE
RETURN resource, notification, user
`
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
const notifications = await result.records.map(transformReturnType)
notification = notifications[0]
} finally {
session.close()
}
return notification
},
},
}

View File

@ -1,397 +1,309 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j'
import { gql } from '../../jest/helpers'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
let client
const factory = Factory()
const instance = neode()
const neode = getNeode()
const driver = getDriver()
const userParams = {
id: 'you',
email: 'test@example.org',
password: '1234',
}
const categoryIds = ['cat9']
let authenticatedUser
let user
let variables
let query
let mutate
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
})
beforeEach(async () => {
await factory.create('User', userParams)
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
authenticatedUser = null
variables = { orderBy: 'createdAt_asc' }
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('Notification', () => {
const notificationQuery = gql`
query {
Notification {
id
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised')
})
describe('given some notifications', () => {
beforeEach(async () => {
user = await factory.create('User', userParams)
await factory.create('User', { id: 'neighbor' })
await Promise.all(setupNotifications.map(s => neode.cypher(s)))
})
})
const setupNotifications = [
`MATCH(user:User {id: 'neighbor'})
MERGE (:Post {id: 'p1', content: 'Not for you'})
-[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'})
-[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'})
-[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}]
->(user);
`,
`MATCH(user:User {id: 'you'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-08-30T19:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`,
`MATCH(user:User {id: 'neighbor'})
MATCH(post:Post {id: 'p3'})
CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'})
MERGE (comment)-[:COMMENTS]->(post)
MERGE (comment)
-[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
->(user);
`,
]
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,
})
})
describe('given some notifications', () => {
beforeEach(async () => {
const neighborParams = {
email: 'neighbor@example.org',
password: '1234',
id: 'neighbor',
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean, $orderBy: NotificationOrdering) {
notifications(read: $read, orderBy: $orderBy) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
await Promise.all([
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)
await factory.create('Post', { id: 'p1', categoryIds })
await Promise.all([
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 queryCurrentUserNotificationsFilterRead = gql`
query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
id
post {
id
}
comment {
id
}
}
}
}
`
const variables = { read: false }
it('returns only unread notifications of current user', async () => {
const expected = {
currentUser: {
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(queryCurrentUserNotificationsFilterRead, variables),
).resolves.toEqual(expected)
})
})
describe('no filters', () => {
const queryCurrentUserNotifications = gql`
query {
currentUser {
notifications(orderBy: createdAt_desc) {
id
post {
id
}
comment {
id
}
}
}
}
`
it('returns all notifications of current user', async () => {
const expected = {
currentUser: {
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(queryCurrentUserNotifications, variables)).resolves.toEqual(
expected,
)
})
})
})
})
})
describe('UpdateNotification', () => {
const mutationUpdateNotification = gql`
mutation($id: ID!, $read: Boolean) {
UpdateNotification(id: $id, read: $read) {
id
read
}
}
`
const variablesPostUpdateNotification = {
id: 'post-mention-to-be-updated',
read: true,
}
const variablesCommentUpdateNotification = {
id: 'comment-mention-to-be-updated',
read: true,
}
describe('given some notifications', () => {
let headers
beforeEach(async () => {
const mentionedParams = {
id: 'mentioned-1',
email: 'mentioned@example.org',
password: '1234',
slug: 'mentioned',
}
await Promise.all([
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)
await factory.create('Post', { id: 'p1', categoryIds })
await Promise.all([
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(mutationUpdateNotification, variablesPostUpdateNotification),
).rejects.toThrow('Not Authorised')
const result = await query({ query: notificationQuery })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
authenticatedUser = await user.toJson()
})
describe('no filters', () => {
it('returns all notifications of current user', async () => {
const expected = expect.objectContaining({
data: {
notifications: [
{
from: {
__typename: 'Comment',
content: 'You have seen this comment mentioning already',
},
read: true,
createdAt: '2019-08-30T15:33:48.651Z',
},
{
from: {
__typename: 'Post',
content: 'Already seen post mentioning',
},
read: true,
createdAt: '2019-08-30T17:33:48.651Z',
},
{
from: {
__typename: 'Comment',
content: 'You have been mentioned in a comment',
},
read: false,
createdAt: '2019-08-30T19:33:48.651Z',
},
{
from: {
__typename: 'Post',
content: 'You have been mentioned in a post',
},
read: false,
createdAt: '2019-08-31T17:33:48.651Z',
},
],
},
})
await expect(query({ query: notificationQuery, variables })).resolves.toEqual(expected)
})
})
it('throws authorization error', async () => {
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,
})
})
it('updates post notification', async () => {
const expected = {
UpdateNotification: {
id: 'post-mention-to-be-updated',
read: true,
describe('filter for read: false', () => {
it('returns only unread notifications of current user', async () => {
const expected = expect.objectContaining({
data: {
notifications: [
{
from: {
__typename: 'Comment',
content: 'You have been mentioned in a comment',
},
read: false,
createdAt: '2019-08-30T19:33:48.651Z',
},
{
from: {
__typename: 'Post',
content: 'You have been mentioned in a post',
},
read: false,
createdAt: '2019-08-31T17:33:48.651Z',
},
],
},
}
})
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),
query({ query: notificationQuery, variables: { ...variables, read: false } }),
).resolves.toEqual(expected)
})
})
})
})
describe('markAsRead', () => {
const markAsReadMutation = gql`
mutation($id: ID!) {
markAsRead(id: $id) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const result = await mutate({
mutation: markAsReadMutation,
variables: { ...variables, id: 'p1' },
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
describe('not being notified at all', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p1',
}
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
describe('being notified', () => {
describe('on a post', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p3',
}
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
expect(data).toEqual({
markAsRead: {
from: {
__typename: 'Post',
content: 'You have been mentioned in a post',
},
read: true,
createdAt: '2019-08-31T17:33:48.651Z',
},
})
})
describe('but notification was already marked as read', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'p2',
}
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
})
describe('on a comment', () => {
beforeEach(async () => {
variables = {
...variables,
id: 'c2',
}
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
expect(data).toEqual({
markAsRead: {
from: {
__typename: 'Comment',
content: 'You have been mentioned in a comment',
},
read: true,
createdAt: '2019-08-30T19:33:48.651Z',
},
})
})
})
})
})
})
})

View File

@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload'
import { getBlockedUsers, getBlockedByUsers } from './users.js'
import { mergeWith, isArray } from 'lodash'
import Resolver from './helpers/Resolver'
const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params
@ -11,6 +12,8 @@ const filterForBlockedUsers = async (params, context) => {
getBlockedByUsers(context),
])
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
if (!badIds.length) return params
params.filter = mergeWith(
params.filter,
{
@ -179,4 +182,46 @@ export default {
return emoted
},
},
Post: {
...Resolver('Post', {
hasMany: {
tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)',
comments: '<-[:COMMENTS]-(related:Comment)',
shoutedBy: '<-[:SHOUTED]-(related:User)',
emotions: '<-[related:EMOTED]',
},
hasOne: {
author: '<-[:WROTE]-(related:User)',
disabledBy: '<-[:DISABLED]-(related:User)',
},
count: {
shoutedCount:
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
emotionsCount: '<-[related:EMOTED]-(:User)',
},
boolean: {
shoutedByCurrentUser:
'<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},
}),
relatedContributions: async (parent, params, context, resolveInfo) => {
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
const { id } = parent
const statement = `
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
RETURN DISTINCT post
LIMIT 10
`
let relatedContributions
const session = context.driver.session()
try {
const result = await session.run(statement, { id })
relatedContributions = result.records.map(r => r.get('post').properties)
} finally {
session.close()
}
return relatedContributions
},
},
}

View File

@ -210,12 +210,15 @@ export default {
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
},
count: {
contributionsCount: '-[:WROTE]->(related:Post)',
contributionsCount:
'-[:WROTE]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
friendsCount: '<-[:FRIENDS]->(related:User)',
followingCount: '-[:FOLLOWS]->(related:User)',
followedByCount: '<-[:FOLLOWS]-(related:User)',
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
shoutedCount: '-[:SHOUTED]->(related:Post)',
commentedCount:
'-[:WROTE]->(c:Comment)-[:COMMENTS]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
shoutedCount:
'-[:SHOUTED]->(related:Post) WHERE NOT related.disabled = true AND NOT related.deleted = true',
badgesCount: '<-[:REWARDED]-(related:Badge)',
},
hasOne: {

View File

@ -1,5 +1,5 @@
enum ReasonNotification {
mentioned_in_post
mentioned_in_comment
comment_on_post
}
commented_on_post
}

View File

@ -4,7 +4,7 @@ type Query {
currentUser: User
# Get the latest Network Statistics
statistics: Statistics!
findPosts(query: String!, limit: Int = 10): [Post]!
findPosts(query: String!, limit: Int = 10, filter: _PostFilter): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $query)

View File

@ -0,0 +1,28 @@
type NOTIFIED {
from: NotificationSource
to: User
createdAt: String
read: Boolean
reason: NotificationReason
}
union NotificationSource = Post | Comment
enum NotificationOrdering {
createdAt_asc
createdAt_desc
}
enum NotificationReason {
mentioned_in_post
mentioned_in_comment
commented_on_post
}
type Query {
notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED]
}
type Mutation {
markAsRead(id: ID!): NOTIFIED
}

View File

@ -1,9 +0,0 @@
type Notification {
id: ID!
read: Boolean
reason: ReasonNotification
createdAt: String
user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN")
comment: Comment @relation(name: "NOTIFIED", direction: "IN")
}

View File

@ -24,10 +24,12 @@ type User {
createdAt: String
updatedAt: String
termsAndConditionsAgreedVersion: String
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
@ -66,7 +68,7 @@ type User {
)
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment)-[:COMMENTS]->(p:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true AND NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
commentedCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(:Comment)-[:COMMENTS]->(p:Post) WHERE NOT p.deleted = true AND NOT p.disabled = true RETURN COUNT(DISTINCT(p))")
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")

View File

@ -8,7 +8,6 @@ import createComment from './comments.js'
import createCategory from './categories.js'
import createTag from './tags.js'
import createReport from './reports.js'
import createNotification from './notifications.js'
export const seedServerHost = 'http://127.0.0.1:4001'
@ -31,7 +30,6 @@ const factories = {
Category: createCategory,
Tag: createTag,
Report: createReport,
Notification: createNotification,
}
export const cleanDatabase = async (options = {}) => {

View File

@ -1,17 +0,0 @@
import uuid from 'uuid/v4'
export default function(params) {
const { id = uuid(), read = false } = params
return {
mutation: `
mutation($id: ID, $read: Boolean) {
CreateNotification(id: $id, read: $read) {
id
read
}
}
`,
variables: { id, read },
}
}

View File

@ -270,7 +270,7 @@ import Factory from './factories'
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. ;-)'
'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="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
await Promise.all([
asAdmin.create('Post', {

View File

@ -689,7 +689,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
@ -1546,6 +1546,14 @@ apollo-cache-control@0.8.2:
apollo-server-env "2.4.2"
graphql-extensions "0.10.1"
apollo-cache-control@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7"
integrity sha512-IZ1d3AXZtkZhLYo0kWqTbZ6nqLFaeUvLdMESs+9orMadBZ7mvzcAfBwrhKyCWPGeAAZ/jKv8FtYHybpchHgFAg==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.10.3"
apollo-cache-inmemory@~1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d"
@ -1587,7 +1595,15 @@ apollo-datasource@0.6.2:
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-engine-reporting-protobuf@0.4.0:
apollo-datasource@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13"
integrity sha512-gRYyFVpJgHE2hhS+VxMeOerxXQ/QYxWG7T6QddfugJWYAG9DRCl65e2b7txcGq2NP3r+O1iCm4GNwhRBDJbd8A==
dependencies:
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-engine-reporting-protobuf@0.4.0, apollo-engine-reporting-protobuf@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.0.tgz#e34c192d86493b33a73181fd6be75721559111ec"
integrity sha512-cXHZSienkis8v4RhqB3YG3DkaksqLpcxApRLTpRMs7IXNozgV7CUPYGFyFBEra1ZFgUyHXx4G9MpelV+n2cCfA==
@ -1607,6 +1623,19 @@ apollo-engine-reporting@1.4.4:
async-retry "^1.2.1"
graphql-extensions "0.10.1"
apollo-engine-reporting@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.4.6.tgz#83af6689c4ab82d1c62c3f5dde7651975508114f"
integrity sha512-acfb7oFnru/8YQdY4x6+7WJbZfzdVETI8Cl+9ImgUrvUnE8P+f2SsGTKXTC1RuUvve4c56PAvaPgE+z8X1a1Mw==
dependencies:
apollo-engine-reporting-protobuf "^0.4.0"
apollo-graphql "^0.3.3"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-types "^0.2.4"
async-retry "^1.2.1"
graphql-extensions "^0.10.3"
apollo-env@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3"
@ -1668,7 +1697,7 @@ apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
tslib "^1.9.3"
zen-observable-ts "^0.8.19"
apollo-server-caching@0.5.0:
apollo-server-caching@0.5.0, apollo-server-caching@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46"
integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw==
@ -1702,6 +1731,33 @@ apollo-server-core@2.9.1:
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-core@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.3.tgz#918f836c8215d371935c831c72d0840c7bf0250f"
integrity sha512-KQpOM3nAXdMqKVE0HHcOkH/EVhyDqFEKLNFlsyGHGOn9ujpI6RsltX+YpXRyAdbfQHpTk11v/IAo6XksWN+g1Q==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
"@apollographql/graphql-playground-html" "1.6.24"
"@types/graphql-upload" "^8.0.0"
"@types/ws" "^6.0.0"
apollo-cache-control "^0.8.4"
apollo-datasource "^0.6.3"
apollo-engine-reporting "^1.4.6"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-errors "^2.3.3"
apollo-server-plugin-base "^0.6.4"
apollo-server-types "^0.2.4"
apollo-tracing "^0.8.4"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "^0.10.3"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
graphql-upload "^8.0.2"
sha.js "^2.4.11"
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-env@2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.2.tgz#8549caa7c8f57af88aadad5c2a0bb7adbcc5f76e"
@ -1710,15 +1766,28 @@ apollo-server-env@2.4.2:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-env@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2"
integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-errors@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.2.tgz#86bbd1ff8f0b5f16bfdcbb1760398928f9fce539"
integrity sha512-twVCP8tNHFzxOzU3jf84ppBFSvjvisZVWlgF82vwG+qEEUaAE5h5DVpeJbcI1vRW4VQPuFV+B+FIsnlweFKqtQ==
apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.1.tgz#9a8cb7fba579e68ddfa1953dfd066b751bca32f0"
integrity sha512-3mmuojt9s9Gyqdf8fbdKtbw23UFYrtVQtTNASgVW8zCabZqs2WjYnijMRf1aL4u9VSl+BFMOZUPMYaeBX+u38w==
apollo-server-errors@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.3.tgz#83763b00352c10dc68fbb0d41744ade66de549ff"
integrity sha512-MO4oJ129vuCcbqwr5ZwgxqGGiLz3hCyowz0bstUF7MR+vNGe4oe3DWajC9lv4CxrhcqUHQOeOPViOdIo1IxE3g==
apollo-server-express@^2.9.0, apollo-server-express@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.3.tgz#67573404030c2676be49a7bf97d423b8462e295c"
integrity sha512-Hkfs+ce6GqaoSzDOJs8Pj7W3YUjH0BzGglo5HMsOXOnjPZ0pJE9v8fmK76rlkITLw7GjvIq5GKlafymC31FMBw==
dependencies:
"@apollographql/graphql-playground-html" "1.6.24"
"@types/accepts" "^1.3.5"
@ -1726,10 +1795,11 @@ apollo-server-express@2.9.1, apollo-server-express@^2.9.0:
"@types/cors" "^2.8.4"
"@types/express" "4.17.1"
accepts "^1.3.5"
apollo-server-core "2.9.1"
apollo-server-types "0.2.2"
apollo-server-core "^2.9.3"
apollo-server-types "^0.2.4"
body-parser "^1.18.3"
cors "^2.8.4"
express "^4.17.1"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
parseurl "^1.3.2"
@ -1743,6 +1813,13 @@ apollo-server-plugin-base@0.6.2:
dependencies:
apollo-server-types "0.2.2"
apollo-server-plugin-base@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.4.tgz#63ea4fd0bbb6c4510bc8d0d2ad0a0684c8d0da8c"
integrity sha512-4rY+cBAIpQomGWYBtk8hHkLQWHrh5hgIBPQqmhXh00YFdcY+Ob1/cU2/2iqTcIzhtcaezsc8OZ63au6ahSBQqg==
dependencies:
apollo-server-types "^0.2.4"
apollo-server-testing@~2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.1.tgz#29d2524e84722a1319d9c1524b4f9d44379d6a49"
@ -1759,13 +1836,22 @@ apollo-server-types@0.2.2:
apollo-server-caching "0.5.0"
apollo-server-env "2.4.2"
apollo-server@~2.9.1:
version "2.9.1"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.1.tgz#16ff443d43ea38f72fe20adea0803c46037b2b3b"
integrity sha512-iCGoRBOvwTUkDz6Nq/rKguMyhDiQdL3VneF0GTjBGrelTIp3YTIxk/qBFkIr2Chtm9ZZYkS6o+ZldUnxYFKg7A==
apollo-server-types@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.4.tgz#28864900ffc7f9711a859297c143a833fdb6aa43"
integrity sha512-G4FvBVgGQcTW6ZBS2+hvcDQkSfdOIKV+cHADduXA275v+5zl42g+bCaGd/hCCKTDRjmQvObLiMxH/BJ6pDMQgA==
dependencies:
apollo-server-core "2.9.1"
apollo-server-express "2.9.1"
apollo-engine-reporting-protobuf "^0.4.0"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server@~2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.3.tgz#2a79fcee25da0b0673eb70d73839c40c3c4b8cca"
integrity sha512-JQoeseSo3yOBu3WJzju0NTreoqYckNILybgXNUOhdurE55VFpZ8dsBEO6nMfdO2y1A70W14mnnVWCBEm+1rE8w==
dependencies:
apollo-server-core "^2.9.3"
apollo-server-express "^2.9.3"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
@ -1778,6 +1864,14 @@ apollo-tracing@0.8.2:
apollo-server-env "2.4.2"
graphql-extensions "0.10.1"
apollo-tracing@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.4.tgz#0117820c3f0ad3aa6daf7bf13ddbb923cbefa6de"
integrity sha512-DjbFW0IvHicSlTVG+vK+1WINfBMRCdPPHJSW/j65JMir9Oe56WGeqL8qz8hptdUUmLYEb+azvcyyGsJsiR3zpQ==
dependencies:
apollo-server-env "^2.4.3"
graphql-extensions "^0.10.3"
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
@ -3311,12 +3405,12 @@ eslint-module-utils@^2.4.0:
debug "^2.6.8"
pkg-dir "^2.0.0"
eslint-plugin-es@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
eslint-plugin-es@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
dependencies:
eslint-utils "^1.3.0"
eslint-utils "^1.4.2"
regexpp "^2.0.1"
eslint-plugin-import@~2.18.2:
@ -3336,20 +3430,20 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.15.2:
version "22.15.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398"
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg==
eslint-plugin-jest@~22.16.0:
version "22.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
eslint-plugin-node@~9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
eslint-plugin-node@~9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
dependencies:
eslint-plugin-es "^1.4.0"
eslint-utils "^1.3.1"
eslint-plugin-es "^1.4.1"
eslint-utils "^1.4.2"
ignore "^5.1.1"
minimatch "^3.0.4"
resolve "^1.10.1"
@ -3388,7 +3482,7 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^1.3.0, eslint-utils@^1.3.1, eslint-utils@^1.4.2:
eslint-utils@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
@ -3400,10 +3494,10 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@~6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f"
integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw==
eslint@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.3.0.tgz#1f1a902f67bfd4c354e7288b81e40654d927eb6a"
integrity sha512-ZvZTKaqDue+N8Y9g0kp6UPZtS4FSY3qARxBs7p4f0H0iof381XHduqVerFWtK8DPtKmemqbqCFENWSQgPR/Gow==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
@ -4086,6 +4180,15 @@ graphql-extensions@0.10.1:
apollo-server-env "2.4.2"
apollo-server-types "0.2.2"
graphql-extensions@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.3.tgz#9e37f3bd26309c40b03a0be0e63e02b3f99d52ea"
integrity sha512-kwU0gUe+Qdfr8iZYT91qrPSwQNgPhB/ClF1m1LEPdxlptk5FhFmjpxAcbMZ8q7j0kjfnbp2IeV1OhRDCEPqz2w==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
apollo-server-env "^2.4.3"
apollo-server-types "^0.2.4"
graphql-import@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223"
@ -4180,10 +4283,10 @@ graphql-upload@^8.0.2:
http-errors "^1.7.2"
object-path "^0.11.4"
graphql@^14.2.1, graphql@^14.5.3:
version "14.5.3"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0"
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg==
graphql@^14.2.1, graphql@^14.5.4:
version "14.5.4"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
dependencies:
iterall "^1.2.2"
@ -6175,12 +6278,12 @@ 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.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==
neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
dependencies:
"@babel/runtime" "^7.4.4"
"@babel/runtime" "^7.5.5"
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"

View File

@ -28,6 +28,17 @@ To start the services that are required for cypress testing, run this:
$ yarn cypress:setup
```
## Install cypress
Even if the required services for testing run via docker, depending on your
setup, the cypress tests themselves run on your host machine. So with our
without docker, you would have to install cypress and its dependencies first:
```
# in the root folder /
yarn install
```
## Run cypress
After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command:

View File

@ -1,5 +1,5 @@
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
import { getLangByName } from "../../support/helpers";
import helpers from "../../support/helpers";
import slugify from "slug";
// import { VERSION } from '../../../webapp/pages/terms-and-conditions.vue';
@ -140,14 +140,17 @@ Then("I am still logged in", () => {
When("I select {string} in the language menu", name => {
cy.switchLanguage(name, true);
});
Given("I previously switched the language to {string}", name => {
cy.switchLanguage(name, true);
});
Then("the whole user interface appears in {string}", name => {
const lang = getLangByName(name);
cy.get(`html[lang=${lang.code}]`);
cy.getCookie("locale").should("have.property", "value", lang.code);
const { code } = helpers.getLangByName(name);
cy.get(`html[lang=${code}]`);
cy.getCookie("locale").should("have.property", "value", code);
});
Then("I see a button with the label {string}", label => {
cy.contains("button", label);
});
@ -175,13 +178,13 @@ Given("we have the following posts in our database:", table => {
};
postAttributes.deleted = Boolean(postAttributes.deleted);
const disabled = Boolean(postAttributes.disabled);
postAttributes.categoryIds = [`cat${i}`];
postAttributes.categoryIds = [`cat${i}${new Date()}`];
postAttributes;
cy.factory()
.create("User", userAttributes)
.authenticateAs(userAttributes)
.create("Category", {
id: `cat${i}`,
id: `cat${i}${new Date()}`,
name: "Just For Fun",
slug: `just-for-fun-${i}`,
icon: "smile"
@ -364,7 +367,7 @@ When("mention {string} in the text", mention => {
});
Then("the notification gets marked as read", () => {
cy.get(".post.createdAt")
cy.get(".notifications-menu-popover .notification")
.first()
.should("have.class", "read");
});

View File

@ -26,6 +26,9 @@ Feature: Block a User
And nobody is following the user profile anymore
Scenario: Posts of blocked users are filtered from search results
Given we have the following posts in our database:
| Author | id | title | content |
| Some unblocked user | im-not-blocked | Post that should be seen | cause I'm not blocked |
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
When I search for "Spam"
Then I should see the following posts in the select dropdown:
@ -35,3 +38,7 @@ Feature: Block a User
And I refresh the page
And I search for "Spam"
Then the search has no results
But I search for "not blocked"
Then I should see the following posts in the select dropdown:
| title |
| Post that should be seen |

View File

@ -14,7 +14,7 @@
/* globals Cypress cy */
import "cypress-file-upload";
import { getLangByName } from "./helpers";
import helpers from "./helpers";
import users from "../fixtures/users.json";
const switchLang = name => {
@ -22,8 +22,9 @@ const switchLang = name => {
cy.contains(".locale-menu-popover a", name).click();
};
Cypress.Commands.add("switchLanguage", (name, force) => {
const code = getLangByName(name).code;
const { code } = helpers.getLangByName(name);
if (force) {
switchLang(name);
} else {

View File

@ -1,10 +1,8 @@
import find from 'lodash/find'
import locales from '../../webapp/locales'
const helpers = {
locales: require('../../webapp/locales'),
getLangByName: name => {
return find(helpers.locales, { name })
export default {
getLangByName(name) {
return find(locales, { name })
}
}
export default helpers

View File

@ -30,17 +30,6 @@
memory: "1G"
limits:
memory: "2G"
env:
- name: NEO4J_apoc_import_file_enabled
value: "true"
- name: NEO4J_dbms_memory_pagecache_size
value: "490M"
- name: NEO4J_dbms_memory_heap_max__size
value: "500M"
- name: NEO4J_dbms_memory_heap_initial__size
value: "500M"
- name: NEO4J_dbms_security_procedures_unrestricted
value: "algo.*,apoc.*"
envFrom:
- configMapRef:
name: configmap

View File

@ -9,6 +9,11 @@
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
NEO4J_AUTH: "none"
CLIENT_URI: "https://nitro-staging.human-connection.org"
NEO4J_apoc_import_file_enabled: "true"
NEO4J_dbms_memory_pagecache_size: "490M"
NEO4J_dbms_memory_heap_max__size: "500M"
NEO4J_dbms_memory_heap_initial__size: "500M"
NEO4J_dbms_security_procedures_unrestricted: "algo.*,apoc.*"
SENTRY_DSN_WEBAPP: ""
SENTRY_DSN_BACKEND: ""
COMMIT: ""

View File

@ -137,8 +137,8 @@ p.contentExcerpt = post.contentExcerpt,
p.visibility = toLower(post.visibility),
p.createdAt = post.createdAt.`$date`,
p.updatedAt = post.updatedAt.`$date`,
p.deleted = COALESCE(post.deleted,false),
p.disabled = NOT post.isEnabled
p.deleted = COALESCE(post.deleted, false),
p.disabled = COALESCE(NOT post.isEnabled, false)
WITH p, post
MATCH (u:User {id: post.userId})
MERGE (u)-[:WROTE]->(p)

View File

@ -23,13 +23,13 @@
"codecov": "^3.5.0",
"cross-env": "^5.2.0",
"cypress": "^3.4.1",
"cypress-cucumber-preprocessor": "^1.15.1",
"cypress-cucumber-preprocessor": "^1.16.0",
"cypress-file-upload": "^3.3.3",
"cypress-plugin-retries": "^1.2.2",
"dotenv": "^8.1.0",
"faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.5",
"neo4j-driver": "^1.7.6",
"neode": "^0.3.2",
"npm-run-all": "^4.1.5",
"slug": "^1.1.0"

View File

@ -3,6 +3,7 @@ import Editor from './Editor'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
import MutationObserver from 'mutation-observer'
import Vue from 'vue'
global.MutationObserver = MutationObserver
@ -55,9 +56,11 @@ describe('Editor.vue', () => {
propsData.value = 'I am a piece of text'
})
it.skip('renders', () => {
it('renders', async () => {
wrapper = Wrapper()
expect(wrapper.find('.ProseMirror').text()).toContain('I am a piece of text')
await Vue.nextTick().then(() => {
expect(wrapper.find('.editor-content').text()).toContain(propsData.value)
})
})
})
@ -88,6 +91,29 @@ describe('Editor.vue', () => {
)
})
describe('limists suggestion list to 15 users', () => {
beforeEach(() => {
let manyUsersList = []
for (let i = 0; i < 25; i++) {
manyUsersList.push({ id: `user${i}` })
}
propsData.users = manyUsersList
wrapper = Wrapper()
})
it('when query is empty', () => {
expect(
wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users),
).toHaveLength(15)
})
it('when query is present', () => {
expect(
wrapper.vm.editor.extensions.options.mention.onFilter(propsData.users, 'user'),
).toHaveLength(15)
})
})
it('sets the Hashtag items to the hashtags', () => {
propsData.hashtags = [
{
@ -105,6 +131,29 @@ describe('Editor.vue', () => {
}),
)
})
describe('limists suggestion list to 15 hashtags', () => {
beforeEach(() => {
let manyHashtagsList = []
for (let i = 0; i < 25; i++) {
manyHashtagsList.push({ id: `hashtag${i}` })
}
propsData.hashtags = manyHashtagsList
wrapper = Wrapper()
})
it('when query is empty', () => {
expect(
wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags),
).toHaveLength(15)
})
it('when query is present', () => {
expect(
wrapper.vm.editor.extensions.options.hashtag.onFilter(propsData.hashtags, 'hashtag'),
).toHaveLength(15)
})
})
})
})
})

View File

@ -203,12 +203,14 @@ export default {
filterSuggestionList(items, query) {
query = this.sanitizeQuery(query)
if (!query) {
return items
return items.slice(0, 15)
}
return items.filter(item => {
const filteredList = items.filter(item => {
const itemString = item.slug || item.id
return itemString.toLowerCase().includes(query.toLowerCase())
})
return filteredList.slice(0, 15)
},
sanitizeQuery(query) {
if (this.suggestionType === HASHTAG) {

View File

@ -37,9 +37,9 @@ describe('Notification', () => {
describe('given a notification about a comment on a post', () => {
beforeEach(() => {
propsData.notification = {
reason: 'comment_on_post',
post: null,
comment: {
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'comment-1',
contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
@ -56,7 +56,7 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.comment_on_post',
'notifications.menu.commented_on_post',
)
})
it('renders title', () => {
@ -92,14 +92,14 @@ describe('Notification', () => {
beforeEach(() => {
propsData.notification = {
reason: 'mentioned_in_post',
post: {
from: {
__typename: 'Post',
title: "It's a post title",
id: 'post-1',
slug: 'its-a-title',
contentExcerpt:
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
},
comment: null,
}
})
@ -138,8 +138,8 @@ describe('Notification', () => {
beforeEach(() => {
propsData.notification = {
reason: 'mentioned_in_comment',
post: null,
comment: {
from: {
__typename: 'Comment',
id: 'comment-1',
contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',

View File

@ -1,14 +1,8 @@
<template>
<ds-space :class="[{ read: notification.read }, notification]" margin-bottom="x-small">
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
<client-only>
<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" />
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
</ds-space>
<ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.menu.${notification.reason}`) }}
@ -22,16 +16,15 @@
>
<ds-space margin-bottom="x-small">
<ds-card
:header="post.title || comment.post.title"
:header="from.title || from.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>
<span v-if="isComment" class="comment-notification-header">Comment:</span>
{{ from.contentExcerpt | removeHtml }}
</div>
</ds-card>
</ds-space>
@ -54,23 +47,21 @@ export default {
},
},
computed: {
resourceType() {
return this.post.id ? 'Post' : 'Comment'
from() {
return this.notification.from
},
post() {
return this.notification.post || {}
},
comment() {
return this.notification.comment || {}
isComment() {
return this.from.__typename === 'Comment'
},
params() {
const post = this.isComment ? this.from.post : this.from
return {
id: this.post.id || this.comment.post.id,
slug: this.post.slug || this.comment.post.slug,
id: post.id,
slug: post.slug,
}
},
hashParam() {
return this.post.id ? {} : { hash: `#commentId-${this.comment.id}` }
return this.isComment ? { hash: `#commentId-${this.from.id}` } : {}
},
},
}

View File

@ -40,9 +40,9 @@ describe('NotificationList.vue', () => {
propsData = {
notifications: [
{
id: 'notification-41',
read: false,
post: {
from: {
__typename: 'Post',
id: 'post-1',
title: 'some post title',
slug: 'some-post-title',
@ -55,9 +55,9 @@ describe('NotificationList.vue', () => {
},
},
{
id: 'notification-42',
read: false,
post: {
from: {
__typename: 'Post',
id: 'post-2',
title: 'another post title',
slug: 'another-post-title',
@ -115,9 +115,9 @@ describe('NotificationList.vue', () => {
.trigger('click')
})
it("emits 'markAsRead' with the notificationId", () => {
it("emits 'markAsRead' with the id of the notification source", () => {
expect(wrapper.emitted('markAsRead')).toBeTruthy()
expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42'])
expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-2'])
})
})
})

View File

@ -4,7 +4,7 @@
v-for="notification in notifications"
:key="notification.id"
:notification="notification"
@read="markAsRead(notification.id)"
@read="markAsRead(notification.from.id)"
/>
</div>
</template>
@ -24,8 +24,8 @@ export default {
},
},
methods: {
markAsRead(notificationId) {
this.$emit('markAsRead', notificationId)
markAsRead(notificationSourceId) {
this.$emit('markAsRead', notificationSourceId)
},
},
}

View File

@ -18,7 +18,7 @@
<script>
import Dropdown from '~/components/Dropdown'
import { currentUserNotificationsQuery, updateNotificationMutation } from '~/graphql/User'
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import NotificationList from '../NotificationList/NotificationList'
export default {
@ -27,36 +27,41 @@ export default {
NotificationList,
Dropdown,
},
data() {
return {
notifications: [],
}
},
props: {
placement: { type: String },
},
computed: {
totalNotifications() {
return (this.notifications || []).length
},
},
methods: {
async markAsRead(notificationId) {
const variables = { id: notificationId, read: true }
async markAsRead(notificationSourceId) {
const variables = { id: notificationSourceId }
try {
await this.$apollo.mutate({
mutation: updateNotificationMutation(),
const {
data: { markAsRead },
} = await this.$apollo.mutate({
mutation: markAsReadMutation(),
variables,
})
if (!(markAsRead && markAsRead.read === true)) return
this.notifications = this.notifications.map(n => {
return n.from.id === markAsRead.from.id ? markAsRead : n
})
} catch (err) {
throw new Error(err)
}
},
},
computed: {
totalNotifications() {
return this.notifications.length
},
},
apollo: {
notifications: {
query: currentUserNotificationsQuery(),
update: data => {
const {
currentUser: { notifications },
} = data
return notifications
},
query: notificationQuery(),
},
},
}

View File

@ -1,5 +1,58 @@
import gql from 'graphql-tag'
const fragments = gql`
fragment post on Post {
id
createdAt
disabled
deleted
title
contentExcerpt
slug
author {
id
slug
name
disabled
deleted
avatar
}
}
fragment comment on 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 default i18n => {
const lang = i18n.locale().toUpperCase()
return gql`
@ -76,77 +129,37 @@ export default i18n => {
`
}
export const currentUserNotificationsQuery = () => {
export const notificationQuery = () => {
return gql`
${fragments}
query {
currentUser {
id
notifications(read: false, orderBy: createdAt_desc) {
id
read
reason
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
}
}
}
notifications(read: false, orderBy: createdAt_desc) {
read
reason
createdAt
from {
__typename
...post
...comment
}
}
}
`
}
export const updateNotificationMutation = () => {
export const markAsReadMutation = () => {
return gql`
mutation($id: ID!, $read: Boolean!) {
UpdateNotification(id: $id, read: $read) {
id
${fragments}
mutation($id: ID!) {
markAsRead(id: $id) {
read
reason
createdAt
from {
__typename
...post
...comment
}
}
}
`

View File

@ -134,7 +134,7 @@
"menu": {
"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 …"
"commented_on_post": "Hat deinen Beitrag kommentiert …"
}
},
"search": {

View File

@ -134,7 +134,7 @@
"menu": {
"mentioned_in_post": "Mentioned you in a post …",
"mentioned_in_comment": "Mentioned you in a comment …",
"comment_on_post": "Commented on your post …"
"commented_on_post": "Commented on your post …"
}
},
"search": {

View File

@ -63,7 +63,7 @@
"cross-env": "~5.2.0",
"date-fns": "2.0.1",
"express": "~4.17.1",
"graphql": "~14.5.3",
"graphql": "~14.5.4",
"isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0",
@ -78,7 +78,7 @@
"v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13",
"vue-infinite-scroll": "^2.0.2",
"vue-izitoast": "^1.2.0",
"vue-izitoast": "^1.2.1",
"vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1",
"zxcvbn": "^4.4.2"
@ -106,8 +106,8 @@
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~3.0.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.2",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-jest": "~22.16.0",
"eslint-plugin-node": "~9.2.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
@ -119,7 +119,7 @@
"node-sass": "~4.12.0",
"nodemon": "~1.19.1",
"prettier": "~1.18.2",
"sass-loader": "~7.3.1",
"sass-loader": "~8.0.0",
"style-loader": "~0.23.1",
"style-resources-loader": "~1.2.1",
"tippy.js": "^4.3.5",

View File

@ -185,9 +185,6 @@ export default {
return result
},
update({ Post }) {
// TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same
// result set twice.
this.hasMore = Post.length >= this.pageSize
const posts = uniqBy([...this.posts, ...Post], 'id')
this.posts = posts

View File

@ -104,9 +104,7 @@ describe('ProfileSlug', () => {
describe('currently no posts available (e.g. after tab switching)', () => {
beforeEach(() => {
wrapper.setData({
Post: null,
})
wrapper.setData({ posts: [], hasMore: false })
})
it('displays no "load more" button', () => {
@ -137,9 +135,7 @@ describe('ProfileSlug', () => {
}
})
wrapper.setData({
Post: posts,
})
wrapper.setData({ posts, hasMore: true })
})
it('displays a "load more" button', () => {
@ -170,9 +166,7 @@ describe('ProfileSlug', () => {
}
})
wrapper.setData({
Post: posts,
})
wrapper.setData({ posts, hasMore: false })
})
it('displays no "load more" button', () => {

View File

@ -219,8 +219,8 @@
</ds-space>
</ds-grid-item>
<template v-if="activePosts.length">
<masonry-grid-item v-for="(post, index) in activePosts" :key="post.id">
<template v-if="posts.length">
<masonry-grid-item v-for="(post, index) in posts" :key="post.id">
<hc-post-card
:post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }"
@ -229,10 +229,10 @@
</masonry-grid-item>
</template>
<template v-else-if="$apollo.loading">
<ds-grid-item>
<ds-section centered>
<ds-grid-item column-span="fullWidth">
<ds-space centered>
<ds-spinner size="base"></ds-spinner>
</ds-section>
</ds-space>
</ds-grid-item>
</template>
<template v-else>
@ -306,33 +306,21 @@ export default {
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
return {
User: [],
Post: [],
activePosts: [],
voted: false,
page: 1,
posts: [],
hasMore: false,
offset: 0,
pageSize: 6,
tabActive: 'post',
filter,
}
},
computed: {
hasMore() {
const total = {
post: this.user.contributionsCount,
shout: this.user.shoutedCount,
comment: this.user.commentedCount,
}[this.tabActive]
return this.Post && this.Post.length < total
},
myProfile() {
return this.$route.params.id === this.$store.getters['auth/user'].id
},
user() {
return this.User ? this.User[0] : {}
},
offset() {
return (this.page - 1) * this.pageSize
},
socialMediaLinks() {
const { socialMedia = [] } = this.user
return socialMedia.map(socialMedia => {
@ -355,19 +343,15 @@ export default {
throw new Error('User not found!')
}
},
Post(val) {
this.activePosts = this.setActivePosts()
},
},
methods: {
removePostFromList(index) {
this.activePosts.splice(index, 1)
this.$apollo.queries.User.refetch()
this.posts.splice(index, 1)
},
handleTab(tab) {
this.tabActive = tab
this.Post = null
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
this.resetPostList()
},
uniq(items, field = 'id') {
return uniqBy(items, field)
@ -377,38 +361,23 @@ export default {
this.$apollo.queries.User.refetch()
},
showMoreContributions() {
// this.page++
// Fetch more data and transform the original result
this.page++
this.$apollo.queries.Post.fetchMore({
variables: {
filter: this.filter,
first: this.pageSize,
offset: this.offset,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
let output = { Post: this.Post }
output.Post = [...previousResult.Post, ...fetchMoreResult.Post]
return output
},
fetchPolicy: 'cache-and-network',
})
this.offset += this.pageSize
},
setActivePosts() {
if (!this.Post) {
return []
}
return this.uniq(this.Post.filter(post => !post.deleted))
resetPostList() {
this.offset = 0
this.posts = []
this.hasMore = false
},
async block(user) {
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch()
},
async unblock(user) {
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch()
},
},
@ -421,10 +390,19 @@ export default {
return {
filter: this.filter,
first: this.pageSize,
offset: 0,
offset: this.offset,
orderBy: 'createdAt_desc',
}
},
fetchPolicy: 'cache-and-network',
update({ Post }) {
if (!Post) return
// TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same
// result set twice.
this.hasMore = Post.length >= this.pageSize
this.posts = this.uniq([...this.posts, ...Post])
},
},
User: {
query() {

View File

@ -1,3 +1,10 @@
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
import introspectionQueryResultData from './apollo-config/fragmentTypes.json'
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
})
export default ({ app }) => {
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
@ -10,5 +17,6 @@ export default ({ app }) => {
tokenName: 'human-connection-token',
persisting: false,
websocketsOnly: false,
cache: new InMemoryCache({ fragmentMatcher }),
}
}

View File

@ -0,0 +1,18 @@
{
"__schema": {
"types": [
{
"kind": "UNION",
"name": "NotificationSource",
"possibleTypes": [
{
"name": "Post"
},
{
"name": "Comment"
}
]
}
]
}
}

View File

@ -91,23 +91,6 @@ export const actions = {
id
url
}
notifications(read: false, orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
}
}
`,

View File

@ -1,99 +0,0 @@
import gql from 'graphql-tag'
export const state = () => {
return {
notifications: null,
pending: false,
}
}
export const mutations = {
SET_NOTIFICATIONS(state, notifications) {
state.notifications = notifications
},
SET_PENDING(state, pending) {
state.pending = pending
},
UPDATE_NOTIFICATIONS(state, notification) {
const notifications = state.notifications
const toBeUpdated = notifications.find(n => {
return n.id === notification.id
})
state.notifications = {
...toBeUpdated,
...notification,
}
},
}
export const getters = {
notifications(state) {
return !!state.notifications
},
}
export const actions = {
async init({ getters, commit }) {
if (getters.notifications) return
commit('SET_PENDING', true)
const client = this.app.apolloProvider.defaultClient
let notifications
try {
const {
data: { currentUser },
} = await client.query({
query: gql`
{
currentUser {
id
notifications(orderBy: createdAt_desc) {
id
read
createdAt
post {
author {
id
slug
name
disabled
deleted
}
title
contentExcerpt
slug
}
}
}
}
`,
})
notifications = currentUser.notifications
commit('SET_NOTIFICATIONS', notifications)
} finally {
commit('SET_PENDING', false)
}
return notifications
},
async markAsRead({ commit, rootGetters }, notificationId) {
const client = this.app.apolloProvider.defaultClient
const mutation = gql`
mutation($id: ID!, $read: Boolean!) {
UpdateNotification(id: $id, read: $read) {
id
read
}
}
`
const variables = {
id: notificationId,
read: true,
}
const {
data: { UpdateNotification },
} = await client.mutate({
mutation,
variables,
})
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
},
}

View File

@ -46,8 +46,8 @@ export const actions = {
await this.app.apolloProvider.defaultClient
.query({
query: gql`
query findPosts($query: String!) {
findPosts(query: $query, limit: 10) {
query findPosts($query: String!, $filter: _PostFilter) {
findPosts(query: $query, limit: 10, filter: $filter) {
id
slug
label: title
@ -64,6 +64,7 @@ export const actions = {
`,
variables: {
query: value.replace(/\s/g, '~ ') + '~',
filter: {},
},
})
.then(res => {

View File

@ -6379,12 +6379,12 @@ eslint-module-utils@^2.4.0:
debug "^2.6.8"
pkg-dir "^2.0.0"
eslint-plugin-es@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6"
integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==
eslint-plugin-es@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998"
integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA==
dependencies:
eslint-utils "^1.3.0"
eslint-utils "^1.4.2"
regexpp "^2.0.1"
eslint-plugin-import@~2.18.2:
@ -6404,20 +6404,20 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.15.2:
version "22.15.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.2.tgz#e3c10d9391f787744e31566f69ebb70c3a98e398"
integrity sha512-p4NME9TgXIt+KgpxcXyNBvO30ZKxwFAO1dJZBc2OGfDnXVEtPwEyNs95GSr6RIE3xLHdjd8ngDdE2icRRXrbxg==
eslint-plugin-jest@~22.16.0:
version "22.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.16.0.tgz#30c4e0e9dc331beb2e7369b70dd1363690c1ce05"
integrity sha512-eBtSCDhO1k7g3sULX/fuRK+upFQ7s548rrBtxDyM1fSoY7dTWp/wICjrJcDZKVsW7tsFfH22SG+ZaxG5BZodIg==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
eslint-plugin-node@~9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a"
integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw==
eslint-plugin-node@~9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.2.0.tgz#b1911f111002d366c5954a6d96d3cd5bf2a3036a"
integrity sha512-2abNmzAH/JpxI4gEOwd6K8wZIodK3BmHbTxz4s79OIYwwIt2gkpEXlAouJXu4H1c9ySTnRso0tsuthSOZbUMlA==
dependencies:
eslint-plugin-es "^1.4.0"
eslint-utils "^1.3.1"
eslint-plugin-es "^1.4.1"
eslint-utils "^1.4.2"
ignore "^5.1.1"
minimatch "^3.0.4"
resolve "^1.10.1"
@ -6455,7 +6455,7 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^1.3.0, eslint-utils@^1.3.1:
eslint-utils@^1.3.1, eslint-utils@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
@ -7627,10 +7627,10 @@ graphql-upload@^8.0.2:
http-errors "^1.7.2"
object-path "^0.11.4"
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.3:
version "14.5.3"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.3.tgz#e025851cc413e153220f4edbbb25d49f55104fa0"
integrity sha512-W8A8nt9BsMg0ZK2qA3DJIVU6muWhxZRYLTmc+5XGwzWzVdUdPVlAAg5hTBjiTISEnzsKL/onasu6vl3kgGTbYg==
"graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.4.0, graphql@~14.5.4:
version "14.5.4"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.4.tgz#b33fe957854e90c10d4c07c7d26b6c8e9f159a13"
integrity sha512-dPLvHoxy5m9FrkqWczPPRnH0X80CyvRE6e7Fa5AWEqEAzg9LpxHvKh24po/482E6VWHigOkAmb4xCp6P9yT9gw==
dependencies:
iterall "^1.2.2"
@ -8154,12 +8154,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5"
integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==
ignore@^5.1.4:
ignore@^5.1.1, ignore@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
@ -13337,7 +13332,7 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sass-loader@^7.1.0, sass-loader@~7.3.1:
sass-loader@^7.1.0:
version "7.3.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
@ -13348,6 +13343,17 @@ sass-loader@^7.1.0, sass-loader@~7.3.1:
pify "^4.0.1"
semver "^6.3.0"
sass-loader@~8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.0.tgz#e7b07a3e357f965e6b03dd45b016b0a9746af797"
integrity sha512-+qeMu563PN7rPdit2+n5uuYVR0SSVwm0JsOUsaJXzgYcClWSlmX0iHDnmeOobPkf5kUglVot3QS6SyLyaQoJ4w==
dependencies:
clone-deep "^4.0.1"
loader-utils "^1.2.3"
neo-async "^2.6.1"
schema-utils "^2.1.0"
semver "^6.3.0"
sass-resources-loader@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.0.tgz#88569c542fbf1f18f33a6578b77cc5b36c56911d"
@ -15165,10 +15171,10 @@ vue-infinite-scroll@^2.0.2:
resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
vue-izitoast@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.0.tgz#55b7434a391c6eb64dd10c0de211e99ba7e486e2"
integrity sha512-Jqxfid12SUBIySJxgyPpu6gZ1ssMcbKtCvu9uMQPNM8RUnd3RKC4nyxkncdYe5L6XPU+SaznjYRudnvtclY4wA==
vue-izitoast@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.2.1.tgz#cd2cbfbd96ea438dede8fb00f2c328364cb7141d"
integrity sha512-5krrKyAftSR3TnnO3zhMihYCSt0Lay4SBO1AWWKD3jhTErJrR+q9kOKyuAYhn1SttNER87hpnRKqdvLjzjHWQQ==
dependencies:
izitoast "^1.4.0"

1113
yarn.lock

File diff suppressed because it is too large Load Diff