Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1733-fix

This commit is contained in:
mattwr18 2019-10-22 11:07:29 +02:00
commit 333b38d1b2
38 changed files with 14803 additions and 765 deletions

13206
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,7 @@
"cheerio": "~1.0.0-rc.3", "cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5", "cors": "~2.8.5",
"cross-env": "~6.0.3", "cross-env": "~6.0.3",
"date-fns": "2.5.0", "date-fns": "2.5.1",
"debug": "~4.1.1", "debug": "~4.1.1",
"dotenv": "~8.2.0", "dotenv": "~8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
@ -82,7 +82,7 @@
"metascraper-lang-detector": "^4.8.5", "metascraper-lang-detector": "^4.8.5",
"metascraper-logo": "^5.7.6", "metascraper-logo": "^5.7.6",
"metascraper-publisher": "^5.7.6", "metascraper-publisher": "^5.7.6",
"metascraper-soundcloud": "^5.7.6", "metascraper-soundcloud": "^5.7.7",
"metascraper-title": "^5.7.6", "metascraper-title": "^5.7.6",
"metascraper-url": "^5.7.6", "metascraper-url": "^5.7.6",
"metascraper-video": "^5.7.6", "metascraper-video": "^5.7.6",

View File

@ -134,6 +134,7 @@ const permissions = shield(
PostsEmotionsByCurrentUser: isAuthenticated, PostsEmotionsByCurrentUser: isAuthenticated,
blockedUsers: isAuthenticated, blockedUsers: isAuthenticated,
notifications: isAuthenticated, notifications: isAuthenticated,
profilePagePosts: or(onlyEnabledContent, isModerator),
}, },
Mutation: { Mutation: {
'*': deny, '*': deny,
@ -174,6 +175,8 @@ const permissions = shield(
markAsRead: isAuthenticated, markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated, AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated, VerifyEmailAddress: isAuthenticated,
pinPost: isAdmin,
unpinPost: isAdmin,
}, },
User: { User: {
email: or(isMyOwn, isAdmin), email: or(isMyOwn, isAdmin),

View File

@ -28,12 +28,18 @@ module.exports = {
relationship: 'FOLLOWS', relationship: 'FOLLOWS',
target: 'User', target: 'User',
direction: 'out', direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
}, },
followedBy: { followedBy: {
type: 'relationship', type: 'relationship',
relationship: 'FOLLOWS', relationship: 'FOLLOWS',
target: 'User', target: 'User',
direction: 'in', direction: 'in',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
}, },
friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' }, friends: { type: 'relationship', relationship: 'FRIENDS', target: 'User', direction: 'both' },
disabledBy: { disabledBy: {
@ -98,6 +104,9 @@ module.exports = {
relationship: 'SHOUTED', relationship: 'SHOUTED',
target: 'Post', target: 'Post',
direction: 'out', direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
}, },
isIn: { isIn: {
type: 'relationship', type: 'relationship',
@ -105,6 +114,15 @@ module.exports = {
target: 'Location', target: 'Location',
direction: 'out', direction: 'out',
}, },
pinned: {
type: 'relationship',
relationship: 'PINNED',
target: 'Post',
direction: 'out',
properties: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
},
},
allowEmbedIframes: { allowEmbedIframes: {
type: 'boolean', type: 'boolean',
default: false, default: false,

View File

@ -131,6 +131,21 @@ describe('follow', () => {
}) })
}) })
test('adds `createdAt` to `FOLLOW` relationship', async () => {
await mutate({
mutation: mutationFollowUser,
variables,
})
const relation = await neode.cypher(
'MATCH (user:User {id: {id}})-[relationship:FOLLOWS]->(followed:User) WHERE relationship.createdAt IS NOT NULL RETURN relationship',
{ id: 'u1' },
)
const relationshipProperties = relation.records.map(
record => record.get('relationship').properties.createdAt,
)
expect(relationshipProperties[0]).toEqual(expect.any(String))
})
test('I can`t follow myself', async () => { test('I can`t follow myself', async () => {
variables.id = user1.id variables.id = user1.id
await expect(mutate({ mutation: mutationFollowUser, variables })).resolves.toMatchObject({ await expect(mutate({ mutation: mutationFollowUser, variables })).resolves.toMatchObject({
@ -155,6 +170,7 @@ describe('follow', () => {
}) })
}) })
}) })
describe('unfollow user', () => { describe('unfollow user', () => {
beforeEach(async () => { beforeEach(async () => {
variables = { id: user2.id } variables = { id: user2.id }

View File

@ -86,6 +86,7 @@ export default function Resolver(type, options = {}) {
} }
return resolvers return resolvers
} }
const result = { const result = {
...undefinedToNullResolver(undefinedToNull), ...undefinedToNullResolver(undefinedToNull),
...booleanResolver(boolean), ...booleanResolver(boolean),

View File

@ -2,10 +2,9 @@ import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js' import { neo4jgraphql } from 'neo4j-graphql-js'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import { getBlockedUsers, getBlockedByUsers } from './users.js' import { getBlockedUsers, getBlockedByUsers } from './users.js'
import { mergeWith, isArray } from 'lodash' import { mergeWith, isArray, isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
const filterForBlockedUsers = async (params, context) => { const filterForBlockedUsers = async (params, context) => {
if (!context.user) return params if (!context.user) return params
const [blockedUsers, blockedByUsers] = await Promise.all([ const [blockedUsers, blockedByUsers] = await Promise.all([
@ -29,16 +28,31 @@ const filterForBlockedUsers = async (params, context) => {
return params return params
} }
const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinnedBy_in: { role_in: ['admin'] } }
if (isEmpty(params.filter)) {
params.filter = { OR: [pinnedPostFilter, {}] }
} else {
params.filter = { OR: [pinnedPostFilter, { ...params.filter }] }
}
return params
}
export default { export default {
Query: { Query: {
Post: async (object, params, context, resolveInfo) => { Post: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context) params = await filterForBlockedUsers(params, context)
params = await maintainPinnedPosts(params)
return neo4jgraphql(object, params, context, resolveInfo, false) return neo4jgraphql(object, params, context, resolveInfo, false)
}, },
findPosts: async (object, params, context, resolveInfo) => { findPosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context) params = await filterForBlockedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo, false) return neo4jgraphql(object, params, context, resolveInfo, false)
}, },
profilePagePosts: async (object, params, context, resolveInfo) => {
params = await filterForBlockedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo, false)
},
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => { PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
const session = context.driver.session() const session = context.driver.session()
const { postId, data } = params const { postId, data } = params
@ -115,10 +129,10 @@ export default {
delete params.categoryIds delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
const session = context.driver.session() const session = context.driver.session()
let updatePostCypher = `MATCH (post:Post {id: $params.id}) let updatePostCypher = `MATCH (post:Post {id: $params.id})
SET post += $params SET post += $params
SET post.updatedAt = toString(datetime()) SET post.updatedAt = toString(datetime())
WITH post
` `
if (categoryIds && categoryIds.length) { if (categoryIds && categoryIds.length) {
@ -131,10 +145,10 @@ export default {
await session.run(cypherDeletePreviousRelations, { params }) await session.run(cypherDeletePreviousRelations, { params })
updatePostCypher += ` updatePostCypher += `
WITH post
UNWIND $categoryIds AS categoryId UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId}) MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category) MERGE (post)-[:CATEGORIZED]->(category)
WITH post
` `
} }
@ -211,10 +225,75 @@ export default {
}) })
return emoted return emoted
}, },
pinPost: async (_parent, params, context, _resolveInfo) => {
let pinnedPostWithNestedAttributes
const { driver, user } = context
const session = driver.session()
const { id: userId } = user
let writeTxResultPromise = session.writeTransaction(async transaction => {
const deletePreviousRelationsResponse = await transaction.run(
`
MATCH (:User)-[previousRelations:PINNED]->(post:Post)
DELETE previousRelations
RETURN post
`,
)
return deletePreviousRelationsResponse.records.map(record => record.get('post').properties)
})
await writeTxResultPromise
writeTxResultPromise = session.writeTransaction(async transaction => {
const pinPostTransactionResponse = await transaction.run(
`
MATCH (user:User {id: $userId}) WHERE user.role = 'admin'
MATCH (post:Post {id: $params.id})
MERGE (user)-[pinned:PINNED {createdAt: toString(datetime())}]->(post)
RETURN post, pinned.createdAt as pinnedAt
`,
{ userId, params },
)
return pinPostTransactionResponse.records.map(record => ({
pinnedPost: record.get('post').properties,
pinnedAt: record.get('pinnedAt'),
}))
})
try {
const [transactionResult] = await writeTxResultPromise
const { pinnedPost, pinnedAt } = transactionResult
pinnedPostWithNestedAttributes = {
...pinnedPost,
pinnedAt,
}
} finally {
session.close()
}
return pinnedPostWithNestedAttributes
},
unpinPost: async (_parent, params, context, _resolveInfo) => {
let unpinnedPost
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async transaction => {
const unpinPostTransactionResponse = await transaction.run(
`
MATCH (:User)-[previousRelations:PINNED]->(post:Post {id: $params.id})
DELETE previousRelations
RETURN post
`,
{ params },
)
return unpinPostTransactionResponse.records.map(record => record.get('post').properties)
})
try {
;[unpinnedPost] = await writeTxResultPromise
} finally {
session.close()
}
return unpinnedPost
},
}, },
Post: { Post: {
...Resolver('Post', { ...Resolver('Post', {
undefinedToNull: ['activityId', 'objectId', 'image', 'language'], undefinedToNull: ['activityId', 'objectId', 'image', 'language', 'pinnedAt'],
hasMany: { hasMany: {
tags: '-[:TAGGED]->(related:Tag)', tags: '-[:TAGGED]->(related:Tag)',
categories: '-[:CATEGORIZED]->(related:Category)', categories: '-[:CATEGORIZED]->(related:Category)',
@ -225,6 +304,7 @@ export default {
hasOne: { hasOne: {
author: '<-[:WROTE]-(related:User)', author: '<-[:WROTE]-(related:User)',
disabledBy: '<-[:DISABLED]-(related:User)', disabledBy: '<-[:DISABLED]-(related:User)',
pinnedBy: '<-[:PINNED]-(related:User)',
}, },
count: { count: {
commentsCount: commentsCount:

View File

@ -39,7 +39,8 @@ const createPostMutation = gql`
} }
` `
beforeAll(() => { beforeAll(async () => {
await factory.cleanDatabase()
const { server } = createServer({ const { server } = createServer({
context: () => { context: () => {
return { return {
@ -269,7 +270,10 @@ describe('CreatePost', () => {
}) })
it('creates a post', async () => { it('creates a post', async () => {
const expected = { data: { CreatePost: { title: 'I am a title', content: 'Some content' } } } const expected = {
data: { CreatePost: { title: 'I am a title', content: 'Some content' } },
errors: undefined,
}
await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
expected, expected,
) )
@ -285,6 +289,7 @@ describe('CreatePost', () => {
}, },
}, },
}, },
errors: undefined,
} }
await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject( await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
expected, expected,
@ -366,7 +371,12 @@ describe('UpdatePost', () => {
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) { mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) { UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id id
title
content content
author {
name
slug
}
categories { categories {
id id
} }
@ -386,7 +396,6 @@ describe('UpdatePost', () => {
}) })
variables = { variables = {
...variables,
id: 'p9876', id: 'p9876',
title: 'New title', title: 'New title',
content: 'New content', content: 'New content',
@ -395,8 +404,11 @@ describe('UpdatePost', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: updatePostMutation, variables }) authenticatedUser = null
expect(errors[0]).toHaveProperty('message', 'Not Authorised!') expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { UpdatePost: null },
})
}) })
}) })
@ -550,6 +562,371 @@ describe('UpdatePost', () => {
}) })
}) })
}) })
describe('pin posts', () => {
const pinPostMutation = gql`
mutation($id: ID!) {
pinPost(id: $id) {
id
title
content
author {
name
slug
}
pinnedBy {
id
name
role
}
createdAt
updatedAt
pinnedAt
}
}
`
beforeEach(async () => {
variables = { ...variables }
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
authenticatedUser = null
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { pinPost: null },
})
})
})
describe('users cannot pin posts', () => {
it('throws authorization error', async () => {
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { pinPost: null },
})
})
})
describe('moderators cannot pin posts', () => {
let moderator
beforeEach(async () => {
moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() })
authenticatedUser = await moderator.toJson()
})
it('throws authorization error', async () => {
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { pinPost: null },
})
})
})
describe('admin can pin posts', () => {
let admin
beforeEach(async () => {
admin = await user.update({
role: 'admin',
name: 'Admin',
updatedAt: new Date().toISOString(),
})
authenticatedUser = await admin.toJson()
})
describe('post created by them', () => {
beforeEach(async () => {
await factory.create('Post', {
id: 'created-and-pinned-by-same-admin',
author: admin,
})
})
it('responds with the updated Post', async () => {
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
const expected = {
data: {
pinPost: {
id: 'created-and-pinned-by-same-admin',
author: {
name: 'Admin',
},
pinnedBy: {
id: 'current-user',
name: 'Admin',
role: 'admin',
},
},
},
errors: undefined,
}
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
it('sets createdAt date for PINNED', async () => {
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
const expected = {
data: {
pinPost: {
id: 'created-and-pinned-by-same-admin',
pinnedAt: expect.any(String),
},
},
errors: undefined,
}
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
})
describe('post created by another admin', () => {
let otherAdmin
beforeEach(async () => {
otherAdmin = await factory.create('User', {
role: 'admin',
name: 'otherAdmin',
})
authenticatedUser = await otherAdmin.toJson()
await factory.create('Post', {
id: 'created-by-one-admin-pinned-by-different-one',
author: otherAdmin,
})
})
it('responds with the updated Post', async () => {
authenticatedUser = await admin.toJson()
variables = { ...variables, id: 'created-by-one-admin-pinned-by-different-one' }
const expected = {
data: {
pinPost: {
id: 'created-by-one-admin-pinned-by-different-one',
author: {
name: 'otherAdmin',
},
pinnedBy: {
id: 'current-user',
name: 'Admin',
role: 'admin',
},
},
},
errors: undefined,
}
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
})
describe('post created by another user', () => {
it('responds with the updated Post', async () => {
const expected = {
data: {
pinPost: {
id: 'p9876',
author: {
slug: 'the-author',
},
pinnedBy: {
id: 'current-user',
name: 'Admin',
role: 'admin',
},
},
},
errors: undefined,
}
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
})
describe('removes other pinned post', () => {
let pinnedPost
beforeEach(async () => {
await factory.create('Post', {
id: 'only-pinned-post',
author: admin,
})
await mutate({ mutation: pinPostMutation, variables })
variables = { ...variables, id: 'only-pinned-post' }
await mutate({ mutation: pinPostMutation, variables })
pinnedPost = await neode.cypher(
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
)
})
it('leaves only one pinned post at a time', async () => {
expect(pinnedPost.records).toHaveLength(1)
})
})
describe('PostOrdering', () => {
let pinnedPost, postCreatedAfterPinnedPost, newDate, timeInPast, admin
beforeEach(async () => {
;[pinnedPost, postCreatedAfterPinnedPost] = await Promise.all([
neode.create('Post', {
id: 'im-a-pinned-post',
}),
neode.create('Post', {
id: 'i-was-created-after-pinned-post',
}),
])
newDate = new Date()
timeInPast = newDate.getDate() - 3
newDate.setDate(timeInPast)
await pinnedPost.update({
createdAt: newDate.toISOString(),
updatedAt: new Date().toISOString(),
})
timeInPast = newDate.getDate() + 1
newDate.setDate(timeInPast)
await postCreatedAfterPinnedPost.update({
createdAt: newDate.toISOString(),
updatedAt: new Date().toISOString(),
})
admin = await user.update({
role: 'admin',
name: 'Admin',
updatedAt: new Date().toISOString(),
})
await admin.relateTo(pinnedPost, 'pinned')
})
it('pinned post appear first even when created before other posts', async () => {
const postOrderingQuery = gql`
query($orderBy: [_PostOrdering]) {
Post(orderBy: $orderBy) {
id
pinnedAt
}
}
`
const expected = {
data: {
Post: [
{
id: 'im-a-pinned-post',
pinnedAt: expect.any(String),
},
{
id: 'p9876',
pinnedAt: null,
},
{
id: 'i-was-created-after-pinned-post',
pinnedAt: null,
},
],
},
}
variables = { orderBy: ['pinnedAt_asc', 'createdAt_desc'] }
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject(
expected,
)
})
})
})
})
describe('unpin posts', () => {
const unpinPostMutation = gql`
mutation($id: ID!) {
unpinPost(id: $id) {
id
title
content
author {
name
slug
}
pinnedBy {
id
name
role
}
createdAt
updatedAt
}
}
`
beforeEach(async () => {
variables = { ...variables }
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
authenticatedUser = null
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { unpinPost: null },
})
})
})
describe('users cannot unpin posts', () => {
it('throws authorization error', async () => {
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { unpinPost: null },
})
})
})
describe('moderators cannot unpin posts', () => {
let moderator
beforeEach(async () => {
moderator = await user.update({ role: 'moderator', updatedAt: new Date().toISOString() })
authenticatedUser = await moderator.toJson()
})
it('throws authorization error', async () => {
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { unpinPost: null },
})
})
})
describe('admin can unpin posts', () => {
let admin, pinnedPost
beforeEach(async () => {
pinnedPost = await factory.create('Post', { id: 'post-to-be-unpinned' })
admin = await user.update({
role: 'admin',
name: 'Admin',
updatedAt: new Date().toISOString(),
})
authenticatedUser = await admin.toJson()
await admin.relateTo(pinnedPost, 'pinned', { createdAt: new Date().toISOString() })
})
it('responds with the unpinned Post', async () => {
authenticatedUser = await admin.toJson()
variables = { ...variables, id: 'post-to-be-unpinned' }
const expected = {
data: {
unpinPost: {
id: 'post-to-be-unpinned',
pinnedBy: null,
},
},
errors: undefined,
}
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
})
})
}) })
describe('DeletePost', () => { describe('DeletePost', () => {

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ export default {
const transactionRes = await session.run( const transactionRes = await session.run(
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId}) `MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {id: $userId})
WHERE $type IN labels(node) AND NOT userWritten.id = $userId WHERE $type IN labels(node) AND NOT userWritten.id = $userId
MERGE (user)-[relation:SHOUTED]->(node) MERGE (user)-[relation:SHOUTED{createdAt:toString(datetime())}]->(node)
RETURN COUNT(relation) > 0 as isShouted`, RETURN COUNT(relation) > 0 as isShouted`,
{ {
id, id,

View File

@ -102,6 +102,22 @@ describe('shout and unshout posts', () => {
}) })
}) })
it('adds `createdAt` to `SHOUT` relationship', async () => {
variables = { id: 'another-user-post-id' }
await mutate({ mutation: mutationShoutPost, variables })
const relation = await instance.cypher(
'MATCH (user:User {id: $userId1})-[relationship:SHOUTED]->(node {id: $userId2}) WHERE relationship.createdAt IS NOT NULL RETURN relationship',
{
userId1: 'current-user-id',
userId2: 'another-user-post-id',
},
)
const relationshipProperties = relation.records.map(
record => record.get('relationship').properties.createdAt,
)
expect(relationshipProperties[0]).toEqual(expect.any(String))
})
it('can not shout my own post', async () => { it('can not shout my own post', async () => {
variables = { id: 'current-user-post-id' } variables = { id: 'current-user-post-id' }
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({ await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({

View File

@ -16,6 +16,10 @@ type Post {
createdAt: String createdAt: String
updatedAt: String updatedAt: String
language: String language: String
pinnedAt: String @cypher(
statement: "MATCH (this)<-[pinned:PINNED]-(:User) WHERE NOT this.deleted = true AND NOT this.disabled = true RETURN pinned.createdAt"
)
pinnedBy: User @relation(name:"PINNED", direction: "IN")
relatedContributions: [Post]! relatedContributions: [Post]!
@cypher( @cypher(
statement: """ statement: """
@ -40,7 +44,7 @@ type Post {
@cypher( @cypher(
statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)" statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
) )
# Has the currently logged in user shouted that post? # Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean! shoutedByCurrentUser: Boolean!
@cypher( @cypher(
@ -84,9 +88,12 @@ type Mutation {
DeletePost(id: ID!): Post DeletePost(id: ID!): Post
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
pinPost(id: ID!): Post
unpinPost(id: ID!): Post
} }
type Query { type Query {
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int! PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String] PostsEmotionsByCurrentUser(postId: ID!): [String]
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]
} }

View File

@ -1,182 +1,181 @@
type User { type User {
id: ID! id: ID!
actorId: String actorId: String
name: String name: String
email: String! @cypher(statement: "MATCH (this)-[: PRIMARY_EMAIL]->(e: EmailAddress) RETURN e.email") email: String! @cypher(statement: "MATCH (this)-[: PRIMARY_EMAIL]->(e: EmailAddress) RETURN e.email")
slug: String! slug: String!
avatar: String avatar: String
coverImg: String coverImg: String
deleted: Boolean deleted: Boolean
disabled: Boolean disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN") disabledBy: User @relation(name: "DISABLED", direction: "IN")
role: UserGroup! role: UserGroup!
publicKey: String publicKey: String
invitedBy: User @relation(name: "INVITED", direction: "IN") invitedBy: User @relation(name: "INVITED", direction: "IN")
invited: [User] @relation(name: "INVITED", direction: "OUT") invited: [User] @relation(name: "INVITED", direction: "OUT")
location: Location @cypher(statement: "MATCH (this)-[: IS_IN]->(l: Location) RETURN l") location: Location @cypher(statement: "MATCH (this)-[: IS_IN]->(l: Location) RETURN l")
locationName: String locationName: String
about: String about: String
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN") socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "IN")
# createdAt: DateTime # createdAt: DateTime
# updatedAt: DateTime # updatedAt: DateTime
createdAt: String createdAt: String
updatedAt: String updatedAt: String
termsAndConditionsAgreedVersion: String termsAndConditionsAgreedVersion: String
termsAndConditionsAgreedAt: String termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean allowEmbedIframes: Boolean
locale: String
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)")
locale: String following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
followingCount: Int! @cypher(statement: "MATCH (this)-[: FOLLOWS]->(r: User) RETURN COUNT(DISTINCT r)")
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)") followedByCount: Int! @cypher(statement: "MATCH (this)<-[: FOLLOWS]-(r: User) RETURN COUNT(DISTINCT r)")
following: [User]! @relation(name: "FOLLOWS", direction: "OUT") # Is the currently logged in user following that user?
followingCount: Int! @cypher(statement: "MATCH (this)-[: FOLLOWS]->(r: User) RETURN COUNT(DISTINCT r)") followedByCurrentUser: Boolean! @cypher(
statement: """
MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
isBlocked: Boolean! @cypher(
statement: """
MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") # contributions: [WrittenPost]!
followedByCount: Int! @cypher(statement: "MATCH (this)<-[: FOLLOWS]-(r: User) RETURN COUNT(DISTINCT r)") # contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
# @cypher(
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
# )
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
contributionsCount: Int! @cypher(
statement: """
MATCH (this)-[: WROTE]->(r: Post)
WHERE NOT r.deleted = true AND NOT r.disabled = true
RETURN COUNT(r)
"""
)
# Is the currently logged in user following that user? comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
followedByCurrentUser: Boolean! @cypher( 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))")
statement: """
MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
isBlocked: Boolean! @cypher(
statement: """
MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
# contributions: [WrittenPost]! shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
# contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! shoutedCount: Int! @cypher(statement: "MATCH (this)-[: SHOUTED]->(r: Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
# @cypher(
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
# )
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
contributionsCount: Int! @cypher(
statement: """
MATCH (this)-[: WROTE]->(r: Post)
WHERE NOT r.deleted = true AND NOT r.disabled = true
RETURN COUNT(r)
"""
)
comments: [Comment]! @relation(name: "WROTE", direction: "OUT") categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
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") badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[: SHOUTED]->(r: Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)") badgesCount: Int! @cypher(statement: "MATCH (this)<-[: REWARDED]-(r: Badge) RETURN COUNT(r)")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT") emotions: [EMOTED]
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[: REWARDED]-(r: Badge) RETURN COUNT(r)")
emotions: [EMOTED]
} }
input _UserFilter { input _UserFilter {
AND: [_UserFilter!] AND: [_UserFilter!]
OR: [_UserFilter!] OR: [_UserFilter!]
name_contains: String name_contains: String
about_contains: String about_contains: String
slug_contains: String slug_contains: String
id: ID id: ID
id_not: ID id_not: ID
id_in: [ID!] id_in: [ID!]
id_not_in: [ID!] id_not_in: [ID!]
id_contains: ID id_contains: ID
id_not_contains: ID id_not_contains: ID
id_starts_with: ID id_starts_with: ID
id_not_starts_with: ID id_not_starts_with: ID
id_ends_with: ID id_ends_with: ID
id_not_ends_with: ID id_not_ends_with: ID
friends: _UserFilter friends: _UserFilter
friends_not: _UserFilter friends_not: _UserFilter
friends_in: [_UserFilter!] friends_in: [_UserFilter!]
friends_not_in: [_UserFilter!] friends_not_in: [_UserFilter!]
friends_some: _UserFilter friends_some: _UserFilter
friends_none: _UserFilter friends_none: _UserFilter
friends_single: _UserFilter friends_single: _UserFilter
friends_every: _UserFilter friends_every: _UserFilter
following: _UserFilter following: _UserFilter
following_not: _UserFilter following_not: _UserFilter
following_in: [_UserFilter!] following_in: [_UserFilter!]
following_not_in: [_UserFilter!] following_not_in: [_UserFilter!]
following_some: _UserFilter following_some: _UserFilter
following_none: _UserFilter following_none: _UserFilter
following_single: _UserFilter following_single: _UserFilter
following_every: _UserFilter following_every: _UserFilter
followedBy: _UserFilter followedBy: _UserFilter
followedBy_not: _UserFilter followedBy_not: _UserFilter
followedBy_in: [_UserFilter!] followedBy_in: [_UserFilter!]
followedBy_not_in: [_UserFilter!] followedBy_not_in: [_UserFilter!]
followedBy_some: _UserFilter followedBy_some: _UserFilter
followedBy_none: _UserFilter followedBy_none: _UserFilter
followedBy_single: _UserFilter followedBy_single: _UserFilter
followedBy_every: _UserFilter followedBy_every: _UserFilter
role_in: [UserGroup!]
} }
type Query { type Query {
User( User(
id: ID id: ID
email: String email: String
actorId: String actorId: String
name: String name: String
slug: String slug: String
avatar: String avatar: String
coverImg: String coverImg: String
role: UserGroup role: UserGroup
locationName: String locationName: String
about: String about: String
createdAt: String createdAt: String
updatedAt: String updatedAt: String
friendsCount: Int friendsCount: Int
followingCount: Int followingCount: Int
followedByCount: Int followedByCount: Int
followedByCurrentUser: Boolean followedByCurrentUser: Boolean
contributionsCount: Int contributionsCount: Int
commentedCount: Int commentedCount: Int
shoutedCount: Int shoutedCount: Int
badgesCount: Int badgesCount: Int
first: Int first: Int
offset: Int offset: Int
orderBy: [_UserOrdering] orderBy: [_UserOrdering]
filter: _UserFilter filter: _UserFilter
): [User] ): [User]
blockedUsers: [User] blockedUsers: [User]
currentUser: User currentUser: User
} }
type Mutation { type Mutation {
UpdateUser ( UpdateUser (
id: ID! id: ID!
name: String name: String
email: String email: String
slug: String slug: String
avatar: String avatar: String
coverImg: String coverImg: String
avatarUpload: Upload avatarUpload: Upload
locationName: String locationName: String
about: String about: String
termsAndConditionsAgreedVersion: String termsAndConditionsAgreedVersion: String
termsAndConditionsAgreedAt: String termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean allowEmbedIframes: Boolean
locale: String locale: String
): User ): User
DeleteUser(id: ID!, resource: [Deletable]): User DeleteUser(id: ID!, resource: [Deletable]): User
block(id: ID!): User block(id: ID!): User
unblock(id: ID!): User unblock(id: ID!): User
} }

View File

@ -2782,10 +2782,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0" whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0" whatwg-url "^7.0.0"
date-fns@2.5.0: date-fns@2.5.1:
version "2.5.0" version "2.5.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.5.0.tgz#b939f17c2902ce81cffe449702ba22c0781b38ec" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.5.1.tgz#6bd76f01d3a438e9c481d4c18512ddac37585b4c"
integrity sha512-I6Tkis01//nRcmvMQw/MRE1HAtcuA5Ie6jGPb8bJZJub7494LGOObqkV3ParnsSVviAjk5C8mNKDqYVBzCopWg== integrity sha512-ZBrQmuaqH9YqIejbgu8f09ki7wdD2JxWsRTZ/+HnnLNmkI56ty0evnWzKY+ihLT0xX5VdUX0vDNZCxJJGKX2+Q==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9" version "2.6.9"
@ -5802,14 +5802,14 @@ metascraper-publisher@^5.7.6:
dependencies: dependencies:
"@metascraper/helpers" "^5.7.6" "@metascraper/helpers" "^5.7.6"
metascraper-soundcloud@^5.7.6: metascraper-soundcloud@^5.7.7:
version "5.7.6" version "5.7.7"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.7.6.tgz#80c725e8746d94c992b5bdd07ac6bd987d09944d" resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.7.7.tgz#b7cb56e5ce7744aed90c83e37303e9b40ad3ff9d"
integrity sha512-fBxX5mYPFf8rWhhEX2XZD5QrmvtUI5IIPzryGuwEWsbPuMGuUkvFA9JjHJiC46uYXoi6UuKLXwSmYHcAACG3Jg== integrity sha512-TDJxUwFJCxU4bTrrx3GWiGeZdNhvRhlI61JiprLkYBriM65uzCfaJ5FjS5uzZy1CfMYhvQgxLZ7XRq1bgbPpTg==
dependencies: dependencies:
"@metascraper/helpers" "^5.7.6" "@metascraper/helpers" "^5.7.6"
memoize-one "~5.1.1" memoize-one "~5.1.1"
tldts "~5.5.0" tldts "~5.6.1"
metascraper-title@^5.7.6: metascraper-title@^5.7.6:
version "5.7.6" version "5.7.6"
@ -8026,17 +8026,17 @@ tlds@^1.187.0, tlds@^1.203.0:
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc"
integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw== integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw==
tldts-core@^5.5.0: tldts-core@^5.6.1:
version "5.5.0" version "5.6.1"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.5.0.tgz#ae22afe586541ac5ecacc520038068639b3420b4" resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.6.1.tgz#943fd020b564018fae308c12ec2435e53101c257"
integrity sha512-o0JzahqioihXz8wj7/1OYtefyhXz/PwLno7VRm5MTwQitEOPpvMPZpj2yjXtjgOMKbi3A5OHvvJwhFf0Hutzng== integrity sha512-ikhUCHoiRu0QzQpba0f0q1Km5YBnn4qsBzGlYCzT3y3wSCGG2GlV0xeEOcXTzp2pRne6bQaHRry4TINMZpDFKQ==
tldts@~5.5.0: tldts@~5.6.1:
version "5.5.0" version "5.6.1"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.5.0.tgz#12ea124593bc5abebd12107c6223986f97972bc1" resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.6.1.tgz#36f4ac97505b9202f2872f6246f326589f49d78b"
integrity sha512-CZ/d7Y4k8onxwerMWz/mTCeKJtX3VAMiL+ajXVFnxsKhH4BV+QavjnZ1Mb9OeCHo3jX0S3Dw6ERNRXqOMVsDvw== integrity sha512-I+imSP592J9GUYApIoiDdJk3KlroHY4zmDmpAp+TlIDZZAPxx192yOUViMB2QmlcRtZUz5XLEM3cS2F0V7P1Fw==
dependencies: dependencies:
tldts-core "^5.5.0" tldts-core "^5.6.1"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"

View File

@ -1,4 +1,4 @@
FROM neo4j:3.5.11-enterprise FROM neo4j:3.5.12-enterprise
LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)" LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
ARG BUILD_COMMIT ARG BUILD_COMMIT

View File

@ -24,7 +24,7 @@
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"cypress": "^3.4.1", "cypress": "^3.4.1",
"cypress-cucumber-preprocessor": "^1.16.2", "cypress-cucumber-preprocessor": "^1.16.2",
"cypress-file-upload": "^3.3.4", "cypress-file-upload": "^3.4.0",
"cypress-plugin-retries": "^1.3.0", "cypress-plugin-retries": "^1.3.0",
"date-fns": "^2.5.0", "date-fns": "^2.5.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View File

@ -149,6 +149,7 @@ export default {
}, },
editCommentMenu(showMenu) { editCommentMenu(showMenu) {
this.openEditCommentMenu = showMenu this.openEditCommentMenu = showMenu
this.$emit('toggleNewCommentForm', !showMenu)
}, },
updateComment(comment) { updateComment(comment) {
this.$emit('updateComment', comment) this.$emit('updateComment', comment)

View File

@ -25,6 +25,7 @@
:routeHash="routeHash" :routeHash="routeHash"
@deleteComment="updateCommentList" @deleteComment="updateCommentList"
@updateComment="updateCommentList" @updateComment="updateCommentList"
@toggleNewCommentForm="toggleNewCommentForm"
/> />
</div> </div>
</div> </div>
@ -51,6 +52,9 @@ export default {
return comment.id === updatedComment.id ? updatedComment : comment return comment.id === updatedComment.id ? updatedComment : comment
}) })
}, },
toggleNewCommentForm(showNewCommentForm) {
this.$emit('toggleNewCommentForm', showNewCommentForm)
},
}, },
} }
</script> </script>

View File

@ -55,24 +55,46 @@ export default {
routes() { routes() {
let routes = [] let routes = []
if (this.isOwner && this.resourceType === 'contribution') { if (this.resourceType === 'contribution') {
routes.push({ if (this.isOwner) {
name: this.$t(`post.menu.edit`), routes.push({
path: this.$router.resolve({ name: this.$t(`post.menu.edit`),
name: 'post-edit-id', path: this.$router.resolve({
params: { name: 'post-edit-id',
id: this.resource.id, params: {
id: this.resource.id,
},
}).href,
icon: 'edit',
})
routes.push({
name: this.$t(`post.menu.delete`),
callback: () => {
this.openModal('delete')
}, },
}).href, icon: 'trash',
icon: 'edit', })
}) }
routes.push({
name: this.$t(`post.menu.delete`), if (this.isAdmin) {
callback: () => { if (!this.resource.pinnedBy) {
this.openModal('delete') routes.push({
}, name: this.$t(`post.menu.pin`),
icon: 'trash', callback: () => {
}) this.$emit('pinPost', this.resource)
},
icon: 'link',
})
} else {
routes.push({
name: this.$t(`post.menu.unpin`),
callback: () => {
this.$emit('unpinPost', this.resource)
},
icon: 'unlink',
})
}
}
} }
if (this.isOwner && this.resourceType === 'comment') { if (this.isOwner && this.resourceType === 'comment') {
@ -155,6 +177,9 @@ export default {
isModerator() { isModerator() {
return this.$store.getters['auth/isModerator'] return this.$store.getters['auth/isModerator']
}, },
isAdmin() {
return this.$store.getters['auth/isAdmin']
},
}, },
methods: { methods: {
openItem(route, toggleMenu) { openItem(route, toggleMenu) {

View File

@ -2,7 +2,7 @@ import { config, shallowMount, mount, createLocalVue, RouterLinkStub } from '@vu
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex' import Vuex from 'vuex'
import Filters from '~/plugins/vue-filters' import Filters from '~/plugins/vue-filters'
import PostCard from '.' import PostCard from './PostCard.vue'
const localVue = createLocalVue() const localVue = createLocalVue()

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/vue' import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y' import { withA11y } from '@storybook/addon-a11y'
import HcPostCard from '~/components/PostCard' import HcPostCard from './PostCard.vue'
import helpers from '~/storybook/helpers' import helpers from '~/storybook/helpers'
helpers.init() helpers.init()
@ -76,3 +76,23 @@ storiesOf('Post Card', module)
/> />
`, `,
})) }))
.add('pinned by admin', () => ({
components: { HcPostCard },
store: helpers.store,
data: () => ({
post: {
...post,
pinnedBy: {
id: '4711',
name: 'Ad Min',
role: 'admin',
},
},
}),
template: `
<hc-post-card
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
/>
`,
}))

View File

@ -1,7 +1,7 @@
<template> <template>
<ds-card <ds-card
:image="post.image | proxyApiUrl" :image="post.image | proxyApiUrl"
:class="{ 'post-card': true, 'disabled-content': post.disabled }" :class="{ 'post-card': true, 'disabled-content': post.disabled, 'post--pinned': isPinned }"
> >
<!-- Post Link Target --> <!-- Post Link Target -->
<nuxt-link <nuxt-link
@ -16,7 +16,8 @@
<client-only> <client-only>
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" /> <hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
</client-only> </client-only>
<hc-ribbon :text="$t('post.name')" /> <hc-ribbon v-if="isPinned" class="ribbon--pinned" :text="$t('post.pinned')" />
<hc-ribbon v-else :text="$t('post.name')" />
</div> </div>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<!-- Post Title --> <!-- Post Title -->
@ -61,6 +62,8 @@
:resource="post" :resource="post"
:modalsData="menuModalsData" :modalsData="menuModalsData"
:is-owner="isAuthor" :is-owner="isAuthor"
@pinPost="pinPost"
@unpinPost="unpinPost"
/> />
</div> </div>
</client-only> </client-only>
@ -114,6 +117,9 @@ export default {
this.deletePostCallback, this.deletePostCallback,
) )
}, },
isPinned() {
return this.post && this.post.pinnedBy
},
}, },
methods: { methods: {
async deletePostCallback() { async deletePostCallback() {
@ -127,6 +133,12 @@ export default {
this.$toast.error(err.message) this.$toast.error(err.message)
} }
}, },
pinPost(post) {
this.$emit('pinPost', post)
},
unpinPost(post) {
this.$emit('unpinPost', post)
},
}, },
} }
</script> </script>
@ -167,4 +179,8 @@ export default {
text-indent: -999999px; text-indent: -999999px;
} }
} }
.post--pinned {
border: 1px solid $color-warning;
}
</style> </style>

