Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into add-missing-portuguese-translations

This commit is contained in:
ppelegrin 2019-10-30 22:35:03 +01:00
commit 405021dea6
92 changed files with 18749 additions and 2157 deletions

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,7 @@
* [Maintenance](deployment/human-connection/maintenance/README.md)
* [Volumes](deployment/volumes/README.md)
* [Neo4J Offline-Backups](deployment/volumes/neo4j-offline-backup/README.md)
* [Neo4J Online-Backups](deployment/volumes/neo4j-online-backup/README.md)
* [Volume Snapshots](deployment/volumes/volume-snapshots/README.md)
* [Reclaim Policy](deployment/volumes/reclaim-policy/README.md)
* [Velero](deployment/volumes/velero/README.md)

View File

@ -1 +1 @@
0.1.3
0.1.8

13293
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -42,21 +42,21 @@
},
"dependencies": {
"@hapi/joi": "^16.1.7",
"@sentry/node": "^5.7.0",
"@sentry/node": "^5.7.1",
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.6",
"apollo-server-express": "^2.9.6",
"apollo-server": "~2.9.7",
"apollo-server-express": "^2.9.7",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~6.0.3",
"date-fns": "2.4.1",
"date-fns": "2.6.0",
"debug": "~4.1.1",
"dotenv": "~8.1.0",
"dotenv": "~8.2.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.5.8",
@ -64,33 +64,33 @@
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~4.0.1",
"graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~6.1.0",
"graphql-shield": "~7.0.0",
"graphql-tag": "~2.10.1",
"helmet": "~3.21.1",
"helmet": "~3.21.2",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.0",
"metascraper": "^4.10.3",
"metascraper-audio": "^5.7.6",
"metascraper-author": "^5.7.6",
"metascraper-audio": "^5.7.14",
"metascraper-author": "^5.7.17",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.7.6",
"metascraper-description": "^5.7.6",
"metascraper-date": "^5.7.14",
"metascraper-description": "^5.7.14",
"metascraper-image": "^5.7.6",
"metascraper-lang": "^5.7.6",
"metascraper-lang": "^5.7.14",
"metascraper-lang-detector": "^4.8.5",
"metascraper-logo": "^5.7.6",
"metascraper-publisher": "^5.7.6",
"metascraper-soundcloud": "^5.7.6",
"metascraper-title": "^5.7.6",
"metascraper-url": "^5.7.6",
"metascraper-video": "^5.7.6",
"metascraper-youtube": "^5.7.6",
"metascraper-logo": "^5.7.17",
"metascraper-publisher": "^5.7.14",
"metascraper-soundcloud": "^5.7.14",
"metascraper-title": "^5.7.14",
"metascraper-url": "^5.7.14",
"metascraper-video": "^5.7.14",
"metascraper-youtube": "^5.7.17",
"minimatch": "^3.0.4",
"mustache": "^3.1.0",
"neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.7.2",
"neo4j-graphql-js": "^2.8.0",
"neode": "^0.3.3",
"node-fetch": "~2.6.0",
"nodemailer": "^6.3.1",
@ -111,24 +111,24 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.6.3",
"@babel/register": "~7.6.2",
"apollo-server-testing": "~2.9.6",
"apollo-server-testing": "~2.9.7",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~6.0.2",
"eslint": "~6.5.1",
"eslint-config-prettier": "~6.4.0",
"cucumber": "~6.0.3",
"eslint": "~6.6.0",
"eslint-config-prettier": "~6.5.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.19.0",
"eslint-plugin-jest": "~23.0.2",
"eslint-plugin-node": "~10.0.0",
"eslint-plugin-prettier": "~3.1.1",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
"graphql-request": "~1.8.2",
"jest": "~24.9.0",
"nodemon": "~1.19.3",
"nodemon": "~1.19.4",
"prettier": "~1.18.2",
"supertest": "~4.0.2"
}

View File

@ -24,7 +24,7 @@
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo!</h1>
<p style="margin: 0;">Du hast bei uns ein neues Password angefordert leider haben wir aber keinen
<p style="margin: 0;">Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen
Account mit Deiner E-Mailadresse gefunden. Kann es sein, dass Du mit einer anderen Adresse bei uns
angemeldet bist?</p>
</td>

View File

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

View File

@ -57,11 +57,37 @@ const validateUpdatePost = async (resolve, root, args, context, info) => {
return validatePost(resolve, root, args, context, info)
}
const validateReport = async (resolve, root, args, context, info) => {
const { resourceId } = args
const { user, driver } = context
if (resourceId === user.id) throw new Error('You cannot report yourself!')
const session = driver.session()
const reportQueryRes = await session.run(
`
MATCH (:User {id:$submitterId})-[:REPORTED]->(resource {id:$resourceId})
RETURN labels(resource)[0] as label
`,
{
resourceId,
submitterId: user.id,
},
)
const [existingReportedResource] = reportQueryRes.records.map(record => {
return {
label: record.get('label'),
}
})
if (existingReportedResource) throw new Error(`${existingReportedResource.label}`)
return resolve(root, args, context, info)
}
export default {
Mutation: {
CreateComment: validateCommentCreation,
UpdateComment: validateUpdateComment,
CreatePost: validatePost,
UpdatePost: validateUpdatePost,
report: validateReport,
},
}

View File