View File

@ -46,4 +46,12 @@ export default {
border-color: $background-color-secondary transparent transparent $background-color-secondary; border-color: $background-color-secondary transparent transparent $background-color-secondary;
} }
} }
.ribbon--pinned {
background-color: $color-warning-active;
&::before {
border-color: $color-warning transparent transparent $color-warning;
}
}
</style> </style>

View File

@ -93,7 +93,7 @@ export default {
return data.notifications return data.notifications
}, },
error(error) { error(error) {
this.$toast.error(error) this.$toast.error(error.message)
}, },
}, },
}, },

View File

@ -57,6 +57,12 @@ export const postFragment = lang => gql`
name name
icon icon
} }
pinnedBy {
id
name
role
}
pinnedAt
} }
` `
export const commentFragment = lang => gql` export const commentFragment = lang => gql`

View File

@ -50,6 +50,11 @@ export default () => {
content content
contentExcerpt contentExcerpt
language language
pinnedBy {
id
name
role
}
} }
} }
`, `,
@ -86,5 +91,39 @@ export default () => {
} }
} }
`, `,
pinPost: gql`
mutation($id: ID!) {
pinPost(id: $id) {
id
title
slug
content
contentExcerpt
language
pinnedBy {
id
name
role
}
}
}
`,
unpinPost: gql`
mutation($id: ID!) {
unpinPost(id: $id) {
id
title
slug
content
contentExcerpt
language
pinnedBy {
id
name
role
}
}
}
`,
} }
} }

View File

@ -35,6 +35,26 @@ export const filterPosts = i18n => {
` `
} }
export const profilePagePosts = i18n => {
const lang = i18n.locale().toUpperCase()
return gql`
${postFragment(lang)}
${postCountsFragment}
query profilePagePosts(
$filter: _PostFilter
$first: Int
$offset: Int
$orderBy: [_PostOrdering]
) {
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
...post
...postCounts
}
}
`
}
export const PostsEmotionsByCurrentUser = () => { export const PostsEmotionsByCurrentUser = () => {
return gql` return gql`
query PostsEmotionsByCurrentUser($postId: ID!) { query PostsEmotionsByCurrentUser($postId: ID!) {

View File

@ -362,6 +362,7 @@
}, },
"post": { "post": {
"name": "Beitrag", "name": "Beitrag",
"pinned": "Meldung",
"moreInfo": { "moreInfo": {
"name": "Mehr Info", "name": "Mehr Info",
"title": "Mehr Informationen", "title": "Mehr Informationen",
@ -375,7 +376,11 @@
}, },
"menu": { "menu": {
"edit": "Beitrag bearbeiten", "edit": "Beitrag bearbeiten",
"delete": "Beitrag löschen" "delete": "Beitrag löschen",
"pin": "Post festpinnen",
"pinnedSuccessfully": "Post erfolgreich festgepinnt!",
"unpin": "Post nicht mehr festpinnen",
"unpinnedSuccessfully": "Post erfolgreich nicht mehr festgepinnt!"
}, },
"comment": { "comment": {
"submit": "Kommentiere", "submit": "Kommentiere",

View File

@ -363,6 +363,7 @@
}, },
"post": { "post": {
"name": "Post", "name": "Post",
"pinned": "Announcement",
"moreInfo": { "moreInfo": {
"name": "More info", "name": "More info",
"title": "More information", "title": "More information",
@ -376,7 +377,11 @@
}, },
"menu": { "menu": {
"edit": "Edit Post", "edit": "Edit Post",
"delete": "Delete Post" "delete": "Delete Post",
"pin": "Pin post",
"pinnedSuccessfully": "Post pinned successfully!",
"unpin": "Unpin post",
"unpinnedSuccessfully": "Post unpinned successfully!"
}, },
"comment": { "comment": {
"submit": "Comment", "submit": "Comment",

View File

@ -79,7 +79,7 @@
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"tippy.js": "^4.3.5", "tippy.js": "^4.3.5",
"tiptap": "~1.26.3", "tiptap": "~1.26.3",
"tiptap-extensions": "~1.28.3", "tiptap-extensions": "~1.28.4",
"trunc-html": "^1.1.2", "trunc-html": "^1.1.2",
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
@ -98,7 +98,7 @@
"@storybook/addon-a11y": "^5.2.4", "@storybook/addon-a11y": "^5.2.4",
"@storybook/addon-actions": "^5.2.4", "@storybook/addon-actions": "^5.2.4",
"@storybook/vue": "~5.2.4", "@storybook/vue": "~5.2.4",
"@vue/cli-shared-utils": "~3.12.0", "@vue/cli-shared-utils": "~4.0.4",
"@vue/eslint-config-prettier": "~5.0.0", "@vue/eslint-config-prettier": "~5.0.0",
"@vue/server-test-utils": "~1.0.0-beta.29", "@vue/server-test-utils": "~1.0.0-beta.29",
"@vue/test-utils": "~1.0.0-beta.29", "@vue/test-utils": "~1.0.0-beta.29",

View File

@ -20,6 +20,8 @@
:post="post" :post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }" :width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@removePostFromList="deletePost" @removePostFromList="deletePost"
@pinPost="pinPost"
@unpinPost="unpinPost"
/> />
</masonry-grid-item> </masonry-grid-item>
</template> </template>
@ -57,12 +59,13 @@
<script> <script>
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue' import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import HcEmpty from '~/components/Empty' import HcEmpty from '~/components/Empty'
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard/PostCard.vue'
import HcLoadMore from '~/components/LoadMore.vue' import HcLoadMore from '~/components/LoadMore.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue' import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js' import { filterPosts } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations'
export default { export default {
components: { components: {
@ -161,6 +164,37 @@ export default {
return post.id !== deletedPost.id return post.id !== deletedPost.id
}) })
}, },
resetPostList() {
this.offset = 0
this.posts = []
this.hasMore = true
},
pinPost(post) {
this.$apollo
.mutate({
mutation: PostMutations().pinPost,
variables: { id: post.id },
})
.then(() => {
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
this.resetPostList()
this.$apollo.queries.Post.refetch()
})
.catch(error => this.$toast.error(error.message))
},
unpinPost(post) {
this.$apollo
.mutate({
mutation: PostMutations().unpinPost,
variables: { id: post.id },
})
.then(() => {
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
this.resetPostList()
this.$apollo.queries.Post.refetch()
})
.catch(error => this.$toast.error(error.message))
},
}, },
apollo: { apollo: {
Post: { Post: {
@ -171,7 +205,7 @@ export default {
return { return {
filter: this.finalFilters, filter: this.finalFilters,
first: this.pageSize, first: this.pageSize,
orderBy: this.orderBy, orderBy: ['pinnedAt_asc', this.orderBy],
offset: 0, offset: 0,
} }
}, },

View File

@ -31,6 +31,9 @@ describe('PostSlug', () => {
$filters: { $filters: {
truncate: a => a, truncate: a => a,
}, },
$route: {
hash: '',
},
// If you are mocking the router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html // If you are mocking the router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$router: { $router: {
history: { history: {

View File

@ -18,6 +18,8 @@
:resource="post" :resource="post"
:modalsData="menuModalsData" :modalsData="menuModalsData"
:is-owner="isAuthor(post.author ? post.author.id : null)" :is-owner="isAuthor(post.author ? post.author.id : null)"
@pinPost="pinPost"
@unpinPost="unpinPost"
/> />
</client-only> </client-only>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
@ -68,9 +70,13 @@
</ds-space> </ds-space>
<!-- Comments --> <!-- Comments -->
<ds-section slot="footer"> <ds-section slot="footer">
<hc-comment-list :post="post" :routeHash="$route.hash" /> <hc-comment-list
:post="post"
:routeHash="$route.hash"
@toggleNewCommentForm="toggleNewCommentForm"
/>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
<hc-comment-form :post="post" @createComment="createComment" /> <hc-comment-form v-if="showNewCommentForm" :post="post" @createComment="createComment" />
</ds-section> </ds-section>
</ds-card> </ds-card>
</transition> </transition>
@ -88,6 +94,7 @@ import HcCommentList from '~/components/CommentList/CommentList'
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers' import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
import PostQuery from '~/graphql/PostQuery' import PostQuery from '~/graphql/PostQuery'
import HcEmotions from '~/components/Emotions/Emotions' import HcEmotions from '~/components/Emotions/Emotions'
import PostMutations from '~/graphql/PostMutations'
export default { export default {
name: 'PostSlug', name: 'PostSlug',
@ -116,6 +123,7 @@ export default {
post: null, post: null,
ready: false, ready: false,
title: 'loading', title: 'loading',
showNewCommentForm: true,
} }
}, },
watch: { watch: {
@ -156,6 +164,31 @@ export default {
async createComment(comment) { async createComment(comment) {
this.post.comments.push(comment) this.post.comments.push(comment)
}, },
pinPost(post) {
this.$apollo
.mutate({
mutation: PostMutations().pinPost,
variables: { id: post.id },
})
.then(() => {
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
})
.catch(error => this.$toast.error(error.message))
},
unpinPost(post) {
this.$apollo
.mutate({
mutation: PostMutations().unpinPost,
variables: { id: post.id },
})
.then(() => {
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
})
.catch(error => this.$toast.error(error.message))
},
toggleNewCommentForm(showNewCommentForm) {
this.showNewCommentForm = showNewCommentForm
},
}, },
apollo: { apollo: {
Post: { Post: {

View File

@ -37,7 +37,7 @@
<script> <script>
import HcEmpty from '~/components/Empty.vue' import HcEmpty from '~/components/Empty.vue'
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard/PostCard.vue'
import HcCategory from '~/components/Category' import HcCategory from '~/components/Category'
import HcHashtag from '~/components/Hashtag/Hashtag' import HcHashtag from '~/components/Hashtag/Hashtag'
import { relatedContributions } from '~/graphql/PostQuery' import { relatedContributions } from '~/graphql/PostQuery'

View File

@ -234,6 +234,8 @@
:post="post" :post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }" :width="{ base: '100%', md: '100%', xl: '50%' }"
@removePostFromList="removePostFromList" @removePostFromList="removePostFromList"
@pinPost="pinPost"
@unpinPost="unpinPost"
/> />
</masonry-grid-item> </masonry-grid-item>
</template> </template>
@ -268,7 +270,7 @@
<script> <script>
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import User from '~/components/User/User' import User from '~/components/User/User'
import HcPostCard from '~/components/PostCard' import HcPostCard from '~/components/PostCard/PostCard.vue'
import HcFollowButton from '~/components/FollowButton.vue' import HcFollowButton from '~/components/FollowButton.vue'
import HcCountTo from '~/components/CountTo.vue' import HcCountTo from '~/components/CountTo.vue'
import HcBadges from '~/components/Badges.vue' import HcBadges from '~/components/Badges.vue'
@ -279,9 +281,10 @@ import HcUpload from '~/components/Upload'
import HcAvatar from '~/components/Avatar/Avatar.vue' import HcAvatar from '~/components/Avatar/Avatar.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue' import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { filterPosts } from '~/graphql/PostQuery' import { profilePagePosts } from '~/graphql/PostQuery'
import UserQuery from '~/graphql/User' import UserQuery from '~/graphql/User'
import { Block, Unblock } from '~/graphql/settings/BlockedUsers' import { Block, Unblock } from '~/graphql/settings/BlockedUsers'
import PostMutations from '~/graphql/PostMutations'
const tabToFilterMapping = ({ tab, id }) => { const tabToFilterMapping = ({ tab, id }) => {
return { return {
@ -412,6 +415,32 @@ export default {
this.resetPostList() this.resetPostList()
this.$apollo.queries.Post.refetch() this.$apollo.queries.Post.refetch()
}, },
pinPost(post) {
this.$apollo
.mutate({
mutation: PostMutations().pinPost,
variables: { id: post.id },
})
.then(() => {
this.$toast.success(this.$t('post.menu.pinnedSuccessfully'))
this.resetPostList()
this.$apollo.queries.Post.refetch()
})
.catch(error => this.$toast.error(error.message))
},
unpinPost(post) {
this.$apollo
.mutate({
mutation: PostMutations().unpinPost,
variables: { id: post.id },
})
.then(() => {
this.$toast.success(this.$t('post.menu.unpinnedSuccessfully'))
this.resetPostList()
this.$apollo.queries.Post.refetch()
})
.catch(error => this.$toast.error(error.message))
},
optimisticFollow({ followedByCurrentUser }) { optimisticFollow({ followedByCurrentUser }) {
/* /*
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow * Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
@ -437,18 +466,18 @@ export default {
apollo: { apollo: {
Post: { Post: {
query() { query() {
return filterPosts(this.$i18n) return profilePagePosts(this.$i18n)
}, },
variables() { variables() {
return { return {
filter: this.filter, filter: this.filter,
first: this.pageSize, first: this.pageSize,
offset: 0, offset: 0,
orderBy: 'createdAt_desc', orderBy: ['pinnedAt_asc', 'createdAt_desc'],
} }
}, },
update({ Post }) { update({ profilePagePosts }) {
this.posts = Post this.posts = profilePagePosts
}, },
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
}, },

View File

@ -31,11 +31,7 @@ export default ({ app = {} }) => {
if (length <= 0) { if (length <= 0) {
return value return value
} }
let output = trunc(value, length).html return trunc(value, length).html
if (output.length < value.length) {
output += ' …'
}
return output
}, },
list: (value, glue = ', ', truncate = 0) => { list: (value, glue = ', ', truncate = 0) => {
if (!Array.isArray(value) || !value.length) { if (!Array.isArray(value) || !value.length) {

View File

@ -2672,10 +2672,10 @@
"@vue/babel-plugin-transform-vue-jsx" "^1.0.0" "@vue/babel-plugin-transform-vue-jsx" "^1.0.0"
camelcase "^5.0.0" camelcase "^5.0.0"
"@vue/cli-shared-utils@~3.12.0": "@vue/cli-shared-utils@~4.0.4":
version "3.12.0" version "4.0.4"
resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-3.12.0.tgz#48fcd786129cf02278b9c91f2c3491199f777248" resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.0.4.tgz#ef94325c3954eaac60ca4af9a6de355131ae6e3f"
integrity sha512-8XEn4s0Cc+98eqdGSQJSrzSKIsf0FMDmfDvgXjT7I2qZWs9e0toOAm7RooypRSad2FhwxzY2bLPgCkNPDJN/jQ== integrity sha512-f8a9MxZJ89zw783gk2yjG6wPu5IHnmrH8whc1jMJhWNKxRTgCkUxevPVQIobiWy1UtMBdDXXOLxd4PRNK9nyxQ==
dependencies: dependencies:
"@hapi/joi" "^15.0.1" "@hapi/joi" "^15.0.1"
chalk "^2.4.1" chalk "^2.4.1"
@ -2687,7 +2687,7 @@
ora "^3.4.0" ora "^3.4.0"
request "^2.87.0" request "^2.87.0"
request-promise-native "^1.0.7" request-promise-native "^1.0.7"
semver "^6.0.0" semver "^6.1.0"
string.prototype.padstart "^3.0.0" string.prototype.padstart "^3.0.0"
"@vue/component-compiler-utils@^3.0.0": "@vue/component-compiler-utils@^3.0.0":
@ -7030,22 +7030,7 @@ execa@^1.0.0:
signal-exit "^3.0.0" signal-exit "^3.0.0"
strip-eof "^1.0.0" strip-eof "^1.0.0"
execa@^2.0.4: execa@^2.0.4, execa@^2.1.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e"
integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ==
dependencies:
cross-spawn "^6.0.5"
get-stream "^5.0.0"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^3.0.0"
onetime "^5.1.0"
p-finally "^2.0.0"
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
execa@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99"
integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==
@ -12901,10 +12886,10 @@ prosemirror-commands@^1.0.8:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-dropcursor@^1.1.2: prosemirror-dropcursor@^1.2.0:
version "1.1.2" version "1.2.0"
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.2.tgz#d54428e0fdbc0fb3d4c5809acd1ad031e6cb6855" resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.2.0.tgz#00b1bdb803ac28f5a68d7e0a16a47c9c11aab97d"
integrity sha512-QHZbYPr8AY0g88TC/Wp7jpYbUoSpTSO8sqHNGvvZOInsAyylIdOpsrfhY1NC+/lh+iuwka0YogGtq2mmE7cr4g== integrity sha512-D7JrvOgN32PmOgfimdDMKCuYp4tGyCulpsd39/Nzvn9A+tCJmM8XY1PB07zkr2vjrjF09WYD3Ifer7Z3pk/YRw==
dependencies: dependencies:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.1.0" prosemirror-transform "^1.1.0"
@ -12937,7 +12922,7 @@ prosemirror-inputrules@^1.0.4:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1: prosemirror-keymap@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2"
integrity sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg== integrity sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg==
@ -12945,17 +12930,25 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
w3c-keyname "^1.1.8" w3c-keyname "^1.1.8"
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.3: prosemirror-keymap@^1.0.2:
version "1.7.3" version "1.0.2"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.3.tgz#d843b9338ebb1c22db85681452cf5724f785906e" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.2.tgz#e4e8b876a586ec6a170615fce19c4450f4075bef"
integrity sha512-Es71i2qXdkJNyIFyH7QoKDnKCTVC4LaQgiAaQV5Zd5XCKHg09m9NIJCEgePrF2yN/1tB/C5NYDY/4QsPvEM59A== integrity sha512-aq3fBT3WMbwGNacUtMbS/mxd87hjJyjtUx5/h3q/P3FiVqHxmeA9snxQsZHYe0cWRziZePun8zw6kHFKLp/DAQ==
dependencies:
prosemirror-state "^1.0.0"
w3c-keyname "^2.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.4:
version "1.7.4"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.7.4.tgz#fadefad98ef9d71ca1749d18a82005313c978e4b"
integrity sha512-yxdpPh9Uv5vAOZvmbhg4fsGUK1oHuQs69iX7cFZ0A4Y+AyMMWRCNKUt21uv84HbXb4I180l4pJE8ibaH/SwYiw==
dependencies: dependencies:
orderedmap "^1.0.0" orderedmap "^1.0.0"
prosemirror-schema-list@^1.0.3: prosemirror-schema-list@^1.0.4:
version "1.0.3" version "1.0.4"
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.3.tgz#539caafa4f9314000943bd783be4017165ec0bd6" resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.4.tgz#cbb11936e306aa45586af4279529ce61b52cfb6e"
integrity sha512-+zzSawVds8LsZpl/bLTCYk2lYactF93W219Czh81zBILikCRDOHjp1CQ1os4ZXBp6LlD+JnBqF1h59Q+hilOoQ== integrity sha512-7Y0b6FIG6ATnCcDSLrZfU9yIfOG5Yad3DMNZ9W7GGfMSzdIl0aHExrsIUgviJZjovO2jtLJVbxWGjMR3OrTupA==
dependencies: dependencies:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
@ -12991,10 +12984,10 @@ prosemirror-utils@^0.9.6:
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973" resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973"
integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA== integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.11.4: prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.11.7:
version "1.11.4" version "1.11.7"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.11.4.tgz#f80aec8924d59d4c3456dcc5bfea733758ec9b40" resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.11.7.tgz#537020acab43e18d51e18717fb1f39466317ce21"
integrity sha512-J0g7xiCDx+p3CtpC69E7HvMmnW7yCILEhOXxSANZPX8iIwUrVTfdWKAzufi9F9MoM08ewsaF254xV90NpkGWVQ== integrity sha512-RDll1mhrOQ5JPOsUZlISH3VP/4zyHm3RAuqh9Cg5HdpfSQow0nsx8xL5YQZ976UdhvwbkiKamLtxhSRKES9wsA==
dependencies: dependencies:
prosemirror-model "^1.1.0" prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
@ -15410,62 +15403,62 @@ tippy.js@^4.3.5:
dependencies: dependencies:
popper.js "^1.14.7" popper.js "^1.14.7"
tiptap-commands@^1.12.2: tiptap-commands@^1.12.3:
version "1.12.2" version "1.12.3"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.2.tgz#5d478604f03ab5bc5b05e3f94f8c75cec175c8c3" resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.3.tgz#604767878073e6344d1daf7a376fd89fc62e4742"
integrity sha512-wE19avtU0N/pNQlwDhfsJH1M3QT0Bc3oukfoB7K+K0H7xAXyfukbVPoZGjItWoyawtPk11A80OIBQCrO63fO4Q== integrity sha512-Dck51lePBwuHmkvkJ6+8V3DbInxAhZwtS2mPvVwz74pDUIcy17tCFw1eHUN50JoXIAci7acuxPKO/weVO1JAyw==
dependencies: dependencies:
prosemirror-commands "^1.0.8" prosemirror-commands "^1.0.8"
prosemirror-inputrules "^1.0.4" prosemirror-inputrules "^1.0.4"
prosemirror-model "^1.7.3" prosemirror-model "^1.7.4"
prosemirror-schema-list "^1.0.3" prosemirror-schema-list "^1.0.4"
prosemirror-state "^1.2.4" prosemirror-state "^1.2.4"
prosemirror-tables "^0.9.5" prosemirror-tables "^0.9.5"
prosemirror-utils "^0.9.6" prosemirror-utils "^0.9.6"
tiptap-utils "^1.8.1" tiptap-utils "^1.8.2"
tiptap-extensions@~1.28.3: tiptap-extensions@~1.28.4:
version "1.28.3" version "1.28.4"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.28.3.tgz#6278ed247c35c97cec41395671433408977cd9ef" resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.28.4.tgz#0e729d081a80105730101512e7eb5acdce8b9bde"
integrity sha512-iTUY5HQ+gYCHlyGMqcfKqO7JAvCqDEuvO4vEUrVxx0RgoYQfEf+Y17Kw8AJlVR6bB/Y/bEmMIuzVPjsgWNWdbw== integrity sha512-UAtxngKifjrMtJFmi3D9RCNC5LJutq4yn1Np0cqJ4dTnvhWR49PqN6gKjlMYyzyutiLLQk+/3GM/E6EfVwmHOA==
dependencies: dependencies:
lowlight "^1.12.1" lowlight "^1.12.1"
prosemirror-collab "^1.1.2" prosemirror-collab "^1.1.2"
prosemirror-history "^1.0.4" prosemirror-history "^1.0.4"
prosemirror-model "^1.7.3" prosemirror-model "^1.7.4"
prosemirror-state "^1.2.4" prosemirror-state "^1.2.4"
prosemirror-tables "^0.9.5" prosemirror-tables "^0.9.5"
prosemirror-transform "^1.1.5" prosemirror-transform "^1.1.5"
prosemirror-utils "^0.9.6" prosemirror-utils "^0.9.6"
prosemirror-view "^1.11.4" prosemirror-view "^1.11.7"
tiptap "^1.26.3" tiptap "^1.26.4"
tiptap-commands "^1.12.2" tiptap-commands "^1.12.3"
tiptap-utils@^1.8.1: tiptap-utils@^1.8.2:
version "1.8.1" version "1.8.2"
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.1.tgz#52eb90524f1ec95e66ddc84a20d892aaac879630" resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.2.tgz#f07a2053c6ac9fbbb4f02e0844b326d0e6c8b7fb"
integrity sha512-FcceXo+yVZni54aB/R3nTpdtcHmFM6QwW6PZg1aHH2u2fhkeV/MB7sXBkx3wIrvOtw8WPT2Kjpou2to27CCtbA== integrity sha512-pyx+3p4fICGM7JU1mcsnRx5jXvLrCL8Nm/9yjeWEZXpAC85L/btY0eFo2Oz4+dKg39+1EGNHheodujx3ngw4lQ==
dependencies: dependencies:
prosemirror-model "^1.7.3" prosemirror-model "^1.7.4"
prosemirror-state "^1.2.4" prosemirror-state "^1.2.4"
prosemirror-tables "^0.9.5" prosemirror-tables "^0.9.5"
prosemirror-utils "^0.9.6" prosemirror-utils "^0.9.6"
tiptap@^1.26.3, tiptap@~1.26.3: tiptap@^1.26.4, tiptap@~1.26.3:
version "1.26.3" version "1.26.4"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.3.tgz#a08e1db4f1dce17a14309532e65a3949b0ceed91" resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.26.4.tgz#bfa289841bc45c6401cbd1661a02b81c3d3f14f0"
integrity sha512-EcTEM8GLuMa1jNxGg5cWR7NqyiFwtRat6im8A5EvL6iiLiOhIaqgkQnZJ5qUxWNgQTfjgCO5IWA85yoRSJWNMQ== integrity sha512-UCH0wufjGdKMuCUydL896sFYXEUWC3bE20h/oONABSf0gull+pqBEm7J1yCl7j50eYa9FiLgUBGPqPTzKLluxQ==
dependencies: dependencies:
prosemirror-commands "^1.0.8" prosemirror-commands "^1.0.8"
prosemirror-dropcursor "^1.1.2" prosemirror-dropcursor "^1.2.0"
prosemirror-gapcursor "^1.0.4" prosemirror-gapcursor "^1.0.4"
prosemirror-inputrules "^1.0.4" prosemirror-inputrules "^1.0.4"
prosemirror-keymap "^1.0.1" prosemirror-keymap "^1.0.2"
prosemirror-model "^1.7.3" prosemirror-model "^1.7.4"
prosemirror-state "^1.2.4" prosemirror-state "^1.2.4"
prosemirror-view "^1.11.4" prosemirror-view "^1.11.7"
tiptap-commands "^1.12.2" tiptap-commands "^1.12.3"
tiptap-utils "^1.8.1" tiptap-utils "^1.8.2"
title-case@^2.1.0: title-case@^2.1.0:
version "2.1.1" version "2.1.1"
@ -16347,6 +16340,11 @@ w3c-keyname@^1.1.8:
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c"
integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA== integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA==
w3c-keyname@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.1.1.tgz#d234195b0a79913d06a0bc636f17f00050a5de56"
integrity sha512-8kSrsGClLiL4kb5/pTxglejUlEAPk3GXtkBblSMrQDxKz0NkMRTVTPBZm6QCNqPOCPsdNvae5XfV+RJZgeGXEA==
walker@^1.0.7, walker@~1.0.5: walker@^1.0.7, walker@~1.0.5:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"

View File

@ -1895,10 +1895,10 @@ cypress-cucumber-preprocessor@^1.16.2:
js-string-escape "^1.0.1" js-string-escape "^1.0.1"
through "^2.3.8" through "^2.3.8"
cypress-file-upload@^3.3.4: cypress-file-upload@^3.4.0:
version "3.3.4" version "3.4.0"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.3.4.tgz#cbeb8a7a07150a1c60f2873666979e48b6335070" resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.4.0.tgz#f066853357994ed7b64e0ea35920d3d85273914e"
integrity sha512-kfdrQ6cWBw82G7EbHSqZJiOQWRh9cGz9K1mjePNZax00gBL0qOdRTjfkAnR2vEmmJyCfnN3efryjfhFeLrGWVw== integrity sha512-BY7jrpOPFEGcGBzkTReEjwQ59+O3u2SH2OleXdnDCuWIPHjbDx7haXukyAFd906JsI4Z2zXPiKrUVFHZc96eFA==
cypress-plugin-retries@^1.3.0: cypress-plugin-retries@^1.3.0:
version "1.3.0" version "1.3.0"
@ -2359,7 +2359,8 @@ extsprintf@^1.2.0:
faker@Marak/faker.js#master: faker@Marak/faker.js#master:
version "4.1.0" version "4.1.0"
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/10bfb9f467b0ac2b8912ffc15690b50ef3244f09" uid "9fd8d7d37b398842d0784a116a340f7aa6afb89b"
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/9fd8d7d37b398842d0784a116a340f7aa6afb89b"
fast-deep-equal@^2.0.1: fast-deep-equal@^2.0.1:
version "2.0.1" version "2.0.1"