@ -85,7 +85,7 @@ function clean(dirty) {
return dirty
}
const fields = ['content', 'contentExcerpt']
const fields = ['content', 'contentExcerpt', 'reasonDescription']
export default {
Mutation: async (resolve, root, args, context, info) => {

View File

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

View File

@ -23,6 +23,7 @@ export default applyScalars(
'Location',
'SocialMedia',
'NOTIFIED',
'REPORTED',
],
// add 'User' here as soon as possible
},
@ -35,7 +36,6 @@ export default applyScalars(
'Notfication',
'Post',
'Comment',
'Report',
'Statistics',
'LoggedInUser',
'Location',
@ -43,6 +43,7 @@ export default applyScalars(
'User',
'EMOTED',
'NOTIFIED',
'REPORTED',
],
// add 'User' here as soon as possible
},

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 () => {
variables.id = user1.id
await expect(mutate({ mutation: mutationFollowUser, variables })).resolves.toMatchObject({
@ -155,6 +170,7 @@ describe('follow', () => {
})
})
})
describe('unfollow user', () => {
beforeEach(async () => {
variables = { id: user2.id }

View File

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

View File

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

View File

@ -39,7 +39,8 @@ const createPostMutation = gql`
}
`
beforeAll(() => {
beforeAll(async () => {
await factory.cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -269,7 +270,10 @@ describe('CreatePost', () => {
})
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(
expected,
)
@ -285,6 +289,7 @@ describe('CreatePost', () => {
},
},
},
errors: undefined,
}
await expect(mutate({ mutation: createPostMutation, variables })).resolves.toMatchObject(
expected,
@ -366,7 +371,12 @@ describe('UpdatePost', () => {
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
title
content
author {
name
slug
}
categories {
id
}
@ -386,7 +396,6 @@ describe('UpdatePost', () => {
})
variables = {
...variables,
id: 'p9876',
title: 'New title',
content: 'New content',
@ -395,8 +404,11 @@ describe('UpdatePost', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: updatePostMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorised!')
authenticatedUser = null
expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { UpdatePost: null },
})
})
})
@ -550,6 +562,399 @@ 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
pinned
}
}
`
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('ordinary users', () => {
it('throws authorization error', async () => {
await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { pinPost: null },
})
})
})
describe('moderators', () => {
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('admins', () => {
let admin
beforeEach(async () => {
admin = await user.update({
role: 'admin',
name: 'Admin',
updatedAt: new Date().toISOString(),
})
authenticatedUser = await admin.toJson()
})
describe('are allowed to pin posts', () => {
beforeEach(async () => {
await factory.create('Post', {
id: 'created-and-pinned-by-same-admin',
author: admin,
})
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
})
it('responds with the updated Post', async () => {
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 () => {
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,
)
})
it('sets redundant `pinned` property for performant ordering', async () => {
variables = { ...variables, id: 'created-and-pinned-by-same-admin' }
const expected = {
data: { pinPost: { pinned: true } },
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('pinned post already exists', () => {
let pinnedPost
beforeEach(async () => {
await factory.create('Post', {
id: 'only-pinned-post',
author: admin,
})
await mutate({ mutation: pinPostMutation, variables })
})
it('removes previous `pinned` attribute', async () => {
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
pinnedPost = await neode.cypher(cypher)
expect(pinnedPost.records).toHaveLength(1)
variables = { ...variables, id: 'only-pinned-post' }
await mutate({ mutation: pinPostMutation, variables })
pinnedPost = await neode.cypher(cypher)
expect(pinnedPost.records).toHaveLength(1)
})
it('removes previous PINNED relationship', async () => {
variables = { ...variables, id: 'only-pinned-post' }
await mutate({ mutation: pinPostMutation, variables })
pinnedPost = await neode.cypher(
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
)
expect(pinnedPost.records).toHaveLength(1)
})
})
describe('PostOrdering', () => {
let pinnedPost, admin
beforeEach(async () => {
;[pinnedPost] = await Promise.all([
neode.create('Post', {
id: 'im-a-pinned-post',
pinned: true,
}),
neode.create('Post', {
id: 'i-was-created-after-pinned-post',
createdAt: '2019-10-22T17:26:29.070Z', // this should always be 3rd
}),
])
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: ['pinned_desc', '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
pinned
pinnedAt
}
}
`
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() })
variables = { ...variables, id: 'post-to-be-unpinned' }
})
it('responds with the unpinned Post', async () => {
authenticatedUser = await admin.toJson()
const expected = {
data: {
unpinPost: {
id: 'post-to-be-unpinned',
pinnedBy: null,
pinnedAt: null,
},
},
errors: undefined,
}
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
it('unsets `pinned` property', async () => {
const expected = {
data: {
unpinPost: {
id: 'post-to-be-unpinned',
pinned: null,
},
},
errors: undefined,
}
await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject(
expected,
)
})
})
})
})
describe('DeletePost', () => {

View File

@ -381,6 +381,7 @@ describe('SignupVerification', () => {
$nonce: String!
$about: String
$termsAndConditionsAgreedVersion: String!
$locale: String
) {
SignupVerification(
name: $name
@ -389,6 +390,7 @@ describe('SignupVerification', () => {
nonce: $nonce
about: $about
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
locale: $locale
) {
id
termsAndConditionsAgreedVersion
@ -405,6 +407,7 @@ describe('SignupVerification', () => {
password: '123',
email: 'john@example.org',
termsAndConditionsAgreedVersion: '0.1.0',
locale: 'en',
}
})

View File

@ -1,83 +1,125 @@
import uuid from 'uuid/v4'
export default {
Mutation: {
report: async (parent, { id, description }, { driver, req, user }, resolveInfo) => {
const reportId = uuid()
report: async (_parent, params, context, _resolveInfo) => {
let createdRelationshipWithNestedAttributes
const { resourceId, reasonCategory, reasonDescription } = params
const { driver, user } = context
const session = driver.session()
const reportData = {
id: reportId,
createdAt: new Date().toISOString(),
description: description,
}
const reportQueryRes = await session.run(
`
match (u:User {id:$submitterId}) -[:REPORTED]->(report)-[:REPORTED]-> (resource {id: $resourceId})
return labels(resource)[0] as label
`,
{
resourceId: id,
submitterId: user.id,
},
)
const [rep] = reportQueryRes.records.map(record => {
return {
label: record.get('label'),
}
const writeTxResultPromise = session.writeTransaction(async txc => {
const reportRelationshipTransactionResponse = await txc.run(
`
MATCH (submitter:User {id: $submitterId})
MATCH (resource {id: $resourceId})
WHERE resource:User OR resource:Comment OR resource:Post
CREATE (resource)<-[report:REPORTED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
RETURN report, submitter, resource, labels(resource)[0] as type
`,
{
resourceId,
submitterId: user.id,
createdAt: new Date().toISOString(),
reasonCategory,
reasonDescription,
},
)
return reportRelationshipTransactionResponse.records.map(record => ({
report: record.get('report'),
submitter: record.get('submitter'),
resource: record.get('resource').properties,
type: record.get('type'),
}))
})
if (rep) {
throw new Error(rep.label)
}
const res = await session.run(
`
MATCH (submitter:User {id: $userId})
MATCH (resource {id: $resourceId})
WHERE resource:User OR resource:Comment OR resource:Post
MERGE (report:Report {id: {reportData}.id })
MERGE (resource)<-[:REPORTED]-(report)
MERGE (report)<-[:REPORTED]-(submitter)
RETURN report, submitter, resource, labels(resource)[0] as type
`,
{
resourceId: id,
userId: user.id,
reportData,
},
)
session.close()
const [dbResponse] = res.records.map(r => {
return {
report: r.get('report'),
submitter: r.get('submitter'),
resource: r.get('resource'),
type: r.get('type'),
try {
const txResult = await writeTxResultPromise
if (!txResult[0]) return null
const { report, submitter, resource, type } = txResult[0]
createdRelationshipWithNestedAttributes = {
...report.properties,
post: null,
comment: null,
user: null,
submitter: submitter.properties,
type,
}
})
if (!dbResponse) return null
const { report, submitter, resource, type } = dbResponse
const response = {
...report.properties,
post: null,
comment: null,
user: null,
submitter: submitter.properties,
type,
switch (type) {
case 'Post':
createdRelationshipWithNestedAttributes.post = resource
break
case 'Comment':
createdRelationshipWithNestedAttributes.comment = resource
break
case 'User':
createdRelationshipWithNestedAttributes.user = resource
break
}
} finally {
session.close()
}
switch (type) {
case 'Post':
response.post = resource.properties
return createdRelationshipWithNestedAttributes
},
},
Query: {
reports: async (_parent, params, context, _resolveInfo) => {
const { driver } = context
const session = driver.session()
let response
let orderByClause
switch (params.orderBy) {
case 'createdAt_asc':
orderByClause = 'ORDER BY report.createdAt ASC'
break
case 'Comment':
response.comment = resource.properties
break
case 'User':
response.user = resource.properties
case 'createdAt_desc':
orderByClause = 'ORDER BY report.createdAt DESC'
break
default:
orderByClause = ''
}
try {
const cypher = `
MATCH (submitter:User)-[report:REPORTED]->(resource)
WHERE resource:User OR resource:Comment OR resource:Post
RETURN report, submitter, resource, labels(resource)[0] as type
${orderByClause}
`
const result = await session.run(cypher, {})
const dbResponse = result.records.map(r => {
return {
report: r.get('report'),
submitter: r.get('submitter'),
resource: r.get('resource'),
type: r.get('type'),
}
})
if (!dbResponse) return null
response = []
dbResponse.forEach(ele => {
const { report, submitter, resource, type } = ele
const responseEle = {
...report.properties,
post: null,
comment: null,
user: null,
submitter: submitter.properties,
type,
}
switch (type) {
case 'Post':
responseEle.post = resource.properties
break
case 'Comment':
responseEle.comment = resource.properties
break
case 'User':
responseEle.user = resource.properties
break
}
response.push(responseEle)
})
} finally {
session.close()
}
return response

View File

@ -1,127 +1,258 @@
import { GraphQLClient } from 'graphql-request'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j'
import { gql } from '../../jest/helpers'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
const factory = Factory()
const instance = neode()
const instance = getNeode()
const driver = getDriver()
describe('report', () => {
let mutation
let headers
let returnedObject
let variables
let createPostVariables
let user
describe('report resources', () => {
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser
const categoryIds = ['cat9']
beforeEach(async () => {
returnedObject = '{ description }'
variables = {
id: 'whatever',
const reportMutation = gql`
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report(
resourceId: $resourceId
reasonCategory: $reasonCategory
reasonDescription: $reasonDescription
) {
createdAt
reasonCategory
reasonDescription
type
submitter {
email
}
user {
name
}
post {
title
}
comment {
content
}
}
}
headers = {}
user = await factory.create('User', {
email: 'test@example.org',
password: '1234',
id: 'u1',
})
await factory.create('User', {
id: 'u2',
name: 'abusive-user',
role: 'user',
email: 'abusive-user@example.org',
})
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
`
const variables = {
resourceId: 'whatever',
reasonCategory: 'other',
reasonDescription: 'Violates code of conduct !!!',
}
beforeAll(async () => {
await factory.cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode: instance,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
query = createTestClient(server).query
})
afterEach(async () => {
await factory.cleanDatabase()
})
let client
const action = () => {
mutation = `
mutation($id: ID!) {
report(
id: $id,
description: "Violates code of conduct"
) ${returnedObject}
}
`
client = new GraphQLClient(host, {
headers,
})
return client.request(mutation, variables)
}
describe('unauthenticated', () => {
it('throws authorization error', async () => {
await expect(action()).rejects.toThrow('Not Authorised')
describe('report a resource', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
authenticatedUser = null
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
data: { report: null },
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({
currentUser = await factory.create('User', {
id: 'current-user-id',
role: 'user',
email: 'test@example.org',
password: '1234',
})
await factory.create('User', {
id: 'abusive-user-id',
role: 'user',
name: 'abusive-user',
email: 'abusive-user@example.org',
})
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
authenticatedUser = await currentUser.toJson()
})
describe('invalid resource id', () => {
it('returns null', async () => {
await expect(action()).resolves.toEqual({
report: null,
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
data: { report: null },
errors: undefined,
})
})
})
describe('valid resource id', () => {
beforeEach(async () => {
variables = {
id: 'u2',
}
})
/*
it('creates a report', async () => {
await expect(action()).resolves.toEqual({
type: null,
})
})
*/
it('returns the submitter', async () => {
returnedObject = '{ submitter { email } }'
await expect(action()).resolves.toEqual({
report: {
submitter: {
email: 'test@example.org',
},
},
})
})
describe('valid resource', () => {
describe('reported resource is a user', () => {
it('returns type "User"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
report: {
type: 'User',
await expect(
mutate({
mutation: reportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
report: {
type: 'User',
},
},
errors: undefined,
})
})
it('returns resource in user attribute', async () => {
returnedObject = '{ user { name } }'
await expect(action()).resolves.toEqual({
report: {
user: {
name: 'abusive-user',
await expect(
mutate({
mutation: reportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
report: {
user: {
name: 'abusive-user',
},
},
},
errors: undefined,
})
})
it('returns the submitter', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
report: {
submitter: {
email: 'test@example.org',
},
},
},
errors: undefined,
})
})
it('returns a date', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: { ...variables, resourceId: 'abusive-user-id' },
}),
).resolves.toMatchObject({
data: {
report: {
createdAt: expect.any(String),
},
},
errors: undefined,
})
})
it('returns the reason category', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
reasonCategory: 'criminal_behavior_violation_german_law',
},
}),
).resolves.toMatchObject({
data: {
report: {
reasonCategory: 'criminal_behavior_violation_german_law',
},
},
errors: undefined,
})
})
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
reasonCategory: 'category_missing_from_enum_reason_category',
},
}),
).resolves.toMatchObject({
data: undefined,
errors: [
{
message:
'Variable "$reasonCategory" got invalid value "category_missing_from_enum_reason_category"; Expected type ReasonCategory.',
},
],
})
})
it('returns the reason description', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
reasonDescription: 'My reason!',
},
}),
).resolves.toMatchObject({
data: {
report: {
reasonDescription: 'My reason!',
},
},
errors: undefined,
})
})
it('sanitize the reason description', async () => {
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'abusive-user-id',
reasonDescription: 'My reason <sanitize></sanitize>!',
},
}),
).resolves.toMatchObject({
data: {
report: {
reasonDescription: 'My reason !',
},
},
errors: undefined,
})
})
})
@ -129,50 +260,75 @@ describe('report', () => {
describe('reported resource is a post', () => {
beforeEach(async () => {
await factory.create('Post', {
author: user,
id: 'p23',
title: 'Matt and Robert having a pair-programming',
author: currentUser,
id: 'post-to-report-id',
title: 'This is a post that is going to be reported',
categoryIds,
})
variables = {
id: 'p23',
}
})
it('returns type "Post"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
report: {
type: 'Post',
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'post-to-report-id',
},
}),
).resolves.toMatchObject({
data: {
report: {
type: 'Post',
},
},
errors: undefined,
})
})
it('returns resource in post attribute', async () => {
returnedObject = '{ post { title } }'
await expect(action()).resolves.toEqual({
report: {
post: {
title: 'Matt and Robert having a pair-programming',
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'post-to-report-id',
},
}),
).resolves.toMatchObject({
data: {
report: {
post: {
title: 'This is a post that is going to be reported',
},
},
},
errors: undefined,
})
})
it('returns null in user attribute', async () => {
returnedObject = '{ user { name } }'
await expect(action()).resolves.toEqual({
report: {
user: null,
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'post-to-report-id',
},
}),
).resolves.toMatchObject({
data: {
report: {
user: null,
},
},
errors: undefined,
})
})
})
/* An der Stelle würde ich den p23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
At this point I would check the p23 again, but this time there must be an error message. */
describe('reported resource is a comment', () => {
let createPostVariables
beforeEach(async () => {
createPostVariables = {
id: 'p1',
@ -180,61 +336,257 @@ describe('report', () => {
content: 'please comment on me',
categoryIds,
}
await factory.create('Post', { ...createPostVariables, author: user })
await factory.create('Post', { ...createPostVariables, author: currentUser })
await factory.create('Comment', {
author: user,
author: currentUser,
postId: 'p1',
id: 'c34',
content: 'Robert getting tired.',
id: 'comment-to-report-id',
content: 'Post comment to be reported.',
})
variables = {
id: 'c34',
}
})
it('returns type "Comment"', async () => {
returnedObject = '{ type }'
await expect(action()).resolves.toEqual({
report: {
type: 'Comment',
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'comment-to-report-id',
},
}),
).resolves.toMatchObject({
data: {
report: {
type: 'Comment',
},
},
errors: undefined,
})
})
it('returns resource in comment attribute', async () => {
returnedObject = '{ comment { content } }'
await expect(action()).resolves.toEqual({
report: {
comment: {
content: 'Robert getting tired.',
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'comment-to-report-id',
},
}),
).resolves.toMatchObject({
data: {
report: {
comment: {
content: 'Post comment to be reported.',
},
},
},
errors: undefined,
})
})
})
/* An der Stelle würde ich den c34 noch mal prüfen, diesmal muss aber eine error meldung kommen.
At this point I would check the c34 again, but this time there must be an error message. */
describe('reported resource is a tag', () => {
beforeEach(async () => {
await factory.create('Tag', {
id: 't23',
id: 'tag-to-report-id',
})
variables = {
id: 't23',
}
})
it('returns null', async () => {
await expect(action()).resolves.toEqual({
report: null,
await expect(
mutate({
mutation: reportMutation,
variables: {
...variables,
resourceId: 'tag-to-report-id',
},
}),
).resolves.toMatchObject({
data: { report: null },
errors: undefined,
})
})
})
})
})
})
describe('query for reported resource', () => {
const reportsQuery = gql`
query {
reports(orderBy: createdAt_desc) {
createdAt
reasonCategory
reasonDescription
submitter {
id
}
type
user {
id
}
post {
id
}
comment {
id
}
}
}
`
/* An der Stelle würde ich den t23 noch mal prüfen, diesmal muss aber eine error meldung kommen.
At this point I would check the t23 again, but this time there must be an error message. */
beforeEach(async () => {
authenticatedUser = null
moderator = await factory.create('User', {
id: 'moderator-1',
role: 'moderator',
email: 'moderator@example.org',
password: '1234',
})
currentUser = await factory.create('User', {
id: 'current-user-id',
role: 'user',
email: 'current.user@example.org',
password: '1234',
})
abusiveUser = await factory.create('User', {
id: 'abusive-user-1',
role: 'user',
name: 'abusive-user',
email: 'abusive-user@example.org',
})
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
await Promise.all([
factory.create('Post', {
author: abusiveUser,
id: 'abusive-post-1',
categoryIds,
content: 'Interesting Knowledge',
}),
factory.create('Post', {
author: moderator,
id: 'post-2',
categoryIds,
content: 'More things to do …',
}),
factory.create('Post', {
author: currentUser,
id: 'post-3',
categoryIds,
content: 'I am at school …',
}),
])
await Promise.all([
factory.create('Comment', {
author: currentUser,
id: 'abusive-comment-1',
postId: 'post-1',
}),
])
authenticatedUser = await currentUser.toJson()
await Promise.all([
mutate({
mutation: reportMutation,
variables: {
resourceId: 'abusive-post-1',
reasonCategory: 'other',
reasonDescription: 'This comment is bigoted',
},
}),
mutate({
mutation: reportMutation,
variables: {
resourceId: 'abusive-comment-1',
reasonCategory: 'discrimination_etc',
reasonDescription: 'This post is bigoted',
},
}),
mutate({
mutation: reportMutation,
variables: {
resourceId: 'abusive-user-1',
reasonCategory: 'doxing',
reasonDescription: 'This user is harassing me with bigoted remarks',
},
}),
])
authenticatedUser = null
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
authenticatedUser = null
expect(query({ query: reportsQuery })).resolves.toMatchObject({
data: { reports: null },
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
it('role "user" gets no reports', async () => {
authenticatedUser = await currentUser.toJson()
expect(query({ query: reportsQuery })).resolves.toMatchObject({
data: { reports: null },
errors: [{ message: 'Not Authorised!' }],
})
})
it('role "moderator" gets reports', async () => {
const expected = {
// to check 'orderBy: createdAt_desc' is not possible here, because 'createdAt' does not differ
reports: expect.arrayContaining([
expect.objectContaining({
createdAt: expect.any(String),
reasonCategory: 'doxing',
reasonDescription: 'This user is harassing me with bigoted remarks',
submitter: expect.objectContaining({
id: 'current-user-id',
}),
type: 'User',
user: expect.objectContaining({
id: 'abusive-user-1',
}),
post: null,
comment: null,
}),
expect.objectContaining({
createdAt: expect.any(String),
reasonCategory: 'other',
reasonDescription: 'This comment is bigoted',
submitter: expect.objectContaining({
id: 'current-user-id',
}),
type: 'Post',
user: null,
post: expect.objectContaining({
id: 'abusive-post-1',
}),
comment: null,
}),
expect.objectContaining({
createdAt: expect.any(String),
reasonCategory: 'discrimination_etc',
reasonDescription: 'This post is bigoted',
submitter: expect.objectContaining({
id: 'current-user-id',
}),
type: 'Comment',
user: null,
post: null,
comment: expect.objectContaining({
id: 'abusive-comment-1',
}),
}),
]),
}
authenticatedUser = await moderator.toJson()
const { data } = await query({ query: reportsQuery })
expect(data).toEqual(expected)
})
})
})

View File

@ -7,7 +7,7 @@ export default {
const transactionRes = await session.run(
`MATCH (node {id: $id})<-[:WROTE]-(userWritten:User), (user:User {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`,
{
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 () => {
variables = { id: 'current-user-post-id' }
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({

View File

@ -24,7 +24,6 @@ type Mutation {
changePassword(oldPassword: String!, newPassword: String!): String!
requestPasswordReset(email: String!): Boolean!
resetPassword(email: String!, nonce: String!, newPassword: String!): Boolean!
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID
# Shout the given Type and ID
@ -35,18 +34,6 @@ type Mutation {
unfollowUser(id: ID!): User
}
type Report {
id: ID!
submitter: User @relation(name: "REPORTED", direction: "IN")
description: String
type: String!
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
createdAt: String
comment: Comment @relation(name: "REPORTED", direction: "OUT")
post: Post @relation(name: "REPORTED", direction: "OUT")
user: User @relation(name: "REPORTED", direction: "OUT")
}
enum Deletable {
Post
Comment

View File

@ -19,6 +19,7 @@ type Mutation {
locationName: String
about: String
termsAndConditionsAgreedVersion: String!
locale: String
): User
AddEmailAddress(email: String!): EmailAddress
VerifyEmailAddress(

View File

@ -1,3 +1,37 @@
enum _PostOrdering {
id_asc
id_desc
activityId_asc
activityId_desc
objectId_asc
objectId_desc
title_asc
title_desc
slug_asc
slug_desc
content_asc
content_desc
contentExcerpt_asc
contentExcerpt_desc
image_asc
image_desc
visibility_asc
visibility_desc
deleted_asc
deleted_desc
disabled_asc
disabled_desc
createdAt_asc
createdAt_desc
updatedAt_asc
updatedAt_desc
language_asc
language_desc
pinned_asc
pinned_desc
}
type Post {
id: ID!
activityId: String
@ -12,10 +46,15 @@ type Post {
visibility: Visibility
deleted: Boolean
disabled: Boolean
pinned: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN")
createdAt: String
updatedAt: 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]!
@cypher(
statement: """
@ -40,7 +79,7 @@ type Post {
@cypher(
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?
shoutedByCurrentUser: Boolean!
@cypher(
@ -84,9 +123,12 @@ type Mutation {
DeletePost(id: ID!): Post
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
pinPost(id: ID!): Post
unpinPost(id: ID!): Post
}
type Query {
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String]
profilePagePosts(filter: _PostFilter, first: Int, offset: Int, orderBy: [_PostOrdering]): [Post]
}

View File

@ -0,0 +1,43 @@
type REPORTED {
createdAt: String
reasonCategory: ReasonCategory
reasonDescription: String
submitter: User
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN user")
# not yet supported
# resource: ReportResource
# @cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN resource")
type: String
@cypher(statement: "MATCH (resource)<-[:REPORTED]-(user:User) RETURN labels(resource)[0]")
user: User
post: Post
comment: Comment
}
# this list equals the strings of an array in file "webapp/constants/modals.js"
enum ReasonCategory {
other
discrimination_etc
pornographic_content_links
glorific_trivia_of_cruel_inhuman_acts
doxing
intentional_intimidation_stalking_persecution
advert_products_services_commercial
criminal_behavior_violation_german_law
}
# not yet supported
# union ReportResource = User | Post | Comment
enum ReportOrdering {
createdAt_asc
createdAt_desc
}
type Query {
reports(orderBy: ReportOrdering): [REPORTED]
}
type Mutation {
report(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): REPORTED
}

View File

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

View File

@ -17,6 +17,7 @@ export default function create() {
termsAndConditionsAgreedVersion: '0.0.1',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
locale: 'en',
}
defaults.slug = slugify(defaults.name, { lower: true })
args = {

View File

@ -647,10 +647,15 @@ import { gql } from '../jest/helpers'
])
authenticatedUser = null
// There is no error logged or the 'try' fails if this mutation is wrong. Why?
const reportMutation = gql`
mutation($id: ID!, $description: String!) {
report(description: $description, id: $id) {
id
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report(
resourceId: $resourceId
reasonCategory: $reasonCategory
reasonDescription: $reasonDescription
) {
type
}
}
`
@ -659,22 +664,25 @@ import { gql } from '../jest/helpers'
mutate({
mutation: reportMutation,
variables: {
description: 'This comment is bigoted',
id: 'c1',
resourceId: 'c1',
reasonCategory: 'other',
reasonDescription: 'This comment is bigoted',
},
}),
mutate({
mutation: reportMutation,
variables: {
description: 'This post is bigoted',
id: 'p1',
resourceId: 'p1',
reasonCategory: 'discrimination_etc',
reasonDescription: 'This post is bigoted',
},
}),
mutate({
mutation: reportMutation,
variables: {
description: 'This user is harassing me with bigoted remarks',
id: 'u1',
resourceId: 'u1',
reasonCategory: 'doxing',
reasonDescription: 'This user is harassing me with bigoted remarks',
},
}),
])

View File

@ -963,10 +963,10 @@
url-regex "~4.1.1"
video-extensions "~1.1.0"
"@metascraper/helpers@^5.7.6":
version "5.7.6"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.7.6.tgz#84007215d3b31525995fd85cf0d28bf6a12bf7bb"
integrity sha512-AD2VTQmMWl/KCUXl9h0fP84VacoiTI/8y8CBgErmYZnm+sliKGedQrDZO3JmzNg73Z5z08GQTjME1WHIDiIQDw==
"@metascraper/helpers@^5.7.14", "@metascraper/helpers@^5.7.17", "@metascraper/helpers@^5.7.6":
version "5.7.17"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.7.17.tgz#401897c7239090ca7149b83e581712845bbb3709"
integrity sha512-t21LqfDpaIrWg2JaivXG6mVzUsIVW05cAsKySA5Tj9Hgi9oZXxaaNes5XipOzk6P242RI48SDo7CkSbYiio7Tw==
dependencies:
audio-extensions "0.0.0"
chrono-node "~1.3.11"
@ -980,7 +980,7 @@
iso-639-3 "~1.2.0"
isostring "0.0.1"
lodash "~4.17.15"
mem "~5.1.1"
memoize-one "~5.1.1"
mime-types "~2.1.24"
normalize-url "~4.5.0"
smartquotes "~2.3.1"
@ -1042,60 +1042,60 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
"@sentry/core@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.7.0.tgz#c2aa5341e703ec7cf2acc69e51971a0b1f7d102a"
integrity sha512-gQel0d7LBSWJGHc7gfZllYAu+RRGD9GcYGmkRfemurmDyDGQDf/sfjiBi8f9QxUc2iFTHnvIR5nMTyf0U3yl3Q==
"@sentry/core@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.7.1.tgz#3eb2b7662cac68245931ee939ec809bf7a639d0e"
integrity sha512-AOn3k3uVWh2VyajcHbV9Ta4ieDIeLckfo7UMLM+CTk2kt7C89SayDGayJMSsIrsZlL4qxBoLB9QY4W2FgAGJrg==
dependencies:
"@sentry/hub" "5.7.0"
"@sentry/minimal" "5.7.0"
"@sentry/types" "5.7.0"
"@sentry/utils" "5.7.0"
"@sentry/hub" "5.7.1"
"@sentry/minimal" "5.7.1"
"@sentry/types" "5.7.1"
"@sentry/utils" "5.7.1"
tslib "^1.9.3"
"@sentry/hub@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.7.0.tgz#f7c356202a9db1daae82ce7f48ebf1139e4e9d02"
integrity sha512-qNdYheJ6j4P9Sk0eqIINpJohImmu/+trCwFb4F8BGLQth5iGMVQD6D0YUrgjf4ZaQwfhw9tv4W6VEfF5tyASoA==
"@sentry/hub@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.7.1.tgz#a52acd9fead7f3779d96e9965c6978aecc8b9cad"
integrity sha512-evGh323WR073WSBCg/RkhlUmCQyzU0xzBzCZPscvcoy5hd4SsLE6t9Zin+WACHB9JFsRQIDwNDn+D+pj3yKsig==
dependencies:
"@sentry/types" "5.7.0"
"@sentry/utils" "5.7.0"
"@sentry/types" "5.7.1"
"@sentry/utils" "5.7.1"
tslib "^1.9.3"
"@sentry/minimal@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.7.0.tgz#832d26bcd862c6ea628d48ad199ac7f966a2d907"
integrity sha512-0sizE2prS9nmfLyVUKmVzFFFqRNr9iorSCCejwnlRe3crqKqjf84tuRSzm6NkZjIyYj9djuuo9l9XN12NLQ/4A==
"@sentry/minimal@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.7.1.tgz#56afc537737586929e25349765e37a367958c1e1"
integrity sha512-nS/Dg+jWAZtcxQW8wKbkkw4dYvF6uyY/vDiz/jFCaux0LX0uhgXAC9gMOJmgJ/tYBLJ64l0ca5LzpZa7BMJQ0g==
dependencies:
"@sentry/hub" "5.7.0"
"@sentry/types" "5.7.0"
"@sentry/hub" "5.7.1"
"@sentry/types" "5.7.1"
tslib "^1.9.3"
"@sentry/node@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.7.0.tgz#153777f06b2fcd346edbff9adbb6b231c7e5fa0a"
integrity sha512-iqQbGAJDBlpQkp1rl9RkDCIfnukr4cOtHPgJPmLY19m/KXIHD2cdKhvbqoCvIPBTIAeSGQIvDT9jD5zT46eoqQ==
"@sentry/node@^5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.7.1.tgz#94e2fbac94f6cc061be3bc14b22813536c59698d"
integrity sha512-hVM10asFStrOhYZzMqFM7V1lrHkr1ydc2n/SFG0ZmIQxfTjCVElyXV/BJASIdqadM1fFIvvtD/EfgkTcZmub1g==
dependencies:
"@sentry/core" "5.7.0"
"@sentry/hub" "5.7.0"
"@sentry/types" "5.7.0"
"@sentry/utils" "5.7.0"
"@sentry/core" "5.7.1"
"@sentry/hub" "5.7.1"
"@sentry/types" "5.7.1"
"@sentry/utils" "5.7.1"
cookie "^0.3.1"
https-proxy-agent "^3.0.0"
lru_map "^0.3.3"
tslib "^1.9.3"
"@sentry/types@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.7.0.tgz#e8677e57b40c2c63cad42c02add12b238e647c10"
integrity sha512-bFRVortg713dE2yJXNFgNe6sNBVVSkpoELLkGPatdVQi0dYc6OggIIX4UZZvkynFx72GwYqO1NOrtUcJY2gmMg==
"@sentry/types@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.7.1.tgz#4c4c1d4d891b6b8c2c3c7b367d306a8b1350f090"
integrity sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ==
"@sentry/utils@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.7.0.tgz#a6850aa4f5476fa26517cd5c6248f871d8d9939b"
integrity sha512-XmwQpLqea9mj8x1N7P/l4JvnEb0Rn5Py5OtBgl0ctk090W+GB1uM8rl9mkMf6698o1s1Z8T/tI/QY0yFA5uZXg==
"@sentry/utils@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.7.1.tgz#cf37ad55f78e317665cd8680f202d307fa77f1d0"
integrity sha512-nhirUKj/qFLsR1i9kJ5BRvNyzdx/E2vorIsukuDrbo8e3iZ11JMgCOVrmC8Eq9YkHBqgwX4UnrPumjFyvGMZ2Q==
dependencies:
"@sentry/types" "5.7.0"
"@sentry/types" "5.7.1"
tslib "^1.9.3"
"@sindresorhus/is@^0.14.0":
@ -1351,32 +1351,35 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yup@0.26.23":
version "0.26.23"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.23.tgz#00721a3b675e7609e5bcccb94234e86b754bcd04"
integrity sha512-+tipAL6prdInS/avA6QityIFBDvHnqk1Tv9L5JMEws5IZC6agymBGAoDsrPyYp42wGcktyQtYKv9kvGPEKd4Qg==
"@types/yup@0.26.24":
version "0.26.24"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.24.tgz#c24170b3a5c867b4fabd49fcc42fe45f780cb153"
integrity sha512-x0bhHnYjH5mZit4HivUYbTMO4LouOTGwp/LLxSL1mbJYVwNJtHYESH0ed2bwM1lkI2yDmsoCDYJnWEgHeJDACg==
"@types/zen-observable@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
"@typescript-eslint/experimental-utils@^1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz#b08c60d780c0067de2fb44b04b432f540138301e"
integrity sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==
"@typescript-eslint/experimental-utils@^2.5.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.6.0.tgz#ed70bef72822bff54031ff0615fc888b9e2b6e8a"
integrity sha512-34BAFpNOwHXeqT+AvdalLxOvcPYnCxA5JGmBAFL64RGMdP0u65rXjii7l/nwpgk5aLEE1LaqF+SsCU0/Cb64xA==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/typescript-estree" "1.13.0"
eslint-scope "^4.0.0"
"@typescript-eslint/typescript-estree" "2.6.0"
eslint-scope "^5.0.0"
"@typescript-eslint/typescript-estree@1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz#8140f17d0f60c03619798f1d628b8434913dc32e"
integrity sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==
"@typescript-eslint/typescript-estree@2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.6.0.tgz#d3e9d8e001492e2b9124c4d4bd4e7f03c0fd7254"
integrity sha512-A3lSBVIdj2Gp0lFEL6in2eSPqJ33uAc3Ko+Y4brhjkxzjbzLnwBH22CwsW2sCo+iwogfIyvb56/AJri15H0u5Q==
dependencies:
debug "^4.1.1"
glob "^7.1.4"
is-glob "^4.0.1"
lodash.unescape "4.0.1"
semver "5.5.0"
semver "^6.3.0"
"@wry/context@^0.4.0":
version "0.4.4"
@ -1419,10 +1422,10 @@ acorn-globals@^4.1.0:
acorn "^6.0.1"
acorn-walk "^6.0.1"
acorn-jsx@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f"
integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==
acorn-jsx@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
acorn-walk@^6.0.1:
version "6.2.0"
@ -1439,10 +1442,10 @@ acorn@^6.0.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==
acorn@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a"
integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==
acorn@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
agent-base@^4.3.0:
version "4.3.0"
@ -1660,10 +1663,10 @@ apollo-server-caching@^0.5.0:
dependencies:
lru-cache "^5.0.0"
apollo-server-core@^2.9.6:
version "2.9.6"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.6.tgz#b6dc087200633f47ca4f08244d3e606b4d616320"
integrity sha512-2tHAWQxP7HrETI/BZvg2fem6YlahF9HUp4Y6SSL95WP3uNMOJBlN12yM1y+O2u5K5e4jwdPNaLjoL2A/26XrLw==
apollo-server-core@^2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.9.7.tgz#0f32344af90dec445ac780be95350bfa736fc416"
integrity sha512-EqKyROy+21sM93YHjGpy6wlnzK/vH0fnZh7RCf3uB69aQ3OjgdP4AQ5oWRQ62NDN+aoic7OLhChSDJeDonq/NQ==
dependencies:
"@apollographql/apollo-tools" "^0.4.0"
"@apollographql/graphql-playground-html" "1.6.24"
@ -1674,7 +1677,7 @@ apollo-server-core@^2.9.6:
apollo-engine-reporting "^1.4.7"
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server-errors "^2.3.3"
apollo-server-errors "^2.3.4"
apollo-server-plugin-base "^0.6.5"
apollo-server-types "^0.2.5"
apollo-tracing "^0.8.5"
@ -1695,15 +1698,15 @@ apollo-server-env@^2.4.3:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
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-errors@^2.3.4:
version "2.3.4"
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34"
integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA==
apollo-server-express@^2.9.6:
version "2.9.6"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.6.tgz#eec2ec43b829b059278e14994d06bd23e43266f9"
integrity sha512-j80azBeXvLvyZsbqCnus7GH+w8vk+2IOnYzROZu/f0D2roDZtsu1XZkn+aplDJZXMcEXtqB6t4qNpyvV4zY0XQ==
apollo-server-express@^2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.9.7.tgz#54fbaf93b68f0123ecb1dead26cbfda5b15bd10e"
integrity sha512-+DuJk1oq34Zx0bLYzgBgJH/eXS0JNxw2JycHQvV0+PAQ0Qi01oomJRA2r1S5isnfnSAnHb2E9jyBTptoHdw3MQ==
dependencies:
"@apollographql/graphql-playground-html" "1.6.24"
"@types/accepts" "^1.3.5"
@ -1711,7 +1714,7 @@ apollo-server-express@^2.9.6:
"@types/cors" "^2.8.4"
"@types/express" "4.17.1"
accepts "^1.3.5"
apollo-server-core "^2.9.6"
apollo-server-core "^2.9.7"
apollo-server-types "^0.2.5"
body-parser "^1.18.3"
cors "^2.8.4"
@ -1729,12 +1732,12 @@ apollo-server-plugin-base@^0.6.5:
dependencies:
apollo-server-types "^0.2.5"
apollo-server-testing@~2.9.6:
version "2.9.6"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.6.tgz#1cae51c93a8865b85e877e2c9927964cf32625e6"
integrity sha512-pbURQD5VjNFk4GMVVxyCds9rY4/NIqjvjE4tyf1k89RHwMdk+zuVggt/DGudteorZtqAqtsOIHWojMBU4s2klA==
apollo-server-testing@~2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.9.7.tgz#8d05058ddda4a715fac2fefb2b8e973e409a7672"
integrity sha512-yy18ceSyX2a9UYcs6X7K0xFZwcS1riEh99zdWU0XB/yzzTIdGZkFYeJmV/zjpGL3CFyXF7Va/muo6otl4nDOsA==
dependencies:
apollo-server-core "^2.9.6"
apollo-server-core "^2.9.7"
apollo-server-types@^0.2.5:
version "0.2.5"
@ -1745,13 +1748,13 @@ apollo-server-types@^0.2.5:
apollo-server-caching "^0.5.0"
apollo-server-env "^2.4.3"
apollo-server@~2.9.6:
version "2.9.6"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.6.tgz#11b6f1128ddb674d2651bb289e0c0fc28aa18653"
integrity sha512-sDvrGpMQsTGQ9FTkFm3xracrSUi8nFoh3svlD98pe6qb75UDDrXAZgxwQCSOwZ3BkaJ7UkdndfhnruhFstTeMw==
apollo-server@~2.9.7:
version "2.9.7"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.9.7.tgz#aab337b75c04ddea0fa9b171b30c4e91932c04d8"
integrity sha512-maGGCsK4Ft5ucox5ZJf6oaKhgPvzHY3jXWbA1F/mn0/EYX8e1RVO3Qtj8aQQ0/vCKx8r4vYgj+ctqBVaN/nr4A==
dependencies:
apollo-server-core "^2.9.6"
apollo-server-express "^2.9.6"
apollo-server-core "^2.9.7"
apollo-server-express "^2.9.7"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
@ -2101,10 +2104,10 @@ boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bowser@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.6.1.tgz#196599588af6f0413449c79ab3bf7a5a1bb3384f"
integrity sha512-hySGUuLhi0KetfxPZpuJOsjM0kRvCiCgPBygBkzGzJNsq/nbJmaO8QJc6xlWfeFFnMvtd/LeKkhDJGVrmVobUA==
bowser@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.7.0.tgz#96eab1fa07fab08c1ec4c75977a7c8ddf8e0fe1f"
integrity sha512-aIlMvstvu8x+34KEiOHD3AsBgdrzg6sxALYiukOWhFvGMbQI6TRP/iY0LMhUrHs56aD6P1G0Z7h45PUJaa5m9w==
boxen@^1.2.1:
version "1.3.0"
@ -2320,7 +2323,7 @@ cheerio@~1.0.0-rc.2, cheerio@~1.0.0-rc.3:
lodash "^4.15.0"
parse5 "^3.0.1"
chokidar@^2.1.5, chokidar@^2.1.8:
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
@ -2720,10 +2723,10 @@ cucumber-tag-expressions@^2.0.2:
resolved "https://registry.yarnpkg.com/cucumber-tag-expressions/-/cucumber-tag-expressions-2.0.2.tgz#aac27aae3690818ec15235bd056282dad8a2d2b8"
integrity sha512-DohmT4X641KX/sb96bdb7J2kXNcQBPrYmf3Oc5kiHCLfzFMWx/o2kB4JvjvQPZnYuA9lRt6pqtArM5gvUn4uzw==
cucumber@~6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-6.0.2.tgz#3c4fbf1f76e60ddee79ab58f137a62c897a4d7f0"
integrity sha512-yEwPYGvgS2KG6ODdUXQwWcxjyr/l31dmpGJsZSkJIXNLNNmieKVefTpf8zLj6+0V2TCPwkmUZt4+OIXv97duEw==
cucumber@~6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cucumber/-/cucumber-6.0.3.tgz#bf69ecc992772e580dabe265b2ed06ddab13d076"
integrity sha512-FSx7xdAQfFjcxp/iRBAuCFSXp2iJP1tF2Q5k/a67YgHiYbnwsD9F+UNv9ZG90LFHNsNQhb+67AmVxHkp4JRDpg==
dependencies:
assertion-error-formatter "^3.0.0"
bluebird "^3.4.1"
@ -2782,10 +2785,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.4.1.tgz#b53f9bb65ae6bd9239437035710e01cf383b625e"
integrity sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==
date-fns@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.6.0.tgz#a5bc82e6a4c3995ae124b0ba1a71aec7b8cbd666"
integrity sha512-F55YxqRdEfP/eYQmQjLN798v0AwLjmZ8nMBjdQvNwEE3N/zWVrlkkqT+9seBlPlsbkybG4JmWg3Ee3dIV9BcGQ==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
@ -3031,10 +3034,10 @@ dotenv@^4.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
dotenv@~8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.1.0.tgz#d811e178652bfb8a1e593c6dd704ec7e90d85ea2"
integrity sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==
dotenv@~8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
duplexer3@^0.1.4:
version "0.1.4"
@ -3206,10 +3209,10 @@ escodegen@^1.9.1:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.4.0.tgz#0a04f147e31d33c6c161b2dd0971418ac52d0477"
integrity sha512-YrKucoFdc7SEko5Sxe4r6ixqXPDP1tunGw91POeZTTRKItf/AMFYt/YLEQtZMkR2LVpAVhcAcZgcWpm1oGPW7w==
eslint-config-prettier@~6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz#aaf9a495e2a816865e541bfdbb73a65cc162b3eb"
integrity sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==
dependencies:
get-stdin "^6.0.0"
@ -3259,12 +3262,12 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.19.0:
version "22.19.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.19.0.tgz#0cf90946a8c927d40a2c64458c89bb635d0f2a0b"
integrity sha512-4zUc3rh36ds0SXdl2LywT4YWA3zRe8sfLhz8bPp8qQPIKvynTTkNGwmSCMpl5d9QiZE2JxSinGF+WD8yU+O0Lg==
eslint-plugin-jest@~23.0.2:
version "23.0.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.0.2.tgz#54a59bfe77245186afe13711a297067aefefff0a"
integrity sha512-fkxcvOJm0hC/jbJqYJjtuC9mvpTJqXd0Nixx7joVQvJoBQuXk/ws3+MtRYzD/4TcKSgvr21uuSLdwSxKJKC2cg==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
"@typescript-eslint/experimental-utils" "^2.5.0"
eslint-plugin-node@~10.0.0:
version "10.0.0"
@ -3295,14 +3298,6 @@ eslint-plugin-standard@~4.0.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4"
integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==
eslint-scope@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-scope@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
@ -3311,22 +3306,22 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0"
estraverse "^4.1.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==
eslint-utils@^1.4.2, eslint-utils@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
dependencies:
eslint-visitor-keys "^1.0.0"
eslint-visitor-keys "^1.1.0"
eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
version "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.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6"
integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==
eslint@~6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.6.0.tgz#4a01a2fb48d32aacef5530ee9c5a78f11a8afd04"
integrity sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
@ -3335,9 +3330,9 @@ eslint@~6.5.1:
debug "^4.0.1"
doctrine "^3.0.0"
eslint-scope "^5.0.0"
eslint-utils "^1.4.2"
eslint-utils "^1.4.3"
eslint-visitor-keys "^1.1.0"
espree "^6.1.1"
espree "^6.1.2"
esquery "^1.0.1"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
@ -3347,7 +3342,7 @@ eslint@~6.5.1:
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.4.1"
inquirer "^7.0.0"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
@ -3366,13 +3361,13 @@ eslint@~6.5.1:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de"
integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ==
espree@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d"
integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==
dependencies:
acorn "^7.0.0"
acorn-jsx "^5.0.2"
acorn "^7.1.0"
acorn-jsx "^5.1.0"
eslint-visitor-keys "^1.1.0"
esprima@^3.1.3:
@ -3594,7 +3589,8 @@ extsprintf@^1.2.0:
faker@Marak/faker.js#master:
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:
version "2.0.1"
@ -3918,6 +3914,18 @@ glob@7.1.4, glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.4:
version "7.1.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
integrity sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-dirs@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
@ -4029,14 +4037,14 @@ graphql-request@~1.8.2:
dependencies:
cross-fetch "2.2.2"
graphql-shield@~6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-6.1.0.tgz#7298af72167e7c9fd19a36fac9b425b94025a393"
integrity sha512-dIZ6ABnUn3XQtIzw9/9f8wFmZoY5XZlsHgkxSKF+N/oXmKvQoi11J5/y/jxJTBmKYi/2JZ12C1JjDn5TOopn+w==
graphql-shield@~7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.0.tgz#0cdca5c16af4ba7dd4fbcce6df279c5c8e463452"
integrity sha512-hr0PR6t/UXAO5+BMOOD2W3zTIKbtO/u8twjLn8hw4646E08NeLrIxDAmUFKKlLhyTe5JzlH4nNflP6SRtL6Q2A==
dependencies:
"@types/yup" "0.26.23"
"@types/yup" "0.26.24"
lightercollective "^0.3.0"
object-hash "^1.3.1"
object-hash "^2.0.0"
yup "^0.27.0"
graphql-subscriptions@^1.0.0:
@ -4207,20 +4215,20 @@ helmet-crossdomain@0.4.0:
resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e"
integrity sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==
helmet-csp@2.9.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.2.tgz#bec0adaf370b0f2e77267c9d8b6e33b34159c1e5"
integrity sha512-Lt5WqNfbNjEJ6ysD4UNpVktSyjEKfU9LVJ1LaFmPfYseg/xPealPfgHhtqdAdjPDopp5zbg/VWCyp4cluMIckw==
helmet-csp@2.9.4:
version "2.9.4"
resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.9.4.tgz#801382bac98f2f88706dc5c89d95c7e31af3a4a9"
integrity sha512-qUgGx8+yk7Xl8XFEGI4MFu1oNmulxhQVTlV8HP8tV3tpfslCs30OZz/9uQqsWPvDISiu/NwrrCowsZBhFADYqg==
dependencies:
bowser "^2.6.1"
bowser "^2.7.0"
camelize "1.0.0"
content-security-policy-builder "2.1.0"
dasherize "2.0.0"
helmet@~3.21.1:
version "3.21.1"
resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.1.tgz#b0ab7c63fc30df2434be27e7e292a9523b3147e9"
integrity sha512-IC/54Lxvvad2YiUdgLmPlNFKLhNuG++waTF5KPYq/Feo3NNhqMFbcLAlbVkai+9q0+4uxjxGPJ9bNykG+3zZNg==
helmet@~3.21.2:
version "3.21.2"
resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.21.2.tgz#7e2a19d5f6d898a77b5d2858e8e4bb2cda59f19f"
integrity sha512-okUo+MeWgg00cKB8Csblu8EXgcIoDyb5ZS/3u0W4spCimeVuCUvVZ6Vj3O2VJ1Sxpyb8jCDvzu0L1KKT11pkIg==
dependencies:
depd "2.0.0"
dns-prefetch-control "0.2.0"
@ -4229,7 +4237,7 @@ helmet@~3.21.1:
feature-policy "0.3.0"
frameguard "3.1.0"
helmet-crossdomain "0.4.0"
helmet-csp "2.9.2"
helmet-csp "2.9.4"
hide-powered-by "1.1.0"
hpkp "2.0.0"
hsts "2.2.0"
@ -4443,10 +4451,10 @@ ini@^1.3.4, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inquirer@^6.4.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.1.tgz#8bfb7a5ac02dac6ff641ac4c5ff17da112fcdb42"
integrity sha512-uxNHBeQhRXIoHWTSNYUFhQVrHYFThIt6IVo2fFmSe8aBwdR3/w6b58hJpiL/fMukFkvGzjg+hSxFtwvVmKZmXw==
inquirer@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a"
integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==
dependencies:
ansi-escapes "^4.2.1"
chalk "^2.4.2"
@ -5668,13 +5676,6 @@ makeerror@1.0.x:
dependencies:
tmpl "1.0.x"
map-age-cleaner@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
dependencies:
p-defer "^1.0.0"
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@ -5692,15 +5693,6 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
mem@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/mem/-/mem-5.1.1.tgz#7059b67bf9ac2c924c9f1cff7155a064394adfb3"
integrity sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==
dependencies:
map-age-cleaner "^0.1.3"
mimic-fn "^2.1.0"
p-is-promise "^2.1.0"
memoize-one@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
@ -5729,19 +5721,19 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
metascraper-audio@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.7.6.tgz#05f3a732e8316eb80c0ee58d5981d053baff6cec"
integrity sha512-1CXw5+2WIxF5O0sJ1Hp4Zt8nSg4zXJXa9AUPMhnyhovLJ4cqGBdNVUbsxduuEHfYPiG1PtZyrtgDq+OQqiS5kA==
metascraper-audio@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.7.14.tgz#f5edbc0249af3fcd09863580c3531eb4b8546589"
integrity sha512-ev2PIDNTILznkDeWy/CFaKdhFlG/3URF0OOO4J2MXy8VNKd5IyP/+LF7zoPR9q5+C40+zRAmy8cqJId3qVAa2A==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
metascraper-author@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-5.7.6.tgz#ccef7c987f433ebe00a444ff9d1bcd1c3f65c825"
integrity sha512-kxUrQIZVZUWzA7GInasT/InTuRZ6VPE3QCWNqrha6p89+nxHfRMpfL1YTgdQWs1Y8MGKETt1uXV20tkKQVbPuw==
metascraper-author@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-5.7.17.tgz#0403eaa4d1992152246f01616fac1d52b0583c8a"
integrity sha512-vaMAn6glCr9f2PGvNObqMI7ECtQ7+CMkXSxKyn3fyxRVKnV95fBR+xi4+UJ2DWqTvVQ6t7gZwlzFWA4CwxfniQ==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.17"
lodash "~4.17.15"
metascraper-clearbit-logo@^5.3.0:
@ -5751,19 +5743,19 @@ metascraper-clearbit-logo@^5.3.0:
dependencies:
got "~9.6.0"
metascraper-date@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-5.7.6.tgz#6d2e2b39f0a43374abf6f8639017b2500b821a54"
integrity sha512-ikTNuOrKk9nA78/dxeTydkO4kajaFEzR6IAi1GVXTKzhMTaH9A8HA8ra/LndD8KYZMAEmJaIFvefi8vGVVkcUw==
metascraper-date@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-5.7.14.tgz#5cb57c8f60bd15c841268fa983545dd3a0586a3c"
integrity sha512-eJKMtIFeBrnkAavkNlIT/O2bKmF2gKVgMpPbdg/9yJ+OS0pH3QTdk/I/NeU91fS0dAaC2ztwFKUqw2zjC27vew==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
metascraper-description@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.7.6.tgz#3a26b8bb8f325b1e959864f2c983ad4ef6050c24"
integrity sha512-DtcIRTwI2RFdy2NBSYCUpekNSqMH4BVNjAJNpWWYsDJZbk5rw6w2gOgzJH4HUM4eYKDkd0/tVd+HZYz57xxYBQ==
metascraper-description@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.7.14.tgz#4b77b04120f5f4f17dcabb4f549a7954da74b581"
integrity sha512-++qN4Rf0Hx13SbhJgRiLSuVOZHsYwhUkMfHa5sVVihSJkrLVjOSdBTpNBajRC7yHwG6m6/qIesuERbT1jdu5bw==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
metascraper-image@^5.7.6:
version "5.7.6"
@ -5781,65 +5773,65 @@ metascraper-lang-detector@^4.8.5:
franc "~4.0.0"
iso-639-3 "~1.1.0"
metascraper-lang@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.7.6.tgz#9d0ac51ed29a99b5864b2291f89b309d08ffe6af"
integrity sha512-RLeAB0Vzz8M3V98unF9VD3Q43I8HqMAx4rpy3Zml4ysvdZAwKsZZUT8IVVWOXhnfWCgjZDstjNhVbgQpsSsthQ==
metascraper-lang@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-5.7.14.tgz#f7d5fa5c72090229c0524105bfe43e27b3bfd0ab"
integrity sha512-FyH3sILuldQBlMbMR5ObpRbxrPJ58sR2+8XJfo4oxnAvsZhBhe2q2uCxOgPhczJRUISzNLUEwMWY1wnYmu8nZg==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
metascraper-logo@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.7.6.tgz#92680c8e839c6c357ecb4d7fe0445f46233d0f09"
integrity sha512-0pMHxua4dNUcWLCk4WGCqBcuMoUoMAr3kFT34tJZTAd345iCagtwNNs2iAcNLdpNqyXzKyGIKcZPNkbWByVcCQ==
metascraper-logo@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.7.17.tgz#b26e2fb38e94cfe9ec9dfc7e28d8da26a0a0689d"
integrity sha512-S4aqxN4Qi3UXDLN4HhinEuQHUopYXbFw0Y5Cwj9TbGKfESeQ1n6Jm4eOgGifEYyyZMSeRR9li189EK3YPnYcFg==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.17"
metascraper-publisher@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-5.7.6.tgz#d0b04ae2f260a5bf0536fad29b3617fb9b6df296"
integrity sha512-RVvyNbzJnWutA2rSbK0CytokBXu1SVfsnME+IeKOGmtiSnKtGx5Q0lALHNVHt+bIShEYgba0UlVO/mPE37dU8g==
metascraper-publisher@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-5.7.14.tgz#74218cd2c1042264a3df754930e18d5e1f731750"
integrity sha512-6WHVr03tu4+KtcETR/q8y7ND9HxKI8QEEI1NAVpAolm1szDSpzV1PXVxWILcc2zPsaAHBSvM0iMwmE1zBEs+BA==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
metascraper-soundcloud@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.7.6.tgz#80c725e8746d94c992b5bdd07ac6bd987d09944d"
integrity sha512-fBxX5mYPFf8rWhhEX2XZD5QrmvtUI5IIPzryGuwEWsbPuMGuUkvFA9JjHJiC46uYXoi6UuKLXwSmYHcAACG3Jg==
metascraper-soundcloud@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-soundcloud/-/metascraper-soundcloud-5.7.14.tgz#5f369ffe06cfee9af9f877dc71da46daa1280218"
integrity sha512-Oc/TEXvq+Qw4acZQipwLCDazME9bsCIbkZmrw1603zOmudANE9EPJ6D3ZdcZkwIDjE/GtYtCkJENjuN55mTQ2Q==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
memoize-one "~5.1.1"
tldts "~5.5.0"
tldts "~5.6.1"
metascraper-title@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.7.6.tgz#346c637e735a040f299af5f67715eb8ed0016850"
integrity sha512-DF6TeMODzzLgJMLyUtN6wLPrz9/3JcRKyUIfWpuuw+WFC3Kx6ON8nWldTRh1yUu9xbSAOleae//f/dn+JhYlCw==
metascraper-title@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.7.14.tgz#4abea12bc9f0d3df5b442cb3c1a8c6559e417ea4"
integrity sha512-ZiVo4LEfqiNHlCGjht5OSZ3yRKxcZnbaXeRmUReMkCHcFujok5YZBj5ktDpAANmG9T3x2gn3twM3ZbBSyXLYyg==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
lodash "~4.17.15"
metascraper-url@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.7.6.tgz#2f35b50e12ed14e2e6062285fd10cd3f37ec1bd8"
integrity sha512-V0ddB/UKsWOXcO5cQVdiX5IHPkC7wpWnVj6sc7NkHWImzq8GAQR6jWaPQ9t8uhQuLdqiXaW9l+a6x6zX3LC/hw==
metascraper-url@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.7.14.tgz#20645ec0299f1fe4bf194b08037e344b9555bfd0"
integrity sha512-scsXsbhI9VFcmgtMI/bsr+onvzzWGX4h80pitQQpECA7X2K2qcm5qic+anv6K2simbPJ/brDkhHC2rMRm9snbw==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
metascraper-video@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.7.6.tgz#ae149d6804ba026155d4c71e59b4d2ffb95b3062"
integrity sha512-9Ak7QI3Je21h0+3i09SruGn1sLWUSB7ATLPtiVd7DfRb5O164LWhhmvMBxaMHhoMnvWgU9xuMkzQfI8kY66rBw==
metascraper-video@^5.7.14:
version "5.7.14"
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-5.7.14.tgz#3deb86e8c63b7bca0222c3b22f9dc27f0c77050e"
integrity sha512-nmXrxaf83f/nRbSYegAtpPzsoNafiJOT9zaAQFawRCQnxCBlMQoPALEzGGx1b/R5YuyWsqJzjjVTigBgQhcrGg==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.14"
lodash "~4.17.15"
metascraper-youtube@^5.7.6:
version "5.7.6"
resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.7.6.tgz#4add25d3e86752d0429cf195ef9a130c5f0d1a4a"
integrity sha512-GLjnYeOELQJ7upu6ji6yILpF835i7cttZZwIrPxKWwTJKyAUJ5Ydp0wiqm/xkmMBIsQ6isgRIVSxOb6g9SnH+w==
metascraper-youtube@^5.7.17:
version "5.7.17"
resolved "https://registry.yarnpkg.com/metascraper-youtube/-/metascraper-youtube-5.7.17.tgz#a3bdf06bbc9aa3766f08a779fa880d8a3fda9f8c"
integrity sha512-CZX03wX8ui8fjx+iBZCiAGdSKy4dMFiDrVSPmTMK2W8sn2guYv2QQ41g8gruFJgrF+m+mCOUG6KYgy3B/v5LdQ==
dependencies:
"@metascraper/helpers" "^5.7.6"
"@metascraper/helpers" "^5.7.17"
get-video-id "~3.1.4"
is-reachable "~4.0.0"
memoize-one "~5.1.1"
@ -6069,13 +6061,14 @@ neo4j-driver@^1.7.3, neo4j-driver@^1.7.5, neo4j-driver@~1.7.6:
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neo4j-graphql-js@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.7.2.tgz#6a56c63874bc41e678cb83580c6c7647e6f61ccf"
integrity sha512-nrhSmNAkiYgksNabNuHyMHYYaLloYZaVXRiYGrRVUcf84TLiwJdg5k9hIQVH9O6QQOvnK3lwBDlE7T0phpAIpg==
neo4j-graphql-js@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.8.0.tgz#58035b9213656e17b6ed4c6cbf4dfe1c56a8a219"
integrity sha512-nDuzmi6W/YGIIVm+GAXCr/8CLABsU/RfeLebLH32vqeKViFATMfm4eT66aOq/GwHJ0838+o20yCbIFdx5rTP/A==
dependencies:
"@babel/runtime" "^7.5.5"
"@babel/runtime-corejs2" "^7.5.5"
debug "^4.1.1"
graphql "^14.2.1"
graphql-auth-directives "^2.1.0"
lodash "^4.17.15"
@ -6187,18 +6180,18 @@ nodemailer@^6.3.1:
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.1.tgz#2784beebac6b9f014c424c54dbdcc5c4d1221346"
integrity sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ==
nodemon@~1.19.3:
version "1.19.3"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.3.tgz#db71b3e62aef2a8e1283a9fa00164237356102c0"
integrity sha512-TBNKRmJykEbxpTniZBusqRrUTHIEqa2fpecbTQDQj1Gxjth7kKAPP296ztR0o5gPUWsiYbuEbt73/+XMYab1+w==
nodemon@~1.19.4:
version "1.19.4"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.4.tgz#56db5c607408e0fdf8920d2b444819af1aae0971"
integrity sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==
dependencies:
chokidar "^2.1.5"
debug "^3.1.0"
chokidar "^2.1.8"
debug "^3.2.6"
ignore-by-default "^1.0.1"
minimatch "^3.0.4"
pstree.remy "^1.1.6"
semver "^5.5.0"
supports-color "^5.2.0"
pstree.remy "^1.1.7"
semver "^5.7.1"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.2"
update-notifier "^2.5.0"
@ -6341,10 +6334,10 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
object-hash@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
object-hash@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.0.tgz#7c4cc341eb8b53367312a7c546142f00c9e0ea20"
integrity sha512-I7zGBH0rDKwVGeGZpZoFaDhIwvJa3l1CZE+8VchylXbInNiCj7sxxea9P5dTM4ftKR5//nrqxrdeGSTWL2VpBA==
object-keys@^1.0.11, object-keys@^1.0.12:
version "1.1.1"
@ -6492,11 +6485,6 @@ p-cancelable@^2.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
p-defer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
p-each-series@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
@ -6509,11 +6497,6 @@ p-finally@^1.0.0:
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
p-is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@ -6895,7 +6878,7 @@ psl@^1.1.24, psl@^1.1.28:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd"
integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==
pstree.remy@^1.1.6:
pstree.remy@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3"
integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==
@ -7407,16 +7390,11 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0:
"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -7890,7 +7868,7 @@ supports-color@^4.0.0:
dependencies:
has-flag "^2.0.0"
supports-color@^5.2.0, supports-color@^5.3.0:
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@ -8026,17 +8004,17 @@ tlds@^1.187.0, tlds@^1.203.0:
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc"
integrity sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw==
tldts-core@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.5.0.tgz#ae22afe586541ac5ecacc520038068639b3420b4"
integrity sha512-o0JzahqioihXz8wj7/1OYtefyhXz/PwLno7VRm5MTwQitEOPpvMPZpj2yjXtjgOMKbi3A5OHvvJwhFf0Hutzng==
tldts-core@^5.6.1:
version "5.6.1"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-5.6.1.tgz#943fd020b564018fae308c12ec2435e53101c257"
integrity sha512-ikhUCHoiRu0QzQpba0f0q1Km5YBnn4qsBzGlYCzT3y3wSCGG2GlV0xeEOcXTzp2pRne6bQaHRry4TINMZpDFKQ==
tldts@~5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.5.0.tgz#12ea124593bc5abebd12107c6223986f97972bc1"
integrity sha512-CZ/d7Y4k8onxwerMWz/mTCeKJtX3VAMiL+ajXVFnxsKhH4BV+QavjnZ1Mb9OeCHo3jX0S3Dw6ERNRXqOMVsDvw==
tldts@~5.6.1:
version "5.6.1"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-5.6.1.tgz#36f4ac97505b9202f2872f6246f326589f49d78b"
integrity sha512-I+imSP592J9GUYApIoiDdJk3KlroHY4zmDmpAp+TlIDZZAPxx192yOUViMB2QmlcRtZUz5XLEM3cS2F0V7P1Fw==
dependencies:
tldts-core "^5.5.0"
tldts-core "^5.6.1"
tmp@^0.0.33:
version "0.0.33"

View File

@ -0,0 +1,2 @@
// please change also version in file "webapp/constants/terms-and-conditions-version.js"
export const VERSION = '0.0.3'

View File

@ -1,4 +1,5 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import { VERSION } from '../../constants/terms-and-conditions-version.js'
/* global cy */
@ -31,7 +32,7 @@ Given('I am logged in with a {string} role', role => {
cy.factory().create('User', {
email: `${role}@example.org`,
password: '1234',
termsAndConditionsAgreedVersion: "0.0.2",
termsAndConditionsAgreedVersion: VERSION,
role
})
cy.login({
@ -107,6 +108,11 @@ Then(`I can't see the moderation menu item`, () => {
When(/^I confirm the reporting dialog .*:$/, message => {
cy.contains(message) // wait for element to become visible
cy.get('.ds-modal').within(() => {
cy.get('.ds-radio-option-label')
.first()
.click({
force: true
})
cy.get('button')
.contains('Report')
.click()
@ -114,21 +120,22 @@ When(/^I confirm the reporting dialog .*:$/, message => {
})
Given('somebody reported the following posts:', table => {
table.hashes().forEach(({ id }) => {
table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
const submitter = {
email: `submitter${id}@example.org`,
email: submitterEmail,
password: '1234'
}
cy.factory()
.create('User', submitter)
.authenticateAs(submitter)
.mutate(`mutation($id: ID!, $description: String!) {
report(description: $description, id: $id) {
id
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
type
}
}`, {
id,
description: 'Offensive content'
resourceId,
reasonCategory,
reasonDescription
})
})
})

View File

@ -4,6 +4,7 @@ import {
Then
} from "cypress-cucumber-preprocessor/steps";
import helpers from "../../support/helpers";
import { VERSION } from '../../constants/terms-and-conditions-version.js'
/* global cy */
@ -14,7 +15,7 @@ let loginCredentials = {
password: "1234"
};
const termsAndConditionsAgreedVersion = {
termsAndConditionsAgreedVersion: "0.0.2"
termsAndConditionsAgreedVersion: VERSION
};
const narratorParams = {
id: 'id-of-peter-pan',

View File

@ -50,8 +50,8 @@ Feature: Report and Moderate
Scenario: Review reported content
Given somebody reported the following posts:
| id |
| p1 |
| submitterEmail | resourceId | reasonCategory | reasonDescription |
| p1.submitter@example.org | p1 | discrimination_etc | Offensive content |
And I am logged in with a "moderator" role
When I click on the avatar menu in the top right corner
And I click on "Moderation"
@ -60,8 +60,8 @@ Feature: Report and Moderate
Scenario: Review reported posts of a user who's blocked a moderator
Given somebody reported the following posts:
| id |
| p2 |
| submitterEmail | resourceId | reasonCategory | reasonDescription |
| p2.submitter@example.org | p2 | other | Offensive content |
And my user account has the role "moderator"
And there is an annoying user who has blocked me
And I am logged in

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)"
ARG BUILD_COMMIT

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
ENV_FILE=$(dirname "$0")/.env
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
if [ -z "$NEO4J_USERNAME" ] || [ -z "$NEO4J_PASSWORD" ]; then
echo "Please set NEO4J_USERNAME and NEO4J_PASSWORD environment variables."
echo "Database manipulation is not possible without connecting to the database."
echo "E.g. you could \`cp .env.template .env\` unless you run the script in a docker container"
fi
until echo 'RETURN "Connection successful" as info;' | cypher-shell
do
echo "Connecting to neo4j failed, trying again..."
sleep 1
done
echo "
MATCH (submitter:User)-[:REPORTED]->(report:Report)-[:REPORTED]->(resource)
DETACH DELETE report
CREATE (submitter)-[reported:REPORTED]->(resource)
SET reported.createdAt = toString(datetime())
SET reported.reasonCategory = 'other'
SET reported.reasonDescription = '!!! Created automatically to ensure database consistency! Creation date is when the database manipulation happened.'
RETURN reported;
" | cypher-shell

View File

@ -1,6 +1,6 @@
{
"name": "nitro-cypress",
"version": "1.0.0",
"version": "0.1.8",
"description": "Fullstack tests with cypress for Human Connection",
"author": "Human Connection gGmbh",
"license": "MIT",
@ -16,22 +16,24 @@
"cypress:setup": "run-p cypress:backend:* cypress:webapp",
"cypress:run": "cypress run --browser chromium",
"cypress:open": "cypress open --browser chromium",
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov"
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov",
"version": "auto-changelog -p"
},
"devDependencies": {
"auto-changelog": "^1.16.2",
"bcryptjs": "^2.4.3",
"codecov": "^3.6.1",
"cross-env": "^6.0.3",
"cypress": "^3.4.1",
"cypress": "^3.5.0",
"cypress-cucumber-preprocessor": "^1.16.2",
"cypress-file-upload": "^3.3.4",
"cypress-file-upload": "^3.4.0",
"cypress-plugin-retries": "^1.3.0",
"date-fns": "^2.4.1",
"dotenv": "^8.1.0",
"date-fns": "^2.6.0",
"dotenv": "^8.2.0",
"faker": "Marak/faker.js#master",
"graphql-request": "^1.8.2",
"neo4j-driver": "^1.7.6",
"neode": "^0.3.3",
"neode": "^0.3.6",
"npm-run-all": "^4.1.5",
"slug": "^1.1.0"
},

@ -1 +1 @@
Subproject commit d46fc1570c6bcea328ae4cc3a4892745edea7319
Subproject commit 808b3c5a9523505cb80b20b50348d29ba9932845

View File

@ -1,4 +1,4 @@
FROM node:12.12.0-alpine as base
FROM node:13.0.1-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000

View File

@ -1,5 +1,5 @@
FROM node:12.12.0-alpine as build
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
FROM node:13.0.1-alpine as build
LABEL Description="Maintenance page of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000
CMD ["yarn", "run", "start"]

View File

@ -10,7 +10,7 @@
</ds-card>
</div>
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-card :id="anchor">
<ds-card :id="anchor" :class="{ 'comment--target': isTarget }">
<ds-space margin-bottom="small" margin-top="small">
<hc-user :user="author" :date-time="comment.createdAt">
<template v-slot:dateTime>
@ -43,35 +43,15 @@
/>
</div>
<div v-else>
<content-viewer
v-if="$filters.removeHtml(comment.content).length < 180"
:content="comment.content"
class="padding-left"
/>
<div v-else class="show-more-or-less-div">
<content-viewer
v-if="isCollapsed"
:content="$filters.truncate(comment.content, 180)"
class="padding-left text-align-left"
/>
<span class="show-more-or-less">
<a v-if="isCollapsed" class="padding-left" @click="isCollapsed = !isCollapsed">
{{ $t('comment.show.more') }}
</a>
</span>
</div>
<content-viewer
v-if="!isCollapsed"
:content="comment.content"
class="padding-left text-align-left"
/>
<div class="show-more-or-less-div">
<span class="show-more-or-less">
<a v-if="!isCollapsed" class="padding-left" @click="isCollapsed = !isCollapsed">
{{ $t('comment.show.less') }}
</a>
</span>
</div>
<content-viewer :content="commentContent" class="comment-content" />
<button
v-if="isLongComment"
type="button"
class="collapse-button"
@click="isCollapsed = !isCollapsed"
>
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
</button>
</div>
<ds-space margin-bottom="small" />
</ds-card>
@ -80,6 +60,7 @@
<script>
import { mapGetters } from 'vuex'
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
import HcUser from '~/components/User/User'
import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
@ -89,9 +70,14 @@ import scrollToAnchor from '~/mixins/scrollToAnchor.js'
export default {
mixins: [scrollToAnchor],
data: function() {
data() {
const anchor = `commentId-${this.comment.id}`
const isTarget = this.routeHash === `#${anchor}`
return {
isCollapsed: true,
anchor,
isTarget,
isCollapsed: !isTarget,
openEditCommentMenu: false,
}
},
@ -102,13 +88,9 @@ export default {
HcCommentForm,
},
props: {
post: { type: Object, default: () => {} },
comment: {
type: Object,
default() {
return {}
},
},
routeHash: { type: String, default: () => '' },
post: { type: Object, default: () => ({}) },
comment: { type: Object, default: () => ({}) },
dateTime: { type: [Date, String], default: null },
},
computed: {
@ -116,8 +98,15 @@ export default {
user: 'auth/user',
isModerator: 'auth/isModerator',
}),
anchor() {
return `commentId-${this.comment.id}`
isLongComment() {
return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH
},
commentContent() {
if (this.isLongComment && this.isCollapsed) {
return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH)
}
return this.comment.content
},
displaysComment() {
return !this.unavailable || this.isModerator
@ -160,6 +149,7 @@ export default {
},
editCommentMenu(showMenu) {
this.openEditCommentMenu = showMenu
this.$emit('toggleNewCommentForm', !showMenu)
},
updateComment(comment) {
this.$emit('updateComment', comment)
@ -182,26 +172,37 @@ export default {
}
</script>
<style lang="scss" scoped>
.collapse-button {
// TODO: move this to css resets
font-family: inherit;
font-size: inherit;
border: none;
background-color: transparent;
float: right;
padding: 0 16px 16px 16px;
color: $color-primary;
cursor: pointer;
}
.comment-content {
padding-left: 40px;
}
.float-right {
float: right;
}
.padding-left {
padding-left: 40px;
@keyframes highlight {
0% {
border: 1px solid $color-primary;
}
100% {
border: 1px solid transparent;
}
}
.text-align-left {
text-align: left;
}
div.show-more-or-less-div {
text-align: right;
margin-right: 20px;
}
span.show-more-or-less {
display: block;
margin: 0px 20px;
cursor: pointer;
.comment--target {
animation: highlight 4s ease;
}
</style>

View File

@ -0,0 +1,45 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import HcCommentList from './CommentList.vue'
import helpers from '~/storybook/helpers'
import faker from 'faker'
helpers.init()
const commentMock = fields => {
return {
id: faker.random.uuid(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
deleted: false,
disabled: false,
...fields,
}
}
const comments = [
commentMock(),
commentMock(),
commentMock(),
commentMock(),
commentMock(),
commentMock(),
commentMock(),
commentMock(),
commentMock(),
commentMock(),
]
storiesOf('CommentList', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('given 10 comments', () => ({
components: { HcCommentList },
store: helpers.store,
data: () => ({
post: { comments },
}),
template: `<hc-comment-list :post="post" />`,
}))

View File

@ -22,8 +22,10 @@
:key="comment.id"
:comment="comment"
:post="post"
:routeHash="routeHash"
@deleteComment="updateCommentList"
@updateComment="updateCommentList"
@toggleNewCommentForm="toggleNewCommentForm"
/>
</div>
</div>
@ -38,6 +40,7 @@ export default {
Comment,
},
props: {
routeHash: { type: String, default: () => '' },
post: { type: Object, default: () => {} },
},
methods: {
@ -49,6 +52,9 @@ export default {
return comment.id === updatedComment.id ? updatedComment : comment
})
},
toggleNewCommentForm(showNewCommentForm) {
this.$emit('toggleNewCommentForm', showNewCommentForm)
},
},
}
</script>

View File

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

View File

@ -10,9 +10,17 @@
</hc-teaser-image>
<ds-card>
<ds-space />
<hc-user :user="currentUser" :trunc="35" />
<client-only>
<hc-user :user="currentUser" :trunc="35" />
</client-only>
<ds-space />
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
<ds-input
model="title"
class="post-title"
:placeholder="$t('contribution.title')"
name="title"
autofocus
/>
<small class="smallTag">{{ form.title.length }}/{{ formSchema.title.max }}</small>
<client-only>
<hc-editor

View File

@ -1,5 +1,5 @@
<template>
<editor-content :editor="editor" />
<editor-content :editor="editor" :key="content" />
</template>
<script>
@ -31,6 +31,9 @@ export default {
}),
}
},
beforeUpdate() {
this.editor.setContent(this.content)
},
beforeDestroy() {
this.editor.destroy()
},

View File

@ -67,13 +67,13 @@ export default {
},
computed: {
...mapGetters({
filteredCategoryIds: 'postsFilter/filteredCategoryIds',
filteredCategoryIds: 'posts/filteredCategoryIds',
}),
},
methods: {
...mapMutations({
resetCategories: 'postsFilter/RESET_CATEGORIES',
toggleCategory: 'postsFilter/TOGGLE_CATEGORY',
resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'posts/TOGGLE_CATEGORY',
}),
},
}

View File

@ -50,20 +50,20 @@ describe('FilterPosts.vue', () => {
describe('mount', () => {
mutations = {
'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'postsFilter/RESET_CATEGORIES': jest.fn(),
'postsFilter/TOGGLE_CATEGORY': jest.fn(),
'postsFilter/TOGGLE_EMOTION': jest.fn(),
'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'posts/RESET_CATEGORIES': jest.fn(),
'posts/TOGGLE_CATEGORY': jest.fn(),
'posts/TOGGLE_EMOTION': jest.fn(),
}
getters = {
'postsFilter/isActive': () => false,
'posts/isActive': () => false,
'auth/isModerator': () => false,
'auth/user': () => {
return { id: 'u34' }
},
'postsFilter/filteredCategoryIds': jest.fn(() => []),
'postsFilter/filteredByUsersFollowed': jest.fn(),
'postsFilter/filteredByEmotions': jest.fn(() => []),
'posts/filteredCategoryIds': jest.fn(() => []),
'posts/filteredByUsersFollowed': jest.fn(),
'posts/filteredByEmotions': jest.fn(() => []),
}
const openFilterPosts = () => {
const store = new Vuex.Store({ mutations, getters })
@ -94,18 +94,18 @@ describe('FilterPosts.vue', () => {
const wrapper = openFilterPosts()
environmentAndNatureButton = wrapper.findAll('button').at(2)
environmentAndNatureButton.trigger('click')
expect(mutations['postsFilter/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
})
it('sets category button attribute `primary` when corresponding category is filtered', () => {
getters['postsFilter/filteredCategoryIds'] = jest.fn(() => ['cat9'])
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(4)
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
})
it('sets "filter-by-followed-authors-only" button attribute `primary`', () => {
getters['postsFilter/filteredByUsersFollowed'] = jest.fn(() => true)
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts()
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
@ -120,7 +120,7 @@ describe('FilterPosts.vue', () => {
})
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
expect(mutations['postsFilter/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
})
})
@ -129,11 +129,11 @@ describe('FilterPosts.vue', () => {
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
happyEmotionButton.trigger('click')
expect(mutations['postsFilter/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
})
it('sets the attribute `src` to colorized image', () => {
getters['postsFilter/filteredByEmotions'] = jest.fn(() => ['happy'])
getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
const happyEmotionButtonImage = happyEmotionButton.find('img')

View File

@ -39,7 +39,7 @@ export default {
computed: {
...mapGetters({
currentUser: 'auth/user',
filterActive: 'postsFilter/isActive',
filterActive: 'posts/isActive',
}),
chunk() {
return chunk(this.categories, 2)

View File

@ -68,14 +68,14 @@ export default {
},
computed: {
...mapGetters({
filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed',
filteredByEmotions: 'postsFilter/filteredByEmotions',
filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByEmotions: 'posts/filteredByEmotions',
}),
},
methods: {
...mapMutations({
toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED',
toogleFilteredByEmotions: 'postsFilter/TOGGLE_EMOTION',
toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
toogleFilteredByEmotions: 'posts/TOGGLE_EMOTION',
}),
iconPath(emotion) {
if (this.filteredByEmotions.includes(emotion)) {

View File

@ -2,26 +2,43 @@ import { mount, createLocalVue } from '@vue/test-utils'
import Styleguide from '@human-connection/styleguide'
import VTooltip from 'v-tooltip'
import LocaleSwitch from './LocaleSwitch.vue'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
localVue.use(Vuex)
describe('LocaleSwitch.vue', () => {
let wrapper
let mocks
let computed
let deutschLanguageItem
let wrapper, mocks, computed, deutschLanguageItem, getters
beforeEach(() => {
mocks = {
$i18n: {
locale: () => 'de',
set: jest.fn(),
locale: () => 'en',
set: jest.fn(locale => locale),
},
$t: jest.fn(),
$toast: {
success: jest.fn(a => a),
error: jest.fn(a => a),
},
setPlaceholderText: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
UpdateUser: {
locale: 'de',
},
},
})
.mockRejectedValueOnce({
message: 'Please log in!',
}),
},
}
computed = {
current: () => {
@ -40,12 +57,21 @@ describe('LocaleSwitch.vue', () => {
]
},
}
getters = {
'auth/user': () => {
return { id: 'u35' }
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(LocaleSwitch, { mocks, localVue, computed })
}
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(LocaleSwitch, { mocks, localVue, computed, store })
}
describe('with current user', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('.locale-menu').trigger('click')
@ -53,8 +79,30 @@ describe('LocaleSwitch.vue', () => {
deutschLanguageItem.trigger('click')
})
it("changes a user's locale", () => {
it("sets a user's locale", () => {
expect(mocks.$i18n.set).toHaveBeenCalledTimes(1)
})
it("updates the user's locale in the database", () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
})
describe('no current user', () => {
beforeEach(() => {
getters = {
'auth/user': () => {
return null
},
}
wrapper = Wrapper()
wrapper.find('.locale-menu').trigger('click')
deutschLanguageItem = wrapper.findAll('li').at(1)
deutschLanguageItem.trigger('click')
})
it('does not send a UpdateUser mutation', () => {
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
})
})

View File

@ -33,10 +33,12 @@
</template>
<script>
import gql from 'graphql-tag'
import Dropdown from '~/components/Dropdown'
import find from 'lodash/find'
import orderBy from 'lodash/orderBy'
import locales from '~/locales'
import { mapGetters, mapMutations } from 'vuex'
export default {
components: {
@ -64,15 +66,52 @@ export default {
})
return routes
},
...mapGetters({
currentUser: 'auth/user',
}),
},
methods: {
changeLanguage(locale, toggleMenu) {
this.$i18n.set(locale)
this.updateUserLocale()
toggleMenu()
},
matcher(locale) {
return locale === this.$i18n.locale()
},
...mapMutations({
setCurrentUser: 'auth/SET_USER',
}),
async updateUserLocale() {
if (!this.currentUser || !this.currentUser.id) return null
try {
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!, $locale: String) {
UpdateUser(id: $id, locale: $locale) {
id
locale
}
}
`,
variables: {
id: this.currentUser.id,
locale: this.$i18n.locale(),
},
update: (store, { data: { UpdateUser } }) => {
const { locale } = UpdateUser
this.setCurrentUser({
...this.currentUser,
locale,
})
},
})
this.$toast.success(this.$t('contribution.success'))
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -0,0 +1,75 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import { action } from '@storybook/addon-actions'
import Vuex from 'vuex'
import helpers from '~/storybook/helpers'
import LoginForm from './LoginForm.vue'
helpers.init()
const createStore = ({ loginSuccess }) => {
return new Vuex.Store({
modules: {
auth: {
namespaced: true,
state: () => ({
pending: false,
}),
mutations: {
SET_PENDING(state, pending) {
state.pending = pending
},
},
getters: {
pending(state) {
return !!state.pending
},
},
actions: {
async login({ commit, dispatch }, args) {
action('Vuex action `auth/login`')(args)
return new Promise((resolve, reject) => {
commit('SET_PENDING', true)
setTimeout(() => {
commit('SET_PENDING', false)
if (loginSuccess) {
resolve(loginSuccess)
} else {
reject(new Error('Login unsuccessful'))
}
}, 1000)
})
},
},
},
},
})
}
storiesOf('LoginForm', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('successful login', () => {
return {
components: { LoginForm },
store: createStore({ loginSuccess: true }),
methods: {
handleSuccess() {
action('Login successful!')()
},
},
template: `<login-form @success="handleSuccess"/>`,
}
})
.add('unsuccessful login', () => {
return {
components: { LoginForm },
store: createStore({ loginSuccess: false }),
methods: {
handleSuccess() {
action('Login successful!')()
},
},
template: `<login-form @success="handleSuccess"/>`,
}
})

View File

@ -0,0 +1,121 @@
<template>
<ds-container width="medium">
<ds-space margin="small">
<blockquote>
<p>{{ $t('quotes.african.quote') }}</p>
<b>- {{ $t('quotes.african.author') }}</b>
</blockquote>
</ds-space>
<ds-card class="login-card">
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<client-only>
<locale-switch class="login-locale-switch" offset="5" />
</client-only>
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
<img
class="login-image"
alt="Human Connection"
src="/img/sign-up/humanconnection.svg"
/>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<ds-space margin="small">
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfoHint')" target="_blank">
{{ $t('login.moreInfo') }}
</a>
</ds-space>
<ds-space margin="small">
<ds-text size="small">{{ $t('login.copy') }}</ds-text>
</ds-space>
<form :disabled="pending" @submit.prevent="onSubmit">
<ds-input
v-model="form.email"
:disabled="pending"
:placeholder="$t('login.email')"
type="email"
name="email"
icon="envelope"
/>
<ds-input
v-model="form.password"
:disabled="pending"
:placeholder="$t('login.password')"
icon="lock"
icon-right="question-circle"
name="password"
type="password"
/>
<ds-space margin-bottom="large">
<nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
</ds-space>
<ds-button
:loading="pending"
primary
fullwidth
name="submit"
type="submit"
icon="sign-in"
>
{{ $t('login.login') }}
</ds-button>
<ds-space margin-top="large" margin-bottom="x-small">
{{ $t('login.no-account') }}
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
</ds-space>
</form>
</ds-flex-item>
</ds-flex>
</ds-card>
</ds-container>
</template>
<script>
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch.vue'
export default {
components: {
LocaleSwitch,
},
data() {
return {
form: {
email: '',
password: '',
},
}
},
computed: {
pending() {
return this.$store.getters['auth/pending']
},
},
methods: {
async onSubmit() {
try {
await this.$store.dispatch('auth/login', { ...this.form })
this.$toast.success(this.$t('login.success'))
this.$emit('success')
} catch (err) {
this.$toast.error(this.$t('login.failure'))
}
},
},
}
</script>
<style lang="scss">
.login-image {
width: 90%;
max-width: 200px;
}
.login-card {
position: relative;
}
.login-locale-switch {
position: absolute;
top: 1em;
left: 1em;
}
</style>

View File

@ -20,7 +20,7 @@ describe('ReportModal.vue', () => {
id: 'c43',
}
mocks = {
$t: jest.fn(),
$t: jest.fn(a => a),
$filters: {
truncate: a => a,
},
@ -29,7 +29,9 @@ describe('ReportModal.vue', () => {
error: () => {},
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
mutate: jest.fn().mockResolvedValue({
data: {},
}),
},
}
})
@ -154,6 +156,7 @@ describe('ReportModal.vue', () => {
describe('click confirm button', () => {
beforeEach(() => {
wrapper.find('.ds-radio-option-label').trigger('click')
wrapper.find('button.confirm').trigger('click')
})

View File

@ -8,14 +8,36 @@
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="message" />
<template slot="footer">
<ds-button class="cancel" icon="close" @click="cancel">{{ $t('report.cancel') }}</ds-button>
<ds-radio
v-model="form.reasonCategory"
:schema="formSchema.reasonCategory"
:label="$t('report.reason.category.label')"
:options="form.reasonCategoryOptions"
labelProp="label"
/>
<ds-input
class="reason-description"
v-model="form.reasonDescription"
:schema="formSchema.reasonDescription"
:label="$t('report.reason.description.label')"
:placeholder="$t('report.reason.description.placeholder')"
type="textarea"
rows="5"
/>
<small class="smallTag">
{{ form.reasonDescription.length }}/{{ formSchema.reasonDescription.max }}
</small>
<ds-space />
<template #footer>
<ds-button class="cancel" icon="close" @click="cancel">
{{ $t('report.cancel') }}
</ds-button>
<ds-button
danger
class="confirm"
icon="exclamation-circle"
:disabled="!form.reasonCategory"
:loading="loading"
@click="confirm"
>
@ -26,8 +48,10 @@
</template>
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import { reportMutation } from '~/graphql/Moderation.js'
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
import validReport from '~/components/utils/ReportModal'
export default {
name: 'ReportModal',
@ -44,8 +68,21 @@ export default {
isOpen: true,
success: false,
loading: false,
form: {
reasonCategory: null,
reasonCategoryOptions: [],
reasonDescription: '',
},
}
},
created() {
this.form.reasonCategoryOptions = valuesReasonCategoryOptions.map(reasonCategory => {
return {
label: this.$t('report.reason.category.options.' + reasonCategory),
value: reasonCategory,
}
})
},
computed: {
title() {
return this.$t(`report.${this.type}.title`)
@ -54,6 +91,12 @@ export default {
const name = this.$filters.truncate(this.name, 30)
return this.$t(`report.${this.type}.message`, { name })
},
formSchema() {
const validReportSchema = validReport({ translate: this.$t })
return {
...validReportSchema.formSchema,
}
},
},
methods: {
async cancel() {
@ -65,52 +108,74 @@ export default {
}, 1000)
},
async confirm() {
const { reasonCategory, reasonDescription } = this.form
this.loading = true
try {
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
// await this.modalData.buttons.confirm.callback()
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {
report(id: $id) {
id
}
}
`,
variables: { id: this.id },
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
// await this.modalData.buttons.confirm.callback()
this.$apollo
.mutate({
mutation: reportMutation(),
variables: {
resourceId: this.id,
reasonCategory: reasonCategory.value,
reasonDescription,
},
})
this.success = true
this.$toast.success(this.$t('report.success'))
setTimeout(() => {
this.isOpen = false
.then(({ _data }) => {
this.success = true
this.$toast.success(this.$t('report.success'))
setTimeout(() => {
this.success = false
this.$emit('close')
}, 500)
}, 1500)
} catch (err) {
this.$emit('close')
this.success = false
switch (err.message) {
case 'GraphQL error: User':
this.$toast.error(this.$t('report.user.error'))
break
case 'GraphQL error: Post':
this.$toast.error(this.$t('report.contribution.error'))
break
case 'GraphQL error: Comment':
this.$toast.error(this.$t('report.comment.error'))
break
}
} finally {
this.loading = false
}
this.isOpen = false
setTimeout(() => {
this.success = false
this.$emit('close')
}, 500)
}, 1500)
this.loading = false
})
.catch(err => {
this.$emit('close')
this.success = false
switch (err.message) {
case 'GraphQL error: User':
this.$toast.error(this.$t('report.user.error'))
break
case 'GraphQL error: Post':
this.$toast.error(this.$t('report.contribution.error'))
break
case 'GraphQL error: Comment':
this.$toast.error(this.$t('report.comment.error'))
break
default:
this.$toast.error(err.message)
}
this.loading = false
})
},
},
}
</script>
<style lang="scss">
.ds-modal {
max-width: 600px !important;
}
.ds-radio-option:not(.ds-button) {
width: 100% !important;
}
.ds-radio-option-label {
margin: 5px 20px 5px 5px !important;
width: 100% !important;
}
.reason-description {
margin-top: $space-x-small !important;
margin-bottom: $space-xx-small !important;
}
.smallTag {
width: 100%;
position: relative;
left: 90%;
}
.hc-modal-success {
pointer-events: none;
position: absolute;

View File

@ -14,6 +14,10 @@
{{ $t('site.data-privacy') }}
</a>
<span>-</span>
<a href="https://faq.human-connection.org/" target="_blank">
{{ $t('site.faq') }}
</a>
<span>-</span>
<a href="https://github.com/Human-Connection/Human-Connection/releases" target="_blank">
{{ $t('site.changelog') }}
</a>

View File

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

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import HcPostCard from '~/components/PostCard'
import HcPostCard from './PostCard.vue'
import helpers from '~/storybook/helpers'
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>
<ds-card
: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 -->
<nuxt-link
@ -16,7 +16,8 @@
<client-only>
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
</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>
<ds-space margin-bottom="small" />
<!-- Post Title -->
@ -61,6 +62,8 @@
:resource="post"
:modalsData="menuModalsData"
:is-owner="isAuthor"
@pinPost="pinPost"
@unpinPost="unpinPost"
/>
</div>
</client-only>
@ -114,6 +117,9 @@ export default {
this.deletePostCallback,
)
},
isPinned() {
return this.post && this.post.pinnedBy
},
},
methods: {
async deletePostCallback() {
@ -127,6 +133,12 @@ export default {
this.$toast.error(err.message)
}
},
pinPost(post) {
this.$emit('pinPost', post)
},
unpinPost(post) {
this.$emit('unpinPost', post)
},
},
}
</script>
@ -167,4 +179,8 @@ export default {
text-indent: -999999px;
}
}
.post--pinned {
border: 1px solid $color-warning;
}
</style>

View File

@ -1,4 +1,5 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
import CreateUserAccount from './CreateUserAccount'
import { SignupVerificationMutation } from '~/graphql/Registration.js'
import Styleguide from '@human-connection/styleguide'
@ -24,6 +25,9 @@ describe('CreateUserAccount', () => {
loading: false,
mutate: jest.fn(),
},
$i18n: {
locale: () => 'en',
},
}
propsData = {}
stubs = {
@ -61,18 +65,14 @@ describe('CreateUserAccount', () => {
wrapper.find('input#password').setValue('hellopassword')
wrapper.find('textarea#about').setValue('Hello I am the `about` attribute')
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
wrapper.find('input#checkbox').setChecked()
wrapper.find('input#checkbox0').setChecked()
wrapper.find('input#checkbox1').setChecked()
wrapper.find('input#checkbox2').setChecked()
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
it('calls CreateUserAccount graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers data to backend', async () => {
await action()
const expected = expect.objectContaining({
@ -82,12 +82,19 @@ describe('CreateUserAccount', () => {
email: 'sixseven@example.org',
nonce: '666777',
password: 'hellopassword',
termsAndConditionsAgreedVersion: '0.0.2',
termsAndConditionsAgreedVersion: VERSION,
locale: 'en',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('calls CreateUserAccount graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case mutation resolves', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockResolvedValue({

View File

@ -64,22 +64,36 @@
<ds-text>
<input
id="checkbox"
id="checkbox0"
type="checkbox"
v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed"
/>
<label
for="checkbox"
for="checkbox0"
v-html="$t('termsAndConditions.termsAndConditionsConfirmed')"
></label>
</ds-text>
<ds-text>
<input id="checkbox1" type="checkbox" v-model="dataPrivacy" :checked="dataPrivacy" />
<label
for="checkbox1"
v-html="$t('components.registration.signup.form.data-privacy')"
></label>
</ds-text>
<ds-text>
<input id="checkbox2" type="checkbox" v-model="minimumAge" :checked="minimumAge" />
<label
for="checkbox2"
v-html="$t('components.registration.signup.form.minimum-age')"
></label>
</ds-text>
<ds-button
style="float: right;"
icon="check"
type="submit"
:loading="$apollo.loading"
:disabled="errors || !termsAndConditionsConfirmed"
:disabled="errors || !termsAndConditionsConfirmed || !dataPrivacy || !minimumAge"
primary
>
{{ $t('actions.save') }}
@ -129,6 +143,8 @@ export default {
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
termsAndConditionsConfirmed: false,
dataPrivacy: false,
minimumAge: false,
}
},
props: {
@ -140,10 +156,19 @@ export default {
const { name, password, about } = this.formData
const { email, nonce } = this
const termsAndConditionsAgreedVersion = VERSION
const locale = this.$i18n.locale()
try {
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
variables: { name, password, about, email, nonce, termsAndConditionsAgreedVersion },
variables: {
name,
password,
about,
email,
nonce,
termsAndConditionsAgreedVersion,
locale,
},
})
this.response = 'success'
setTimeout(() => {

View File

@ -46,4 +46,12 @@ export default {
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>

View File

@ -65,7 +65,7 @@ export default {
<div data-dz-thumbnail-bg></div>
</div>
</div>
`
`
},
verror(file, message) {
this.error = true
@ -100,8 +100,9 @@ export default {
thumbnailElement.appendChild(image)
// Remove the editor from view
editor.parentNode.removeChild(editor)
this.$emit('addTeaserImage', blob)
})
const croppedImageFile = new File([blob], file.name, { type: 'image/jpeg' })
this.$emit('addTeaserImage', croppedImageFile)
}, 'image/jpeg')
})
editor.appendChild(confirm)
@ -110,7 +111,7 @@ export default {
image.src = URL.createObjectURL(file)
editor.appendChild(image)
// Create Cropper.js and pass image
let cropper = new Cropper(image, { zoomable: false })
let cropper = new Cropper(image, { zoomable: false, autoCropArea: 1.0 })
},
dropzoneDrop() {
let cropOverlay = document.querySelectorAll('.crop-overlay')[0]

View File

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

View File

@ -0,0 +1,25 @@
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
export default function validReport({ translate }) {
return {
formSchema: {
reasonCategory: {
type: 'object',
required: true,
fields: {
value: {
type: 'enum',
enum: valuesReasonCategoryOptions,
required: true,
message: translate('report.reason.category.invalid'),
},
},
},
reasonDescription: {
type: 'string',
min: 0,
max: 200,
},
},
}
}

View File

@ -0,0 +1,36 @@
import validReport from './ReportModal'
import Schema from 'async-validator'
let translate
beforeEach(() => {
translate = jest.fn(() => 'Validation error')
})
describe('validReport', () => {
let validate = object => {
const { formSchema } = validReport({ translate })
const validator = new Schema(formSchema)
return validator.validate(object, { suppressWarning: true }).catch(({ errors }) => {
throw new Error(errors[0].message)
})
}
describe('reasonCategory', () => {
describe('invalid enum', () => {
it('rejects', async () => {
await expect(validate({ reasonCategory: { value: 'invalid_enum' } })).rejects.toThrow(
'Validation error',
)
})
})
describe('valid enum', () => {
it('resolves', async () => {
await expect(
validate({ reasonCategory: { value: 'discrimination_etc' } }),
).resolves.toBeUndefined()
})
})
})
})

View File

@ -1 +1,3 @@
export const COMMENT_MIN_LENGTH = 1
export const COMMENT_MAX_UNTRUNCATED_LENGTH = 300
export const COMMENT_TRUNCATE_TO_LENGTH = 180

View File

@ -0,0 +1,11 @@
// this list equals to enums in GraphQL schema file "backend/src/schema/types/type/REPORTED.gql"
export const valuesReasonCategoryOptions = [
'discrimination_etc',
'pornographic_content_links',
'glorific_trivia_of_cruel_inhuman_acts',
'doxing',
'intentional_intimidation_stalking_persecution',
'advert_products_services_commercial',
'criminal_behavior_violation_german_law',
'other',
]

View File

@ -1 +1,2 @@
export const VERSION = '0.0.2'
// please change also version in file "cypress/constants/terms-and-conditions-version.js"
export const VERSION = '0.0.3'

View File

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

View File

@ -1,13 +1,14 @@
import gql from 'graphql-tag'
export default app => {
export const reportListQuery = () => {
// no limit vor the moment like before: "reports(first: 20, orderBy: createdAt_desc)"
return gql`
query {
Report(first: 20, orderBy: createdAt_desc) {
id
description
type
reports(orderBy: createdAt_desc) {
createdAt
reasonCategory
reasonDescription
type
submitter {
id
slug
@ -79,3 +80,17 @@ export default app => {
}
`
}
export const reportMutation = () => {
return gql`
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
report(
resourceId: $resourceId
reasonCategory: $reasonCategory
reasonDescription: $reasonDescription
) {
type
}
}
`
}

View File

@ -50,6 +50,11 @@ export default () => {
content
contentExcerpt
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 = () => {
return gql`
query PostsEmotionsByCurrentUser($postId: ID!) {

View File

@ -7,6 +7,7 @@ export const SignupVerificationMutation = gql`
$password: String!
$about: String
$termsAndConditionsAgreedVersion: String!
$locale: String
) {
SignupVerification(
nonce: $nonce
@ -15,6 +16,7 @@ export const SignupVerificationMutation = gql`
password: $password
about: $about
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
locale: $locale
) {
id
name

View File

@ -153,3 +153,14 @@ export const checkSlugAvailableQuery = gql`
}
}
`
export const localeMutation = () => {
return gql`
mutation($id: ID!, $locale: String) {
UpdateUser(id: $id, locale: $locale) {
id
locale
}
}
`
}

View File

@ -30,6 +30,9 @@
"title": "Mach mit bei Human Connection!",
"form": {
"description": "Um loszulegen, gib deine E-Mail Adresse ein:",
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" > Nutzungsbedingungen</ds-text></a>zu.",
"data-privacy": "Ich habe die <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Datenschutzerklärung</ds-text></a> gelesen und verstanden",
"minimum-age": "Ich bin 18 Jahre oder älter.",
"invitation-code": "Dein Einladungscode lautet: <b>{code}</b>",
"errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
@ -47,6 +50,18 @@
}
}
},
"store": {
"posts": {
"orderBy": {
"newest": {
"label": "Neueste"
},
"oldest": {
"label": "Älteste"
}
}
}
},
"maintenance": {
"title": "Human Connection befindet sich in der Wartung",
"explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.",
@ -80,7 +95,7 @@
"imprint": "Impressum",
"data-privacy": "Datenschutz",
"termsAndConditions": "Nutzungsbedingungen",
"changelog": "Änderungen & Verlauf",
"changelog": "Änderungen",
"contact": "Kontakt",
"tribunal": "Registergericht",
"register": "Registernummer",
@ -90,11 +105,8 @@
"bank": "Bankverbindung",
"germany": "Deutschland",
"code-of-conduct": "Verhaltenscodex",
"back-to-login": "Zurück zur Anmeldung"
},
"sorting": {
"newest": "Neueste",
"oldest": "Älteste"
"back-to-login": "Zurück zur Anmeldung",
"faq": "FAQ"
},
"login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
@ -109,7 +121,8 @@
"moreInfoURL": "https://human-connection.org",
"moreInfoHint": "zur Präsentationsseite",
"hello": "Hallo",
"success": "Du bist eingeloggt!"
"success": "Du bist eingeloggt!",
"failure": "Fehlerhafte E-Mail-Adresse oder Passwort."
},
"editor": {
"placeholder": "Schreib etwas Inspirierendes …",
@ -351,6 +364,7 @@
},
"post": {
"name": "Beitrag",
"pinned": "Meldung",
"moreInfo": {
"name": "Mehr Info",
"title": "Mehr Informationen",
@ -364,7 +378,11 @@
},
"menu": {
"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": {
"submit": "Kommentiere",
@ -429,8 +447,11 @@
"reports": {
"empty": "Glückwunsch, es gibt nichts zu moderieren.",
"name": "Meldungen",
"submitter": "gemeldet von",
"disabledBy": "deaktiviert von"
"reasonCategory": "Kategorie",
"reasonDescription": "Beschreibung",
"createdAt": "Datum",
"submitter": "Gemeldet von",
"disabledBy": "Deaktiviert von"
}
},
"disable": {
@ -490,6 +511,27 @@
"type": "Kommentar",
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" melden möchtest?",
"error": "Du hast den Kommentar bereits gemeldet!"
},
"reason": {
"category": {
"label": "Wähle eine Kategorie:",
"placeholder": "Kategorie …",
"options": {
"discrimination_etc": "Diskriminierende Beiträge, Kommentare, Äußerungen oder Beleidigungen.",
"pornographic_content_links": "Das Posten oder Verlinken eindeutig pornografischen Materials.",
"glorific_trivia_of_cruel_inhuman_acts": "Verherrlichung oder Verharmlosung grausamer oder unmenschlicher Gewalttätigkeiten.",
"doxing": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen (\"Doxing\").",
"intentional_intimidation_stalking_persecution": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
"other": "Andere …"
},
"invalid": "Bitte wähle eine gültige Kategorie aus"
},
"description": {
"label": "Bitte erkläre: Warum möchtest du dies melden?",
"placeholder": "Zusätzliche Information …"
}
}
},
"followButton": {
@ -528,6 +570,7 @@
}
},
"contribution": {
"title": "Titel",
"newPost": "Erstelle einen neuen Beitrag",
"filterFollow": "Beiträge filtern von Usern denen ich folge",
"filterALL": "Alle Beiträge anzeigen",
@ -624,39 +667,37 @@
"termsAndConditionsNewConfirmText": "Bitte lies dir die neue Nutzungsbedingungen jetzt durch!",
"termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.",
"agree": "Ich stimme zu!",
"risk": {
"title": "Unfallgefahr",
"description": "Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen."
"terms-of-service": {
"title": "Nutzungsbedingungen",
"description": "Die folgenden Nutzungsbedingungen sind Basis für die Nutzung unseres Netzwerkes. Beim Registrieren musst Du sie anerkennen und wir werden Dich auch später über ggf. stattfindende Änderungen informieren. Das Human Connection Netzwerk wird in Deutschland betrieben und unterliegt daher deutschem Recht. Gerichtsstand ist Kirchheim / Teck. Zu Details schau in unser Impressum: <a href=\"https://human-connection.org/impressum\" target=\"_blank\" >https://human-connection.org/impressum</a> "
},
"use-and-license" : {
"title": "Nutzung und Lizenz",
"description": "Sind Inhalte, die Du bei uns einstellst, durch Rechte am geistigen Eigentum geschützt, erteilst Du uns eine nicht-exklusive, übertragbare, unterlizenzierbare und weltweite Lizenz für die Nutzung dieser Inhalte für die Bereitstellung in unserem Netzwerk. Diese Lizenz endet, sobald Du Deine Inhalte oder Deinen ganzen Account löscht. Bedenke, dass andere Deine Inhalte weiter teilen können und wir diese nicht löschen können."
},
"data-privacy": {
"title": "Du und deine Daten",
"description": "Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\" >Datenschutzerklärung</a>"
"privacy-statement" : {
"title": "Datenschutz",
"description": " Unser Netzwerk ist ein soziales Wissens- und Aktionsnetzwerk. Daher ist es uns besonders wichtig, dass möglichst viele Inhalte öffentlich zugänglich sind. Im Laufe der Entwicklung unseres Netzwerkes wird es mehr und mehr die Möglichkeit geben, über die Sichtbarkeit der selbst angegebenen bzw. persönlichen Daten zu entscheiden. Über diese neuen Features werden wir informieren. Ansonsten gilt, dass Du immer darüber nachdenken solltest, welche persönlichen Daten Du über Dich (oder andere) preisgibst. Dies gilt insbesondere für Inhalte von Posts und Kommentaren, da diese einen weitestgehend öffentlichen Charakter haben. Bei den Profilangaben wird es später Möglichkeiten geben, die Sichtbarkeit selbst einzuschränken. Teil der Nutzungsbedingungen ist unsere Datenschutzerklärung, die Dich über die einzelnen Datenverarbeitungen in unserem Netzwerk informiert: <a href=\"https://human-connection.org/datenschutz/#netzwerk\" target=\"_blank\"> https://human-connection.org/datenschutz/#netzwerk</a> bzw. <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\">https://human-connection.org/datenschutz/</a> Unsere Datenschutzerklärung wird je nach Gesetzeslage und Eigenschaften unseres Netzwerkes angepasst und ist jeweils in der aktuellen Version gültig."
},
"work-in-progress": {
"title": "Baustellen",
"description": "Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href=\"mailto:support@human-connection.org\" target=\"_blank\">support@human-connection.org</a>"
},
"code-of-conduct": {
"code-of-conduct" : {
"title": "Verhaltenscodex",
"description": "<a href=\"/code-of-conduct\">Die Verhaltensregeln </a> dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an"
"description": "Unser Verhaltenscodex dient als Handreiche für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an. <a href=\"https://alpha.human-connection.org/code-of-conduct\" target=\"_blank\"> https://alpha.human-connection.org/code-of-conduct</a>"
},
"moderation": {
"moderation" : {
"title": "Moderation",
"description": "Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!"
"description": "Bis unsere finanziellen Möglichkeiten uns erlauben, das Community-Moderationssystem zu implementieren, moderieren wir mit einem vereinfachten System und eigenen bzw. ggf. ehrenamtlichen Mitarbeitern. Wir schulen diese Moderatoren und aus diesem Grund treffen auch nur diese entsprechende Entscheidungen. Diese Moderatoren führen Ihre Tätigkeit anonym aus. Du kannst uns Beiträge, Kommentare und auch Nutzer melden (wenn diese zum Beispiel in ihrem Profil Angaben machen oder Bilder haben, die diese Nutzungsbedingungen verletzen). Wenn Du uns etwas meldest, kannst Du einen Meldegrund angeben und noch einen kurze Erläuterung mitgeben. Wir schauen uns dann das Gemeldete an und sanktionieren ggf., z.B. indem wir Beiträge, Kommentare oder Nutzer sperren. Du und auch der Betroffene erhält zum jetzigen Zeitpunkt von uns leider noch keine Rückmeldung, das ist aber in Planung. Unabhängig davon behalten wir uns prinzipiell Sanktionen vor aus Gründen, die unter Umständen nicht oder noch nicht in unserem Verhaltenscodex oder diesen Nutzungsbedingungen aufgeführt sind."
},
"fairness": {
"title": "Fairness",
"description": "Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href=\"mailto:info@human-connection.org\" target=\"_blank\" s> info@human-connection.org </a> Achtung: Viele Funktionen werden erst nach und nach eingebaut."
"errors-and-feedback" : {
"title": " Fehler und Feedback",
"description": "Wir geben uns größte Mühe, unser Netzwerk und die Daten sicher und verfügbar zu halten. Jedes neue Release der Software durchläuft sowohl automatisierte und händische Tests. Trotzdem kann es passieren, dass unvorhergesehene Fehler auftreten. Daher sind wir dankbar für jeden gemeldeten Fehler. Du kannst von Dir entdeckte Fehler gerne per E-Mail an den Support mitteilen: support@human-connection.org"
},
"questions": {
"title": "Fragen",
"description": "Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href=\"https://human-connection.org/events-und-news/\" target=\"_blank\" >https://human-connection.org/veranstaltungen/ </a>"
"help-and-questions" : {
"title": "Hilfe und Fragen",
"description": "Für Hilfe und Fragen haben wir für Dich eine umfassende Sammlung an immer wieder gestellten Fragen bzw. Antworten (FAQ) zusammengestellt. Du findest diese hier: <a href=\"https://support.human-connection.org/kb/\" target=\"_blank\" > https://support.human-connection.org/kb/ </a>"
},
"human-connection": {
"title": "Von Menschen für Menschen:",
"description": "Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href=\"https://human-connection.org/\" target=\"_blank\"> https://human-connection.org </a>"
},
"have-fun": "Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎",
"closing": "Herzlichst <br><br> Euer Human Connection Team"
"addition" : {
"title": "Zusätzliche machen wir regelmäßig Veranstaltungen, wo Du auch Eindrücke wiedergeben und Fragen stellen kannst. Du findest eine aktuelle Übersicht hier:",
"description": "<a href=\"https://human-connection.org/veranstaltungen/\" target=\"_blank\" > https://human-connection.org/veranstaltungen/ </a>"
}
}
}

View File

@ -4,9 +4,9 @@
"request": {
"title": "Reset your password",
"form": {
"description": "A password reset email will be sent to the given email address.",
"submit": "Request email",
"submitted": "An email with further instructions has been sent to <b>{email}</b>"
"description": "A password reset e-mail will be sent to the given e-mail address.",
"submit": "Request e-mail",
"submitted": "An e-mail with further instructions has been sent to <b>{email}</b>"
}
},
"change-password": {
@ -30,10 +30,13 @@
"unavailable": "Unfortunately, public registration of user accounts is not available right now on this server.",
"title": "Join Human Connection!",
"form": {
"description": "To get started, enter your email address:",
"description": "To get started, enter your e-mail address:",
"terms-and-condition": "I confirm to the <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" > Terms and conditions</ds-text></a>.",
"data-privacy": " I have read and understood the <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Privacy Statement</ds-text></a> ",
"minimum-age": "I'm 18 years or older.",
"invitation-code": "Your invitation code is: <b>{code}</b>",
"errors": {
"email-exists": "There is already a user account with this email address!",
"email-exists": "There is already a user account with this e-mail address!",
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
},
"submit": "Create an account",
@ -48,10 +51,22 @@
}
}
},
"store": {
"posts": {
"orderBy": {
"newest": {
"label": "Newest"
},
"oldest": {
"label": "Oldest"
}
}
}
},
"maintenance": {
"title": "Human Connection is under maintenance",
"explanation": "At the moment we are doing some scheduled maintenance, please try again later.",
"questions": "Any Questions or concerns, send an email to"
"questions": "Any Questions or concerns, send an e-mail to"
},
"index": {
"no-results": "No contributions found.",
@ -81,7 +96,7 @@
"imprint": "Imprint",
"termsAndConditions": "Terms and conditions",
"data-privacy": "Data privacy",
"changelog": "Changes & History",
"changelog": "Changes",
"contact": "Contact",
"tribunal": "Registry court",
"register": "Registry number",
@ -91,17 +106,14 @@
"bank": "bank account",
"germany": "Germany",
"code-of-conduct": "Code of Conduct",
"back-to-login": "Back to login page"
},
"sorting": {
"newest": "Newest",
"oldest": "Oldest"
"back-to-login": "Back to login page",
"faq": "FAQ"
},
"login": {
"copy": "If you already have a human-connection account, please login.",
"login": "Login",
"logout": "Logout",
"email": "Your Email",
"email": "Your E-mail",
"password": "Your Password",
"forgotPassword": "Forgot Password?",
"no-account": "Don't have an account?",
@ -110,7 +122,8 @@
"moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "to the presentation page",
"hello": "Hello",
"success": "You are logged in!"
"success": "You are logged in!",
"failure": "Incorrect email address or password."
},
"editor": {
"placeholder": "Leave your inspirational thoughts …",
@ -149,8 +162,8 @@
},
"invites": {
"title": "Invite somebody to Human Connection!",
"description": "Enter thier email address for invitation.",
"emailPlaceholder": "Email to invite"
"description": "Enter thier e-mail address for invitation.",
"emailPlaceholder": "E-mail to invite"
}
},
"notifications": {
@ -179,23 +192,23 @@
},
"email": {
"validation": {
"same-email": "This is your current email address"
"same-email": "This is your current e-mail address"
},
"name": "Your email",
"labelEmail": "Change your email address",
"labelNewEmail": "New email Address",
"name": "Your e-mail",
"labelEmail": "Change your e-mail address",
"labelNewEmail": "New e-mail Address",
"labelNonce": "Enter your code",
"success": "A new email address has been registered.",
"submitted": "An email to verify your address has been sent to <b>{email}</b>.",
"change-successful": "Your email address has been changed successfully.",
"success": "A new e-mail address has been registered.",
"submitted": "An e-mail to verify your address has been sent to <b>{email}</b>.",
"change-successful": "Your e-mail address has been changed successfully.",
"verification-error": {
"message": "Your email could not be changed.",
"message": "Your e-mail could not be changed.",
"explanation": "This can have different causes:",
"reason": {
"invalid-nonce": "Is the confirmation code invalid?",
"no-email-request": "Are you certain that you requested a change of your email address?"
"no-email-request": "Are you certain that you requested a change of your e-mail address?"
},
"support": "If the problem persists, please contact us by email at"
"support": "If the problem persists, please contact us by e-mail at"
}
},
"validation": {
@ -309,7 +322,7 @@
"users": {
"name": "Users",
"form": {
"placeholder": "email, name or description"
"placeholder": "e-mail, name or description"
},
"table": {
"columns": {
@ -352,6 +365,7 @@
},
"post": {
"name": "Post",
"pinned": "Announcement",
"moreInfo": {
"name": "More info",
"title": "More information",
@ -365,7 +379,11 @@
},
"menu": {
"edit": "Edit Post",
"delete": "Delete Post"
"delete": "Delete Post",
"pin": "Pin post",
"pinnedSuccessfully": "Post pinned successfully!",
"unpin": "Unpin post",
"unpinnedSuccessfully": "Post unpinned successfully!"
},
"comment": {
"submit": "Comment",
@ -412,7 +430,7 @@
"loading": "loading",
"reportContent": "Report",
"validations": {
"email": "must be a valid email address",
"email": "must be a valid e-mail address",
"url": "must be a valid URL"
}
},
@ -430,8 +448,11 @@
"reports": {
"empty": "Congratulations, nothing to moderate.",
"name": "Reports",
"submitter": "reported by",
"disabledBy": "disabled by"
"reasonCategory": "Category",
"reasonDescription": "Description",
"createdAt": "Date",
"submitter": "Reported by",
"disabledBy": "Disabled by"
}
},
"disable": {
@ -491,6 +512,27 @@
"type": "Comment",
"message": "Do you really want to report the comment from \"<b>{name}</b>\"?",
"error": "You have already reported the comment!"
},
"reason": {
"category": {
"label": "Select a category:",
"placeholder": "Category …",
"options": {
"discrimination_etc": "Discriminatory posts, comments, utterances or insults.",
"pornographic_content_links": "Posting or linking of clearly pornographic material.",
"glorific_trivia_of_cruel_inhuman_acts": "Glorification or trivialization of cruel or inhuman acts of violence.",
"doxing": "The disclosure of others' personal information without their consent or threat there of (\"doxing\").",
"intentional_intimidation_stalking_persecution": "Intentional intimidation, stalking or persecution.",
"advert_products_services_commercial": "Advertising products and services with commercial intent.",
"criminal_behavior_violation_german_law": "Criminal behavior or violation of German law.",
"other": "Other …"
},
"invalid": "Please select a valid category"
},
"description": {
"label": "Please explain: Why you like to report this?",
"placeholder": "Additional information …"
}
}
},
"followButton": {
@ -529,6 +571,7 @@
}
},
"contribution": {
"title": "Title",
"newPost": "Create a new Post",
"filterFollow": "Filter contributions from users I follow",
"filterALL": "View all contributions",
@ -625,39 +668,41 @@
"termsAndConditionsNewConfirmText": "Please read the new terms of use now!",
"termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.",
"agree": "I agree!",
"risk": {
"title": "Risk of accident",
"description": "This is a test version! All data, your profile and the server can be completely destroyed, wiped out, lost, burnt and eventually synchronised near Alpha Centauri at any time. Use on your own risk. Commercial effects are not likely though."
"terms-of-service": {
"title": "Terms of Service",
"description": "The following terms of use form the basis for the use of our network. When you register, you must accept them and we will inform you later about any changes that may take place. The Human Connection Network is operated in Germany and is therefore subject to German law. Place of jurisdiction is Kirchheim / Teck. For details see our imprint: <a href=\"https://human-connection.org/imprint\" target=\"_blank\" >https://human-connection.org/imprint</a> "
},
"use-and-license" : {
"title": "Use and License",
"description": "If any content you post to us is protected by intellectual property rights, you grant us a non-exclusive, transferable, sublicensable, worldwide license to use such content for posting to our network. This license expires when you delete your content or your entire account. Remember that others may share your content and we cannot delete it."
},
"data-privacy": {
"title": "You and your data",
"description": "Please note that the content of the alpha version will be used for public web presentations etc. but we are sure, this is in your interest. Avoid real names and use anonymous profile pictures without your face. You can find more information in our <a href=\"https://human-connection.org/en/privacy/\" target=\"_blank\">data privacy policy</a>"
"privacy-statement" : {
"title": "Privacy Statement",
"description": "Our network is a social knowledge and action network. It is therefore particularly important to us that as much content as possible is publicly accessible. In the course of the development of our network there will be more and more the possibility to decide about the visibility of the personal data. We will inform you about these new features. Otherwise, you should always think about which personal data you disclose about yourself (or others). This applies in particular to the content of posts and comments, as these have a largely public character. Later there will be possibilities to limit the visibility of your profile. Part of the terms of service is our privacy statement, which informs you about the individual data processing operations in our network: <a href=\"https://human-connection.org/datenschutz/#netzwerk\" target=\"_blank\"> https://human-connection.org/datenschutz/#netzwerk</a> bzw. <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\">https://human-connection.org/datenschutz/</a> Our privacy statement is adapted to the legal situation and characteristics of our network and is always valid in the most current version."
},
"work-in-progress": {
"title": "Work in progress",
"description": "This is still a test version. Please excuse if some applications are not working, blocking, irritating, displayed falsely or not able to be clicked on. Please report faults and bugs! <a href=\"mailto:support@human-connection.org\" target=\"_blank\">mailto:support@human-connection.org</a>"
"code-of-conduct" : {
"title": "Code of Conduct",
"description": "Our code of conduct serves as a handbook for personal appearance and interaction with each other. Whoever is active as a user in the Human Connection network, writes articles, comments or makes contact with other users, even outside the network, acknowledges these rules of conduct as binding. <a href=\"https://alpha.human-connection.org/code-of-conduct\" target=\"_blank\"> https://alpha.human-connection.org/code-of-conduct</a>"
},
"code-of-conduct": {
"title": "Code of conduct",
"description": "The <a href=\"/code-of-conduct\">code of conduct</a> serves as guiding principles for our personal appearance and interaction with one another. Anyone who is active as a user in the Human Connection Network, writes articles, comments or contacts other users, including those outside the network, acknowledges these rules of conduct as binding."
},
"moderation": {
"moderation" : {
"title": "Moderation",
"description": "As long as there is no community moderation-system in operation, a rainbow colored unicorn decides, if you are physically and mentally stable enough to operate our test version. The unicorn can delete you from the alpha version at any time. So be so kind and leave rainbow food!"
"description": "Until our financial possibilities allow us to implement the community moderation system, we moderate with a simplified system and with our own or possibly volunteer staff. We train these moderators and for this reason only they make the appropriate decisions. These moderators carry out their work anonymously. You can report posts, comments and users to us (for example, if they provide information in their profile or have images that violate these Terms of Use). If you report something to us, you can give us a reason and a short explanation. We will then take a look at what you have reported and sanction you if necessary, e.g. by blocking contributions, comments or users. Unfortunately, you and the person concerned will not receive any feedback from us at this time, but this is in the planning stage. Irrespective of this, we reserve the right to impose sanctions in principle for reasons that may not or not yet be listed in our Code of Conduct or these terms of service."
},
"fairness": {
"title": "Fairness",
"description": "If, against all expectations, our alpha version is not to your liking, we return your monthly payment within the first two months. Please send a mail to: <a href=\"mailto:info@human-connection.org\" target=\"_blank\"> info@human-connection.org </a> Note that more features are added on a regular basis."
"errors-and-feedback" : {
"title": "Errors and Feedback",
"description": "We make every effort to keep our network and data secure and available. Each new release of the software goes through both automated and manual testing. However, unforeseen errors may occur. Therefore, we are grateful for any reported bugs. You are welcome to report any bugs you discover by emailing Support at support@human-connection.org"
},
"questions": {
"title": "Questions",
"description": "You can find the dates and links to our zoom-rooms here: <a href=\"https://human-connection.org/en/events/\" target=\"_blank\" >https://human-connection.org/en/events/ </a>"
"help-and-questions" : {
"title": "Help and Questions",
"description": "For help and questions we have compiled a comprehensive collection of frequently asked questions and answers (FAQ) for you. You can find them here: <a href=\"https://support.human-connection.org/kb/\" target=\"_blank\" > https://support.human-connection.org/kb/ </a>"
},
"human-connection": {
"title": "By humans for humans",
"description": "Please help us to get new donators for Human Connection, so the network can take off as soon as possible. <a href=\"https://human-connection.org/\" target=\"_blank\"> https://human-connection.org </a>"
},
"have-fun": "Now have fun with the alpha version of Human Connection! For the first universal peace. ♥︎",
"closing": "Thank you very much <br> <br> your Human Connection Team"
"addition" : {
"title": "In addition, we regularly hold events where you can also share your impressions and ask questions. You can find a current overview here:",
"description": "<a href=\"https://human-connection.org/events/\" target=\"_blank\" > https://human-connection.org/events/ </a>"
}
}
}

View File

@ -12,7 +12,7 @@
"scripts": {
"dev": "nuxt",
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn run dev",
"storybook": "start-storybook -p 3002 -c storybook/",
"storybook": "start-storybook -p 3002 -s ./static -c storybook/",
"build": "nuxt build",
"start": "nuxt start",
"generate:maintenance": "nuxt generate -c nuxt.config.maintenance.js",
@ -53,8 +53,8 @@
},
"dependencies": {
"@human-connection/styleguide": "0.5.21",
"@nuxtjs/apollo": "^4.0.0-rc15",
"@nuxtjs/axios": "~5.6.0",
"@nuxtjs/apollo": "^4.0.0-rc16",
"@nuxtjs/axios": "~5.8.0",
"@nuxtjs/dotenv": "~1.4.1",
"@nuxtjs/pwa": "^3.0.0-beta.19",
"@nuxtjs/sentry": "^3.0.1",
@ -65,21 +65,21 @@
"cookie-universal-nuxt": "~2.0.18",
"cropperjs": "^1.5.5",
"cross-env": "~6.0.3",
"date-fns": "2.4.1",
"date-fns": "2.6.0",
"express": "~4.17.1",
"graphql": "~14.5.8",
"isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0",
"node-fetch": "^2.6.0",
"nuxt": "~2.10.1",
"nuxt": "~2.10.2",
"nuxt-dropzone": "^1.0.4",
"nuxt-env": "~0.1.0",
"stack-utils": "^1.0.2",
"string-hash": "^1.1.3",
"tippy.js": "^4.3.5",
"tiptap": "~1.26.3",
"tiptap-extensions": "~1.28.3",
"tiptap-extensions": "~1.28.4",
"trunc-html": "^1.1.2",
"v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13",
@ -95,14 +95,14 @@
"@babel/core": "~7.6.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "~7.6.3",
"@storybook/addon-a11y": "^5.2.4",
"@storybook/addon-actions": "^5.2.4",
"@storybook/vue": "~5.2.4",
"@vue/cli-shared-utils": "~3.12.0",
"@storybook/addon-a11y": "^5.2.5",
"@storybook/addon-actions": "^5.2.5",
"@storybook/vue": "~5.2.5",
"@vue/cli-shared-utils": "~4.0.5",
"@vue/eslint-config-prettier": "~5.0.0",
"@vue/server-test-utils": "~1.0.0-beta.29",
"@vue/test-utils": "~1.0.0-beta.29",
"async-validator": "^3.1.0",
"async-validator": "^3.2.0",
"babel-core": "~7.0.0-bridge.0",
"babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0",
@ -111,22 +111,23 @@
"core-js": "~2.6.10",
"css-loader": "~3.2.0",
"eslint": "~5.16.0",
"eslint-config-prettier": "~6.4.0",
"eslint-config-prettier": "~6.5.0",
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~3.0.2",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.19.0",
"eslint-plugin-jest": "~23.0.2",
"eslint-plugin-node": "~10.0.0",
"eslint-plugin-prettier": "~3.1.1",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.1",
"eslint-plugin-vue": "~5.2.3",
"faker": "^4.1.0",
"flush-promises": "^1.0.2",
"fuse.js": "^3.4.5",
"identity-obj-proxy": "^3.0.0",
"jest": "~24.9.0",
"mutation-observer": "^1.0.3",
"node-sass": "~4.12.0",
"node-sass": "~4.13.0",
"prettier": "~1.18.2",
"sass-loader": "~8.0.0",
"style-loader": "~0.23.1",

View File

@ -24,15 +24,37 @@ describe('PostIndex', () => {
let Wrapper
let store
let mocks
let mutations
beforeEach(() => {
mutations = {
'posts/SELECT_ORDER': jest.fn(),
}
store = new Vuex.Store({
getters: {
'postsFilter/postsFilter': () => ({}),
'posts/filter': () => ({}),
'posts/orderOptions': () => () => [
{
key: 'store.posts.orderBy.oldest.label',
label: 'store.posts.orderBy.oldest.label',
icon: 'sort-amount-asc',
value: 'createdAt_asc',
},
{
key: 'store.posts.orderBy.newest.label',
label: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
value: 'createdAt_desc',
},
],
'posts/selectedOrder': () => () => 'createdAt_desc',
'posts/orderIcon': () => 'sort-amount-desc',
'posts/orderBy': () => 'createdAt_desc',
'auth/user': () => {
return { id: 'u23' }
},
},
mutations,
})
mocks = {
$t: key => key,
@ -103,12 +125,12 @@ describe('PostIndex', () => {
})
})
it('sets the post in the store when there are posts', () => {
it('calls store when using order by menu', () => {
wrapper
.findAll('li')
.at(0)
.trigger('click')
expect(wrapper.vm.sorting).toEqual('createdAt_desc')
expect(mutations['posts/SELECT_ORDER']).toHaveBeenCalledWith({}, 'createdAt_asc')
})
it('updates offset when a user clicks on the load more button', () => {

View File

@ -10,8 +10,7 @@
v-model="selected"
:options="sortingOptions"
size="large"
v-bind:icon-right="sortingIcon"
@input="toggleOnlySorting"
:icon-right="sortingIcon"
></ds-select>
</div>
</ds-grid-item>
@ -21,6 +20,8 @@
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@removePostFromList="deletePost"
@pinPost="pinPost"
@unpinPost="unpinPost"
/>
</masonry-grid-item>
</template>
@ -58,12 +59,13 @@
<script>
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import HcEmpty from '~/components/Empty'
import HcPostCard from '~/components/PostCard'
import HcPostCard from '~/components/PostCard/PostCard.vue'
import HcLoadMore from '~/components/LoadMore.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { mapGetters } from 'vuex'
import { mapGetters, mapMutations } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations'
export default {
components: {
@ -83,30 +85,29 @@ export default {
offset: 0,
pageSize: 12,
hashtag,
placeholder: this.$t('sorting.newest'),
selected: this.$t('sorting.newest'),
sortingIcon: 'sort-amount-desc',
sorting: 'createdAt_desc',
sortingOptions: [
{
label: this.$t('sorting.newest'),
value: 'Newest',
icons: 'sort-amount-desc',
order: 'createdAt_desc',
},
{
label: this.$t('sorting.oldest'),
value: 'Oldest',
icons: 'sort-amount-asc',
order: 'createdAt_asc',
},
],
}
},
computed: {
...mapGetters({
postsFilter: 'postsFilter/postsFilter',
postsFilter: 'posts/filter',
orderOptions: 'posts/orderOptions',
orderBy: 'posts/orderBy',
selectedOrder: 'posts/selectedOrder',
sortingIcon: 'posts/orderIcon',
}),
selected: {
get() {
return this.selectedOrder(this)
},
set({ value }) {
this.offset = 0
this.posts = []
this.selectOrder(value)
},
},
sortingOptions() {
return this.orderOptions(this)
},
finalFilters() {
let filter = this.postsFilter
if (this.hashtag) {
@ -122,12 +123,9 @@ export default {
},
},
methods: {
toggleOnlySorting(x) {
this.offset = 0
this.posts = []
this.sortingIcon = x.icons
this.sorting = x.order
},
...mapMutations({
selectOrder: 'posts/SELECT_ORDER',
}),
clearSearch() {
this.$router.push({ path: '/' })
this.hashtag = null
@ -148,7 +146,7 @@ export default {
offset: this.offset,
filter: this.finalFilters,
first: this.pageSize,
orderBy: this.sorting,
orderBy: ['pinned_asc', this.orderBy],
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult || fetchMoreResult.Post.length < this.pageSize) {
@ -166,6 +164,37 @@ export default {
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: {
Post: {
@ -176,7 +205,7 @@ export default {
return {
filter: this.finalFilters,
first: this.pageSize,
orderBy: this.sorting,
orderBy: ['pinned_asc', this.orderBy],
offset: 0,
}
},

View File

@ -1,138 +1,27 @@
<template>
<transition name="fade" appear>
<ds-container v-if="ready" width="medium">
<ds-space margin="small">
<blockquote>
<p>{{ $t('quotes.african.quote') }}</p>
<b>- {{ $t('quotes.african.author') }}</b>
</blockquote>
</ds-space>
<ds-card class="login-card">
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<client-only>
<locale-switch class="login-locale-switch" offset="5" />
</client-only>
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
<img
class="login-image"
alt="Human Connection"
src="/img/sign-up/humanconnection.svg"
/>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<ds-space margin="small">
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfoHint')" target="_blank">
{{ $t('login.moreInfo') }}
</a>
</ds-space>
<ds-space margin="small">
<ds-text size="small">{{ $t('login.copy') }}</ds-text>
</ds-space>
<form :disabled="pending" @submit.prevent="onSubmit">
<ds-input
v-model="form.email"
:disabled="pending"
:placeholder="$t('login.email')"
type="email"
name="email"
icon="envelope"
/>
<ds-input
v-model="form.password"
:disabled="pending"
:placeholder="$t('login.password')"
icon="lock"
icon-right="question-circle"
name="password"
type="password"
/>
<ds-space margin-bottom="large">
<nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
</ds-space>
<ds-button
:loading="pending"
primary
fullwidth
name="submit"
type="submit"
icon="sign-in"
>
{{ $t('login.login') }}
</ds-button>
<ds-space margin-top="large" margin-bottom="x-small">
{{ $t('login.no-account') }}
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
</ds-space>
</form>
</ds-flex-item>
</ds-flex>
</ds-card>
</ds-container>
<login-form @success="handleSuccess" />
</transition>
</template>
<script>
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import LoginForm from '~/components/LoginForm/LoginForm.vue'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
export default {
layout: 'no-header',
components: {
LocaleSwitch,
},
data() {
return {
ready: false,
form: {
email: '',
password: '',
},
}
},
computed: {
pending() {
return this.$store.getters['auth/pending']
},
LoginForm,
},
asyncData({ store, redirect }) {
if (store.getters['auth/user'].termsAndConditionsAgreedVersion === VERSION) {
redirect('/')
}
},
mounted() {
setTimeout(() => {
// NOTE: quick fix for jumping flexbox implementation
// will be fixed in a future update of the styleguide
this.ready = true
}, 50)
},
methods: {
async onSubmit() {
try {
await this.$store.dispatch('auth/login', { ...this.form })
this.$toast.success(this.$t('login.success'))
this.$router.replace(this.$route.query.path || '/')
} catch (err) {
this.$toast.error(err.message)
}
handleSuccess() {
this.$router.replace(this.$route.query.path || '/')
},
},
}
</script>
<style lang="scss">
.login-image {
width: 90%;
max-width: 200px;
}
.login-card {
position: relative;
}
.login-locale-switch {
position: absolute;
top: 1em;
left: 1em;
}
</style>

View File

@ -1,8 +1,29 @@
<template>
<ds-card space="small">
<ds-heading tag="h3">{{ $t('moderation.reports.name') }}</ds-heading>
<ds-table v-if="Report && Report.length" :data="Report" :fields="fields" condensed>
<template slot="name" slot-scope="scope">
<ds-table v-if="reports && reports.length" :data="reports" :fields="fields" condensed>
<!-- Icon -->
<template slot="type" slot-scope="scope">
<ds-text color="soft">
<ds-icon
v-if="scope.row.type === 'Post'"
v-tooltip="{ content: $t('report.contribution.type'), placement: 'right' }"
name="bookmark"
/>
<ds-icon
v-else-if="scope.row.type === 'Comment'"
v-tooltip="{ content: $t('report.comment.type'), placement: 'right' }"
name="comments"
/>
<ds-icon
v-else-if="scope.row.type === 'User'"
v-tooltip="{ content: $t('report.user.type'), placement: 'right' }"
name="user"
/>
</ds-text>
</template>
<!-- reported user or content -->
<template slot="reportedUserContent" slot-scope="scope">
<div v-if="scope.row.type === 'Post'">
<nuxt-link
:to="{
@ -42,25 +63,15 @@
</nuxt-link>
</div>
</template>
<template slot="type" slot-scope="scope">
<ds-text color="soft">
<ds-icon
v-if="scope.row.type === 'Post'"
v-tooltip="{ content: $t('report.contribution.type'), placement: 'right' }"
name="bookmark"
/>
<ds-icon
v-else-if="scope.row.type === 'Comment'"
v-tooltip="{ content: $t('report.comment.type'), placement: 'right' }"
name="comments"
/>
<ds-icon
v-else-if="scope.row.type === 'User'"
v-tooltip="{ content: $t('report.user.type'), placement: 'right' }"
name="user"
/>
</ds-text>
<!-- reasonCategory -->
<template slot="reasonCategory" slot-scope="scope">
{{ $t('report.reason.category.options.' + scope.row.reasonCategory) }}
</template>
<!-- reasonDescription -->
<template slot="reasonDescription" slot-scope="scope">
{{ scope.row.reasonDescription }}
</template>
<!-- submitter -->
<template slot="submitter" slot-scope="scope">
<nuxt-link
:to="{
@ -71,6 +82,15 @@
{{ scope.row.submitter.name }}
</nuxt-link>
</template>
<!-- createdAt -->
<template slot="createdAt" slot-scope="scope">
<ds-text size="small">
<client-only>
<hc-relative-date-time :date-time="scope.row.createdAt" />
</client-only>
</ds-text>
</template>
<!-- disabledBy -->
<template slot="disabledBy" slot-scope="scope">
<nuxt-link
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
@ -102,6 +122,7 @@
>
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<b v-else></b>
</template>
</ds-table>
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
@ -110,31 +131,36 @@
<script>
import HcEmpty from '~/components/Empty.vue'
import query from '~/graphql/ModerationListQuery.js'
import HcRelativeDateTime from '~/components/RelativeDateTime'
import { reportListQuery } from '~/graphql/Moderation.js'
export default {
components: {
HcEmpty,
HcRelativeDateTime,
},
data() {
return {
Report: [],
reports: [],
}
},
computed: {
fields() {
return {
type: ' ',
name: ' ',
reportedUserContent: ' ',
reasonCategory: this.$t('moderation.reports.reasonCategory'),
reasonDescription: this.$t('moderation.reports.reasonDescription'),
submitter: this.$t('moderation.reports.submitter'),
createdAt: this.$t('moderation.reports.createdAt'),
disabledBy: this.$t('moderation.reports.disabledBy'),
// actions: ' '
}
},
},
apollo: {
Report: {
query,
reports: {
query: reportListQuery(),
fetchPolicy: 'cache-and-network',
},
},

View File

@ -31,6 +31,9 @@ describe('PostSlug', () => {
$filters: {
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
$router: {
history: {

View File

@ -18,6 +18,8 @@
:resource="post"
:modalsData="menuModalsData"
:is-owner="isAuthor(post.author ? post.author.id : null)"
@pinPost="pinPost"
@unpinPost="unpinPost"
/>
</client-only>
<ds-space margin-bottom="small" />
@ -68,9 +70,13 @@
</ds-space>
<!-- Comments -->
<ds-section slot="footer">
<hc-comment-list :post="post" />
<hc-comment-list
:post="post"
:routeHash="$route.hash"
@toggleNewCommentForm="toggleNewCommentForm"
/>
<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-card>
</transition>
@ -88,6 +94,7 @@ import HcCommentList from '~/components/CommentList/CommentList'
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
import PostQuery from '~/graphql/PostQuery'
import HcEmotions from '~/components/Emotions/Emotions'
import PostMutations from '~/graphql/PostMutations'
export default {
name: 'PostSlug',
@ -116,6 +123,7 @@ export default {
post: null,
ready: false,
title: 'loading',
showNewCommentForm: true,
}
},
watch: {
@ -156,6 +164,31 @@ export default {
async createComment(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: {
Post: {

View File

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

View File

@ -172,7 +172,7 @@
<ds-grid-item class="profile-top-navigation" :row-span="3" column-span="fullWidth">
<ds-card class="ds-tab-nav">
<ul class="Tabs">
<li class="Tabs__tab Tab pointer" :class="{ active: tabActive === 'post' }">
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'post' }">
<a @click="handleTab('post')">
<ds-space margin="small">
<client-only placeholder="Loading...">
@ -183,7 +183,7 @@
</ds-space>
</a>
</li>
<li class="Tabs__tab Tab pointer" :class="{ active: tabActive === 'comment' }">
<li class="Tabs__tab pointer" :class="{ active: tabActive === 'comment' }">
<a @click="handleTab('comment')">
<ds-space margin="small">
<client-only placeholder="Loading...">
@ -194,7 +194,11 @@
</ds-space>
</a>
</li>
<li class="Tabs__tab Tab pointer" :class="{ active: tabActive === 'shout' }">
<li
class="Tabs__tab pointer"
:class="{ active: tabActive === 'shout' }"
v-if="myProfile"
>
<a @click="handleTab('shout')">
<ds-space margin="small">
<client-only placeholder="Loading...">
@ -205,7 +209,6 @@
</ds-space>
</a>
</li>
<li class="Tabs__presentation-slider" role="presentation"></li>
</ul>
</ds-card>
</ds-grid-item>
@ -234,6 +237,8 @@
:post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }"
@removePostFromList="removePostFromList"
@pinPost="pinPost"
@unpinPost="unpinPost"
/>
</masonry-grid-item>
</template>
@ -268,7 +273,7 @@
<script>
import uniqBy from 'lodash/uniqBy'
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 HcCountTo from '~/components/CountTo.vue'
import HcBadges from '~/components/Badges.vue'
@ -279,9 +284,10 @@ import HcUpload from '~/components/Upload'
import HcAvatar from '~/components/Avatar/Avatar.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { filterPosts } from '~/graphql/PostQuery'
import { profilePagePosts } from '~/graphql/PostQuery'
import UserQuery from '~/graphql/User'
import { Block, Unblock } from '~/graphql/settings/BlockedUsers'
import PostMutations from '~/graphql/PostMutations'
const tabToFilterMapping = ({ tab, id }) => {
return {
@ -373,7 +379,7 @@ export default {
return uniqBy(items, field)
},
showMoreContributions() {
const { Post: PostQuery } = this.$apollo.queries
const { profilePagePosts: PostQuery } = this.$apollo.queries
if (!PostQuery) return // seems this can be undefined on subpages
this.offset += this.pageSize
@ -385,11 +391,14 @@ export default {
orderBy: 'createdAt_desc',
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult || fetchMoreResult.Post.length < this.pageSize) {
if (!fetchMoreResult || fetchMoreResult.profilePagePosts.length < this.pageSize) {
this.hasMore = false
}
const result = Object.assign({}, previousResult, {
Post: [...previousResult.Post, ...fetchMoreResult.Post],
profilePagePosts: [
...previousResult.profilePagePosts,
...fetchMoreResult.profilePagePosts,
],
})
return result
},
@ -404,13 +413,39 @@ export default {
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.Post.refetch()
this.$apollo.queries.profilePagePosts.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()
this.$apollo.queries.profilePagePosts.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.profilePagePosts.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.profilePagePosts.refetch()
})
.catch(error => this.$toast.error(error.message))
},
optimisticFollow({ followedByCurrentUser }) {
/*
@ -435,9 +470,9 @@ export default {
},
},
apollo: {
Post: {
profilePagePosts: {
query() {
return filterPosts(this.$i18n)
return profilePagePosts(this.$i18n)
},
variables() {
return {
@ -447,8 +482,8 @@ export default {
orderBy: 'createdAt_desc',
}
},
update({ Post }) {
this.posts = Post
update({ profilePagePosts }) {
this.posts = profilePagePosts
},
fetchPolicy: 'cache-and-network',
},
@ -469,51 +504,28 @@ export default {
.pointer {
cursor: pointer;
}
.Tab {
border-collapse: collapse;
padding-bottom: 5px;
}
.Tab:hover {
border-bottom: 2px solid #c9c6ce;
}
.Tabs {
position: relative;
background-color: #fff;
height: 100%;
&:after {
content: ' ';
display: table;
clear: both;
}
display: flex;
margin: 0;
padding: 0;
list-style: none;
&__tab {
float: left;
width: 33.333%;
text-align: center;
height: 100%;
flex-grow: 1;
&:first-child.active ~ .Tabs__presentation-slider {
left: 0;
&:hover {
border-bottom: 2px solid #c9c6ce;
}
&:nth-child(2).active ~ .Tabs__presentation-slider {
left: 33.333%;
&.active {
border-bottom: 2px solid #17b53f;
}
&:nth-child(3).active ~ .Tabs__presentation-slider {
left: calc(33.333% * 2);
}
}
&__presentation-slider {
position: absolute;
bottom: 0;
left: 0;
width: 33.333%;
height: 2px;
background-color: #17b53f;
transition: left 0.25s;
}
}
.profile-avatar.ds-avatar {

View File

@ -11,11 +11,6 @@
<p v-html="$t(`termsAndConditions.${section}.description`)" />
</li>
</ol>
<p>{{ $t('termsAndConditions.have-fun') }}</p>
<br />
<p>
<strong v-html="$t('termsAndConditions.closing')" />
</p>
</div>
</ds-container>
</div>
@ -31,15 +26,16 @@ export default {
},
data() {
return {
// if you change terms and conditions please change also version in file "webapp/constants/terms-and-conditions-version.js"
sections: [
'risk',
'data-privacy',
'work-in-progress',
'terms-of-service',
'use-and-license',
'privacy-statement',
'code-of-conduct',
'moderation',
'fairness',
'questions',
'human-connection',
'errors-and-feedback',
'help-and-questions',
'addition',
],
}
},

View File

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

View File

@ -7,11 +7,25 @@ import clone from 'lodash/clone'
const defaultFilter = {}
const orderOptions = {
createdAt_asc: {
value: 'createdAt_asc',
key: 'store.posts.orderBy.oldest.label',
icon: 'sort-amount-asc',
},
createdAt_desc: {
value: 'createdAt_desc',
key: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
},
}
export const state = () => {
return {
filter: {
...defaultFilter,
},
order: orderOptions['createdAt_desc'],
}
}
@ -46,13 +60,16 @@ export const mutations = {
if (isEmpty(get(filter, 'emotions_some.emotion_in'))) delete filter.emotions_some
state.filter = filter
},
SELECT_ORDER(state, value) {
state.order = orderOptions[value]
},
}
export const getters = {
isActive(state) {
return !isEqual(state.filter, defaultFilter)
},
postsFilter(state) {
filter(state) {
return state.filter
},
filteredCategoryIds(state) {
@ -64,4 +81,23 @@ export const getters = {
filteredByEmotions(state) {
return get(state.filter, 'emotions_some.emotion_in') || []
},
orderOptions: state => ({ $t }) =>
Object.values(orderOptions).map(option => {
return {
...option,
label: $t(option.key),
}
}),
selectedOrder: state => ({ $t }) => {
return {
...state.order,
label: $t(state.order.key),
}
},
orderBy(state) {
return state.order.value
},
orderIcon(state) {
return state.order.icon
},
}

View File

@ -1,7 +1,7 @@
import { getters, mutations } from './postsFilter.js'
import { getters, mutations } from './posts.js'
let state
let testAction
let testMutation
describe('getters', () => {
describe('isActive', () => {
@ -25,10 +25,10 @@ describe('getters', () => {
})
})
describe('postsFilter', () => {
describe('filter', () => {
it('returns filter', () => {
state = { filter: { author: { followedBy_some: { id: 7 } } } }
expect(getters.postsFilter(state)).toEqual({ author: { followedBy_some: { id: 7 } } })
expect(getters.filter(state)).toEqual({ author: { followedBy_some: { id: 7 } } })
})
})
@ -67,14 +67,48 @@ describe('getters', () => {
expect(getters.filteredByEmotions(state)).toEqual([])
})
})
describe('orderByOptions', () => {
it('returns all options regardless of current state', () => {
const $t = jest.fn(t => t)
expect(getters.orderOptions()({ $t })).toEqual([
{
key: 'store.posts.orderBy.oldest.label',
label: 'store.posts.orderBy.oldest.label',
icon: 'sort-amount-asc',
value: 'createdAt_asc',
},
{
key: 'store.posts.orderBy.newest.label',
label: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
value: 'createdAt_desc',
},
])
})
})
describe('orderBy', () => {
it('returns value for graphql query', () => {
state = {
order: {
key: 'store.posts.orderBy.newest.label',
label: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
value: 'createdAt_desc',
},
}
expect(getters.orderBy(state)).toEqual('createdAt_desc')
})
})
})
describe('mutations', () => {
describe('RESET_CATEGORIES', () => {
beforeEach(() => {
testAction = categoryId => {
testMutation = categoryId => {
mutations.RESET_CATEGORIES(state, categoryId)
return getters.postsFilter(state)
return getters.filter(state)
}
})
it('resets the categories filter', () => {
@ -84,37 +118,37 @@ describe('mutations', () => {
categories_some: { id_in: [23] },
},
}
expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
expect(testMutation(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
})
})
describe('TOGGLE_CATEGORY', () => {
beforeEach(() => {
testAction = categoryId => {
testMutation = categoryId => {
mutations.TOGGLE_CATEGORY(state, categoryId)
return getters.postsFilter(state)
return getters.filter(state)
}
})
it('creates category filter if empty', () => {
state = { filter: {} }
expect(testAction(23)).toEqual({ categories_some: { id_in: [23] } })
expect(testMutation(23)).toEqual({ categories_some: { id_in: [23] } })
})
it('adds category id not present', () => {
state = { filter: { categories_some: { id_in: [24] } } }
expect(testAction(23)).toEqual({ categories_some: { id_in: [24, 23] } })
expect(testMutation(23)).toEqual({ categories_some: { id_in: [24, 23] } })
})
it('removes category id if present', () => {
state = { filter: { categories_some: { id_in: [23, 24] } } }
const result = testAction(23)
const result = testMutation(23)
expect(result).toEqual({ categories_some: { id_in: [24] } })
})
it('removes category filter if empty', () => {
state = { filter: { categories_some: { id_in: [23] } } }
expect(testAction(23)).toEqual({})
expect(testMutation(23)).toEqual({})
})
it('does not get in the way of other filters', () => {
@ -124,15 +158,15 @@ describe('mutations', () => {
categories_some: { id_in: [23] },
},
}
expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
expect(testMutation(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
})
})
describe('TOGGLE_FILTER_BY_FOLLOWED', () => {
beforeEach(() => {
testAction = userId => {
testMutation = userId => {
mutations.TOGGLE_FILTER_BY_FOLLOWED(state, userId)
return getters.postsFilter(state)
return getters.filter(state)
}
})
@ -142,7 +176,7 @@ describe('mutations', () => {
})
it('attaches the id of the current user to the filter object', () => {
expect(testAction(4711)).toEqual({ author: { followedBy_some: { id: 4711 } } })
expect(testMutation(4711)).toEqual({ author: { followedBy_some: { id: 4711 } } })
})
})
@ -152,8 +186,23 @@ describe('mutations', () => {
})
it('remove the id of the current user from the filter object', () => {
expect(testAction(4711)).toEqual({})
expect(testMutation(4711)).toEqual({})
})
})
})
describe('SELECT_ORDER', () => {
beforeEach(() => {
testMutation = key => {
mutations.SELECT_ORDER(state, key)
return getters.orderBy(state)
}
})
it('switches the currently selected order', () => {
state = {
// does not matter
}
expect(testMutation('createdAt_asc')).toEqual('createdAt_asc')
})
})
})

View File

@ -3,7 +3,9 @@ import Vuex from 'vuex'
import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js'
import Styleguide from '@human-connection/styleguide'
import Filters from '~/plugins/vue-filters'
import IziToast from '~/plugins/izi-toast'
import layout from './layout.vue'
import locales from '~/locales/index.js'
import '~/plugins/v-tooltip'
@ -12,10 +14,13 @@ const helpers = {
Vue.use(Vuex)
Vue.use(Styleguide)
Vue.use(Filters)
Vue.use(IziToast)
Vue.use(vuexI18n.plugin, helpers.store)
Vue.i18n.add('en', require('~/locales/en.json'))
Vue.i18n.add('de', require('~/locales/de.json'))
locales.forEach(({ code }) => {
Vue.i18n.add(code, require(`~/locales/${code}.json`))
})
Vue.i18n.set('en')
Vue.i18n.fallback('en')
@ -35,14 +40,6 @@ const helpers = {
},
},
},
editor: {
namespaced: true,
getters: {
placeholder(state) {
return 'Leave your inspirational thoughts ...'
},
},
},
},
}),
layout(storyFn) {

File diff suppressed because it is too large Load Diff

170
yarn.lock
View File

@ -891,6 +891,11 @@
dependencies:
"@hapi/hoek" "8.x.x"
"@types/sizzle@2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
JSONStream@^1.0.3:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@ -1120,6 +1125,20 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
auto-changelog@^1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/auto-changelog/-/auto-changelog-1.16.2.tgz#4b08b7cbd07fdbd9139c6e06ea0b704db3f5485c"
integrity sha512-QL7zKH5FBBHz6tECO8CjZ8LpdevVSJoDskDzzPeoB9Bfe6LyXmRzXUoTIFKJXXdVaX8ydMpDO9Oa8ihZ4Au+CA==
dependencies:
commander "^3.0.1"
core-js "^3.2.1"
handlebars "^4.1.2"
lodash.uniqby "^4.7.0"
node-fetch "^2.6.0"
parse-github-url "^1.0.2"
regenerator-runtime "^0.13.3"
semver "^6.3.0"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -1649,10 +1668,15 @@ commander@2.15.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
commander@^2.9.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
commander@^2.9.0, commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
common-tags@1.8.0:
version "1.8.0"
@ -1726,6 +1750,11 @@ core-js@^2.4.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
core-js@^3.2.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.3.3.tgz#b7048d3c6c1a52b5fe55a729c1d4ccdffe0891bb"
integrity sha512-0xmD4vUJRY8nfLyV9zcpC17FtSie5STXzw+HyYw2t8IIvmDnbq7RJUULECCo+NstpJtwK9kx8S+898iyqgeUow==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -1895,23 +1924,24 @@ cypress-cucumber-preprocessor@^1.16.2:
js-string-escape "^1.0.1"
through "^2.3.8"
cypress-file-upload@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.3.4.tgz#cbeb8a7a07150a1c60f2873666979e48b6335070"
integrity sha512-kfdrQ6cWBw82G7EbHSqZJiOQWRh9cGz9K1mjePNZax00gBL0qOdRTjfkAnR2vEmmJyCfnN3efryjfhFeLrGWVw==
cypress-file-upload@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.4.0.tgz#f066853357994ed7b64e0ea35920d3d85273914e"
integrity sha512-BY7jrpOPFEGcGBzkTReEjwQ59+O3u2SH2OleXdnDCuWIPHjbDx7haXukyAFd906JsI4Z2zXPiKrUVFHZc96eFA==
cypress-plugin-retries@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.3.0.tgz#a2c1f49dce69b521cbb5ce3ab1a3a25acf41f08f"
integrity sha512-s2STd3vVeoIeKmdOvDhmWicARxK3cu7xF02MhH120wycUhdtR0SbAbo+zmcNnHquyshccE6cv17DfNvPOV7Rog==
cypress@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30"
integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg==
cypress@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.5.0.tgz#e188bc8f48782953f6865d8830a4dc342334b81c"
integrity sha512-I1iSReD2C8CTP6s4BvQky4gEqHBnKLmhBIqFyCUZdj6BQ6ZDxGnmIbQPM5g79E2iP60KTIbTK99ZPSDVtsNUUg==
dependencies:
"@cypress/listr-verbose-renderer" "0.4.1"
"@cypress/xvfb" "1.2.4"
"@types/sizzle" "2.3.2"
arch "2.1.1"
bluebird "3.5.0"
cachedir "1.3.0"
@ -1938,6 +1968,7 @@ cypress@^3.4.1:
request-progress "3.0.0"
supports-color "5.5.0"
tmp "0.1.0"
untildify "3.0.3"
url "0.11.0"
yauzl "2.10.0"
@ -1965,10 +1996,10 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.4.1.tgz#b53f9bb65ae6bd9239437035710e01cf383b625e"
integrity sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==
date-fns@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.6.0.tgz#a5bc82e6a4c3995ae124b0ba1a71aec7b8cbd666"
integrity sha512-F55YxqRdEfP/eYQmQjLN798v0AwLjmZ8nMBjdQvNwEE3N/zWVrlkkqT+9seBlPlsbkybG4JmWg3Ee3dIV9BcGQ==
date-now@^0.1.4:
version "0.1.4"
@ -2113,10 +2144,10 @@ dotenv@^4.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=
dotenv@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.1.0.tgz#d811e178652bfb8a1e593c6dd704ec7e90d85ea2"
integrity sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==
dotenv@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
version "0.1.4"
@ -2359,7 +2390,7 @@ extsprintf@^1.2.0:
faker@Marak/faker.js#master:
version "4.1.0"
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/10bfb9f467b0ac2b8912ffc15690b50ef3244f09"
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/9fd8d7d37b398842d0784a116a340f7aa6afb89b"
fast-deep-equal@^2.0.1:
version "2.0.1"
@ -2576,6 +2607,17 @@ graphql-request@^1.8.2:
dependencies:
cross-fetch "2.2.2"
handlebars@^4.1.2:
version "4.4.5"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.5.tgz#1b1f94f9bfe7379adda86a8b73fb570265a0dddd"
integrity sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg==
dependencies:
neo-async "^2.6.0"
optimist "^0.6.1"
source-map "^0.6.1"
optionalDependencies:
uglify-js "^3.1.4"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -3245,6 +3287,11 @@ lodash.once@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.uniqby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=
lodash@4.17.15, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.4:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
@ -3376,6 +3423,11 @@ minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
minipass@^2.2.1, minipass@^2.3.4:
version "2.3.5"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
@ -3482,7 +3534,12 @@ needle@^2.2.1:
iconv-lite "^0.4.4"
sax "^1.2.4"
neo4j-driver@^1.7.5, neo4j-driver@^1.7.6:
neo-async@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
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==
@ -3491,14 +3548,14 @@ neo4j-driver@^1.7.5, neo4j-driver@^1.7.6:
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neode@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.3.tgz#a539830cce6f6e4825462f6cb03f2969a0003f1b"
integrity sha512-pArHG1hD2kVwrzLlz6B1+IgdOJRQj/BgR6KzH6DlVzSA6geoZRe68fbpvmOJtzyPU7iuUYxXVk87PpPM1A7dlg==
neode@^0.3.6:
version "0.3.6"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.6.tgz#7daf791eff6d170e52c338ea2e5cca6fdc6bfbe3"
integrity sha512-jCskCPobtHpsIIYQD72h5lRjMJEX70KwIeqgpt1VOLI+d1zJZvUlDkcOKgarAW0fmwtHIrPOP6mLPe5G/ZG9+g==
dependencies:
"@hapi/joi" "^15.1.0"
dotenv "^4.0.0"
neo4j-driver "^1.7.5"
neo4j-driver "^1.7.6"
uuid "^3.3.2"
next-tick@^1.0.0:
@ -3523,10 +3580,10 @@ node-fetch@2.1.2:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
node-fetch@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==
node-fetch@^2.2.0, node-fetch@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-pre-gyp@^0.10.0:
version "0.10.3"
@ -3696,6 +3753,14 @@ onetime@^1.0.0:
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
dependencies:
minimist "~0.0.1"
wordwrap "~0.0.2"
ora@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
@ -3777,6 +3842,11 @@ parse-asn1@^5.0.0:
pbkdf2 "^3.0.3"
safe-buffer "^5.1.1"
parse-github-url@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-github-url/-/parse-github-url-1.0.2.tgz#242d3b65cbcdda14bb50439e3242acf6971db395"
integrity sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@ -4040,10 +4110,10 @@ regenerator-runtime@^0.12.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
regenerator-transform@^0.14.0:
version "0.14.1"
@ -4228,16 +4298,11 @@ seed-random@~2.2.0:
resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"
integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=
"semver@2 || 3 || 4 || 5":
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
version "5.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@ -4379,6 +4444,11 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
spdx-correct@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
@ -4775,6 +4845,14 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
uglify-js@^3.1.4:
version "3.6.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.4.tgz#88cc880c6ed5cf9868fdfa0760654e7bed463f1d"
integrity sha512-9Yc2i881pF4BPGhjteCXQNaXx1DCwm3dtOyBaG2hitHjLWOczw/ki8vD1bqyT3u6K0Ms/FpCShkmfg+FtlOfYA==
dependencies:
commander "~2.20.3"
source-map "~0.6.1"
umd@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
@ -4842,6 +4920,11 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
untildify@3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==
upath@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068"
@ -4965,6 +5048,11 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"