mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
Merge branch 'master' into 1746-Blur_explicit_Image_Content
This commit is contained in:
commit
ed617f8c83
29
CHANGELOG.md
29
CHANGELOG.md
@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.1.13](https://github.com/Human-Connection/Human-Connection/compare/v0.1.12...v0.1.13)
|
||||
|
||||
> 13 December 2019
|
||||
|
||||
- Update de.json [`#2492`](https://github.com/Human-Connection/Human-Connection/pull/2492)
|
||||
- Fix broken scroll behaviour on index and profile page [`#2487`](https://github.com/Human-Connection/Human-Connection/pull/2487)
|
||||
- Lokalise: Translations update [`#2503`](https://github.com/Human-Connection/Human-Connection/pull/2503)
|
||||
- build(deps): bump node from 13.1.0-alpine to 13.3.0-alpine in /webapp [`#2454`](https://github.com/Human-Connection/Human-Connection/pull/2454)
|
||||
- Lokalise: Translations update [`#2485`](https://github.com/Human-Connection/Human-Connection/pull/2485)
|
||||
- build(deps-dev): bump css-loader from 3.3.0 to 3.3.2 in /webapp [`#2505`](https://github.com/Human-Connection/Human-Connection/pull/2505)
|
||||
- build(deps-dev): bump cypress from 3.7.0 to 3.8.0 [`#2504`](https://github.com/Human-Connection/Human-Connection/pull/2504)
|
||||
- Favor transaction functions [`#2433`](https://github.com/Human-Connection/Human-Connection/pull/2433)
|
||||
- build(deps): bump nodemailer from 6.4.1 to 6.4.2 in /backend [`#2500`](https://github.com/Human-Connection/Human-Connection/pull/2500)
|
||||
- Update en.json [`#2491`](https://github.com/Human-Connection/Human-Connection/pull/2491)
|
||||
- Update es.json [`#2493`](https://github.com/Human-Connection/Human-Connection/pull/2493)
|
||||
- Update fr.json [`#2494`](https://github.com/Human-Connection/Human-Connection/pull/2494)
|
||||
- Update it.json [`#2496`](https://github.com/Human-Connection/Human-Connection/pull/2496)
|
||||
- build(deps-dev): bump nodemon from 2.0.1 to 2.0.2 in /backend [`#2499`](https://github.com/Human-Connection/Human-Connection/pull/2499)
|
||||
- build(deps): bump @nuxtjs/apollo from 4.0.0-rc18 to 4.0.0-rc19 in /webapp [`#2498`](https://github.com/Human-Connection/Human-Connection/pull/2498)
|
||||
- build(deps): bump neo4j-graphql-js from 2.10.0 to 2.10.1 in /backend [`#2497`](https://github.com/Human-Connection/Human-Connection/pull/2497)
|
||||
- Fix docker manifest on Travis CI [`#2488`](https://github.com/Human-Connection/Human-Connection/pull/2488)
|
||||
- build(deps-dev): bump @babel/core from 7.7.4 to 7.7.5 [`#2453`](https://github.com/Human-Connection/Human-Connection/pull/2453)
|
||||
- build(deps-dev): bump cypress-file-upload from 3.5.0 to 3.5.1 [`#2489`](https://github.com/Human-Connection/Human-Connection/pull/2489)
|
||||
- build(deps): bump cookie-universal-nuxt from 2.0.19 to 2.1.0 in /webapp [`#2490`](https://github.com/Human-Connection/Human-Connection/pull/2490)
|
||||
- Update to version 0.1.12 [`#2483`](https://github.com/Human-Connection/Human-Connection/pull/2483)
|
||||
- Lokalise: update of locale/ru.json [`60b3035`](https://github.com/Human-Connection/Human-Connection/commit/60b3035a3d475cb481130c6fe94f2901711a4053)
|
||||
- Write test/refactor tests/resolvers/middleware [`d375ebe`](https://github.com/Human-Connection/Human-Connection/commit/d375ebe7d90e3251b17f59ffba8fb1470923ebe8)
|
||||
- Fix this annoying bug with a tested helper [`e24d803`](https://github.com/Human-Connection/Human-Connection/commit/e24d8035b13040dc29f5f9cb033de8c1a401ac34)
|
||||
|
||||
#### [v0.1.12](https://github.com/Human-Connection/Human-Connection/compare/v0.1.10...v0.1.12)
|
||||
|
||||
> 10 December 2019
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"@hapi/joi": "^16.1.8",
|
||||
"@sentry/node": "^5.10.2",
|
||||
"apollo-cache-inmemory": "~1.6.3",
|
||||
"apollo-client": "~2.6.4",
|
||||
"apollo-client": "~2.6.8",
|
||||
"apollo-link-context": "~1.0.19",
|
||||
"apollo-link-http": "~1.5.16",
|
||||
"apollo-server": "~2.9.13",
|
||||
@ -63,15 +63,15 @@
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.7.3",
|
||||
"metascraper": "^5.8.9",
|
||||
"metascraper-audio": "^5.8.7",
|
||||
"metascraper-audio": "^5.8.10",
|
||||
"metascraper-author": "^5.8.7",
|
||||
"metascraper-clearbit-logo": "^5.3.0",
|
||||
"metascraper-date": "^5.8.7",
|
||||
"metascraper-description": "^5.8.7",
|
||||
"metascraper-description": "^5.8.10",
|
||||
"metascraper-image": "^5.8.7",
|
||||
"metascraper-lang": "^5.8.9",
|
||||
"metascraper-lang-detector": "^4.10.2",
|
||||
"metascraper-logo": "^5.8.7",
|
||||
"metascraper-logo": "^5.8.10",
|
||||
"metascraper-publisher": "^5.8.7",
|
||||
"metascraper-soundcloud": "^5.8.9",
|
||||
"metascraper-title": "^5.8.7",
|
||||
@ -81,10 +81,10 @@
|
||||
"minimatch": "^3.0.4",
|
||||
"mustache": "^3.1.0",
|
||||
"neo4j-driver": "~1.7.6",
|
||||
"neo4j-graphql-js": "^2.10.0",
|
||||
"neo4j-graphql-js": "^2.10.1",
|
||||
"neode": "^0.3.3",
|
||||
"node-fetch": "~2.6.0",
|
||||
"nodemailer": "^6.4.1",
|
||||
"nodemailer": "^6.4.2",
|
||||
"nodemailer-html-to-text": "^3.1.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.0",
|
||||
@ -115,11 +115,11 @@
|
||||
"eslint-plugin-import": "~2.19.1",
|
||||
"eslint-plugin-jest": "~23.1.1",
|
||||
"eslint-plugin-node": "~10.0.0",
|
||||
"eslint-plugin-prettier": "~3.1.1",
|
||||
"eslint-plugin-prettier": "~3.1.2",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"jest": "~24.9.0",
|
||||
"nodemon": "~2.0.1",
|
||||
"nodemon": "~2.0.2",
|
||||
"prettier": "~1.19.1",
|
||||
"supertest": "~4.0.2"
|
||||
}
|
||||
|
||||
@ -11,27 +11,28 @@ export default async (driver, authorizationHeader) => {
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
const query = `
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
SET user.lastActiveAt = toString(datetime())
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`
|
||||
const session = driver.session()
|
||||
let result
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateUserLastActiveTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||
SET user.lastActiveAt = toString(datetime())
|
||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||
LIMIT 1
|
||||
`,
|
||||
{ id },
|
||||
)
|
||||
return updateUserLastActiveTransactionResponse.records.map(record => record.get('user'))
|
||||
})
|
||||
try {
|
||||
result = await session.run(query, { id })
|
||||
const [currentUser] = await writeTxResultPromise
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (!currentUser) return null
|
||||
return {
|
||||
token,
|
||||
...currentUser,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,30 +2,23 @@ import extractHashtags from '../hashtags/extractHashtags'
|
||||
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p: Post { id: $postId})
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (t: Tag { id: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
const session = context.driver.session()
|
||||
|
||||
try {
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
await session.writeTransaction(txc => {
|
||||
return txc.run(
|
||||
`
|
||||
MATCH (post:Post { id: $postId})
|
||||
OPTIONAL MATCH (post)-[previousRelations:TAGGED]->(tag:Tag)
|
||||
DELETE previousRelations
|
||||
WITH post
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (tag:Tag {id: tagName, disabled: false, deleted: false })
|
||||
MERGE (post)-[:TAGGED]->(tag)
|
||||
RETURN post, tag
|
||||
`,
|
||||
{ postId, hashtags },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -7,7 +7,7 @@ import sluggify from './sluggifyMiddleware'
|
||||
import excerpt from './excerptMiddleware'
|
||||
import xss from './xssMiddleware'
|
||||
import permissions from './permissionsMiddleware'
|
||||
import user from './userMiddleware'
|
||||
import user from './user/userMiddleware'
|
||||
import includedFields from './includedFieldsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
import validation from './validation/validationMiddleware'
|
||||
|
||||
@ -38,7 +38,7 @@ const createLocation = async (session, mapboxData) => {
|
||||
lng: mapboxData.center && mapboxData.center.length ? mapboxData.center[1] : null,
|
||||
}
|
||||
|
||||
let query =
|
||||
let mutation =
|
||||
'MERGE (l:Location {id: $id}) ' +
|
||||
'SET l.name = $nameEN, ' +
|
||||
'l.nameEN = $nameEN, ' +
|
||||
@ -53,19 +53,23 @@ const createLocation = async (session, mapboxData) => {
|
||||
'l.type = $type'
|
||||
|
||||
if (data.lat && data.lng) {
|
||||
query += ', l.lat = $lat, l.lng = $lng'
|
||||
mutation += ', l.lat = $lat, l.lng = $lng'
|
||||
}
|
||||
query += ' RETURN l.id'
|
||||
mutation += ' RETURN l.id'
|
||||
|
||||
await session.run(query, data)
|
||||
session.close()
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(mutation, data)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const createOrUpdateLocations = async (userId, locationName, driver) => {
|
||||
if (isEmpty(locationName)) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
|
||||
locationName,
|
||||
@ -106,33 +110,44 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
|
||||
if (data.context) {
|
||||
await asyncForEach(data.context, async ctx => {
|
||||
await createLocation(session, ctx)
|
||||
|
||||
await session.run(
|
||||
'MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId}) ' +
|
||||
'MERGE (child)<-[:IS_IN]-(parent) ' +
|
||||
'RETURN child.id, parent.id',
|
||||
{
|
||||
parentId: parent.id,
|
||||
childId: ctx.id,
|
||||
},
|
||||
)
|
||||
|
||||
parent = ctx
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (parent:Location {id: $parentId}), (child:Location {id: $childId})
|
||||
MERGE (child)<-[:IS_IN]-(parent)
|
||||
RETURN child.id, parent.id
|
||||
`,
|
||||
{
|
||||
parentId: parent.id,
|
||||
childId: ctx.id,
|
||||
},
|
||||
)
|
||||
})
|
||||
parent = ctx
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
// delete all current locations from user
|
||||
await session.run('MATCH (u:User {id: $userId})-[r:IS_IN]->(l:Location) DETACH DELETE r', {
|
||||
userId: userId,
|
||||
})
|
||||
// connect user with location
|
||||
await session.run(
|
||||
'MATCH (u:User {id: $userId}), (l:Location {id: $locationId}) MERGE (u)-[:IS_IN]->(l) RETURN l.id, u.id',
|
||||
{
|
||||
userId: userId,
|
||||
locationId: data.id,
|
||||
},
|
||||
)
|
||||
session.close()
|
||||
// delete all current locations from user and add new location
|
||||
try {
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})-[relationship:IS_IN]->(location:Location)
|
||||
DETACH DELETE relationship
|
||||
WITH user
|
||||
MATCH (location:Location {id: $locationId})
|
||||
MERGE (user)-[:IS_IN]->(location)
|
||||
RETURN location.id, user.id
|
||||
`,
|
||||
{ userId: userId, locationId: data.id },
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default createOrUpdateLocations
|
||||
|
||||
@ -1,164 +1,121 @@
|
||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||
import { validateNotifyUsers } from '../validation/validationMiddleware'
|
||||
|
||||
const postAuthorOfComment = async (comment, { context }) => {
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
if (post && idsOfUsers && idsOfUsers.length)
|
||||
await notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { content } = args
|
||||
let idsOfUsers = extractMentionedUsers(content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
|
||||
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
|
||||
if (idsOfUsers && idsOfUsers.length)
|
||||
await notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
if (context.user.id !== postAuthor.id)
|
||||
await notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context)
|
||||
return comment
|
||||
}
|
||||
|
||||
const postAuthorOfComment = async (commentId, { context }) => {
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
let postAuthorId
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
postAuthorId = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (author:User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN author { .id } as authorId
|
||||
`,
|
||||
{ commentId },
|
||||
)
|
||||
})
|
||||
return postAuthorId.records.map(record => record.get('authorId'))
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
return postAuthor
|
||||
}
|
||||
|
||||
const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
if (!idsOfUsers.length) return
|
||||
|
||||
// Checked here, because it does not go through GraphQL checks at all in this file.
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) {
|
||||
throw new Error('Notification reason is not allowed!')
|
||||
}
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
|
||||
let cypher
|
||||
const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
||||
await validateNotifyUsers(label, reason)
|
||||
let mentionedCypher
|
||||
switch (reason) {
|
||||
case 'mentioned_in_post': {
|
||||
cypher = `
|
||||
mentionedCypher = `
|
||||
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'mentioned_in_comment': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'commented_on_post': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (author)<-[:BLOCKED]-(user)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
mentionedCypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
`
|
||||
break
|
||||
}
|
||||
}
|
||||
mentionedCypher += `
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
await session.run(cypher, {
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(mentionedCypher, { id, idsOfUsers, reason })
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => {
|
||||
await validateNotifyUsers(label, reason)
|
||||
const session = context.driver.session()
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (post) {
|
||||
await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
let idsOfUsers = extractMentionedUsers(args.content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const postAuthor = await postAuthorOfComment(comment, { context })
|
||||
idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id)
|
||||
|
||||
await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
const handleCreateComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
let result
|
||||
try {
|
||||
result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
try {
|
||||
await session.writeTransaction(async transaction => {
|
||||
await transaction.run(
|
||||
`
|
||||
MATCH (postAuthor:User {id: $postAuthorId})-[:WROTE]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User)
|
||||
WHERE NOT (postAuthor)-[:BLOCKED]-(commenter)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(postAuthor)
|
||||
SET notification.read = FALSE
|
||||
SET (
|
||||
CASE
|
||||
WHEN notification.createdAt IS NULL
|
||||
THEN notification END ).createdAt = toString(datetime())
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
`,
|
||||
{ commentId, postAuthorId, reason },
|
||||
)
|
||||
})
|
||||
if (context.user.id !== postAuthor.id) {
|
||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
CreateComment: handleCreateComment,
|
||||
CreateComment: handleContentDataOfComment,
|
||||
UpdateComment: handleContentDataOfComment,
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,11 +4,7 @@ import { createTestClient } from 'apollo-server-testing'
|
||||
import { getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
let query
|
||||
let mutate
|
||||
let notifiedUser
|
||||
let authenticatedUser
|
||||
let server, query, mutate, notifiedUser, authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
@ -39,7 +35,8 @@ const createCommentMutation = gql`
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
@ -173,7 +170,6 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -190,7 +186,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -214,7 +210,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -265,7 +261,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -409,7 +405,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -467,7 +463,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -501,7 +497,7 @@ describe('notifications', () => {
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
@ -532,7 +528,7 @@ describe('notifications', () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
|
||||
@ -47,17 +47,18 @@ const isAuthor = rule({
|
||||
if (!user) return false
|
||||
const { id: resourceId } = args
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
const authorReadTxPromise = session.readTransaction(async transaction => {
|
||||
const authorTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
|
||||
RETURN author
|
||||
`,
|
||||
MATCH (resource {id: $resourceId})<-[:WROTE]-(author {id: $userId})
|
||||
RETURN author
|
||||
`,
|
||||
{ resourceId, userId: user.id },
|
||||
)
|
||||
const [author] = result.records.map(record => {
|
||||
return record.get('author')
|
||||
})
|
||||
return authorTransactionResponse.records.map(record => record.get('author'))
|
||||
})
|
||||
try {
|
||||
const [author] = await authorReadTxPromise
|
||||
return !!author
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -4,10 +4,16 @@ const isUniqueFor = (context, type) => {
|
||||
return async slug => {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const response = await session.run(`MATCH(p:${type} {slug: $slug }) return p.slug`, {
|
||||
slug,
|
||||
const existingSlug = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH(p:${type} {slug: $slug })
|
||||
RETURN p.slug
|
||||
`,
|
||||
{ slug },
|
||||
)
|
||||
})
|
||||
return response.records.length === 0
|
||||
return existingSlug.records.length === 0
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import createOrUpdateLocations from './nodes/locations'
|
||||
import createOrUpdateLocations from '../nodes/locations'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
SignupVerification: async (resolve, root, args, context, info) => {
|
||||
const result = await resolve(root, args, context, info)
|
||||
await createOrUpdateLocations(args.id, args.locationName, context.driver)
|
||||
await createOrUpdateLocations(result.id, args.locationName, context.driver)
|
||||
return result
|
||||
},
|
||||
UpdateUser: async (resolve, root, args, context, info) => {
|
||||
213
backend/src/middleware/user/userMiddleware.spec.js
Normal file
213
backend/src/middleware/user/userMiddleware.spec.js
Normal file
@ -0,0 +1,213 @@
|
||||
import { gql } from '../../helpers/jest'
|
||||
import Factory from '../../seed/factories'
|
||||
import { getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../../server'
|
||||
|
||||
const factory = Factory()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
let authenticatedUser, mutate, variables
|
||||
|
||||
const signupVerificationMutation = gql`
|
||||
mutation(
|
||||
$name: String!
|
||||
$password: String!
|
||||
$email: String!
|
||||
$nonce: String!
|
||||
$termsAndConditionsAgreedVersion: String!
|
||||
$locationName: String
|
||||
) {
|
||||
SignupVerification(
|
||||
name: $name
|
||||
password: $password
|
||||
email: $email
|
||||
nonce: $nonce
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locationName: $locationName
|
||||
) {
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String!, $locationName: String) {
|
||||
UpdateUser(id: $id, name: $name, locationName: $locationName) {
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
lng: 41.1534,
|
||||
nameES: 'Hamburg',
|
||||
nameFR: 'Hamburg',
|
||||
nameIT: 'Hamburg',
|
||||
nameEN: 'Hamburg',
|
||||
type: 'place',
|
||||
namePT: 'Hamburg',
|
||||
nameRU: 'Хамбург',
|
||||
nameDE: 'Hamburg',
|
||||
nameNL: 'Hamburg',
|
||||
name: 'Hamburg',
|
||||
namePL: 'Hamburg',
|
||||
id: 'place.5977106083398860',
|
||||
lat: -74.5763,
|
||||
},
|
||||
state: {
|
||||
namePT: 'Nova Jérsia',
|
||||
nameRU: 'Нью-Джерси',
|
||||
nameDE: 'New Jersey',
|
||||
nameNL: 'New Jersey',
|
||||
nameES: 'Nueva Jersey',
|
||||
name: 'New Jersey',
|
||||
namePL: 'New Jersey',
|
||||
nameFR: 'New Jersey',
|
||||
nameIT: 'New Jersey',
|
||||
id: 'region.14919479731700330',
|
||||
nameEN: 'New Jersey',
|
||||
type: 'region',
|
||||
},
|
||||
country: {
|
||||
namePT: 'Estados Unidos',
|
||||
nameRU: 'Соединённые Штаты Америки',
|
||||
nameDE: 'Vereinigte Staaten',
|
||||
nameNL: 'Verenigde Staten van Amerika',
|
||||
nameES: 'Estados Unidos',
|
||||
namePL: 'Stany Zjednoczone',
|
||||
name: 'United States of America',
|
||||
nameFR: 'États-Unis',
|
||||
nameIT: "Stati Uniti d'America",
|
||||
id: 'country.9053006287256050',
|
||||
nameEN: 'United States of America',
|
||||
type: 'country',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
variables = {}
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('userMiddleware', () => {
|
||||
describe('SignupVerification', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: 'John Doe',
|
||||
password: '123',
|
||||
email: 'john@example.org',
|
||||
nonce: '123456',
|
||||
termsAndConditionsAgreedVersion: '0.1.0',
|
||||
locationName: 'Hamburg, New Jersey, United States of America',
|
||||
}
|
||||
const args = {
|
||||
email: 'john@example.org',
|
||||
nonce: '123456',
|
||||
}
|
||||
await neode.model('EmailAddress').create(args)
|
||||
})
|
||||
it('creates a Location node with localised city/state/country names', async () => {
|
||||
await mutate({ mutation: signupVerificationMutation, variables })
|
||||
const locations = await neode.cypher(
|
||||
`MATCH (city:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city, state, country`,
|
||||
)
|
||||
expect(
|
||||
locations.records.map(record => {
|
||||
return {
|
||||
city: record.get('city').properties,
|
||||
state: record.get('state').properties,
|
||||
country: record.get('country').properties,
|
||||
}
|
||||
}),
|
||||
).toEqual(newlyCreatedNodesWithLocales)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
let user, userParams
|
||||
beforeEach(async () => {
|
||||
newlyCreatedNodesWithLocales = [
|
||||
{
|
||||
city: {
|
||||
lng: 53.55,
|
||||
nameES: 'Hamburgo',
|
||||
nameFR: 'Hambourg',
|
||||
nameIT: 'Amburgo',
|
||||
nameEN: 'Hamburg',
|
||||
type: 'region',
|
||||
namePT: 'Hamburgo',
|
||||
nameRU: 'Гамбург',
|
||||
nameDE: 'Hamburg',
|
||||
nameNL: 'Hamburg',
|
||||
namePL: 'Hamburg',
|
||||
name: 'Hamburg',
|
||||
id: 'region.10793468240398860',
|
||||
lat: 10,
|
||||
},
|
||||
country: {
|
||||
namePT: 'Alemanha',
|
||||
nameRU: 'Германия',
|
||||
nameDE: 'Deutschland',
|
||||
nameNL: 'Duitsland',
|
||||
nameES: 'Alemania',
|
||||
name: 'Germany',
|
||||
namePL: 'Niemcy',
|
||||
nameFR: 'Allemagne',
|
||||
nameIT: 'Germania',
|
||||
id: 'country.10743216036480410',
|
||||
nameEN: 'Germany',
|
||||
type: 'country',
|
||||
},
|
||||
},
|
||||
]
|
||||
userParams = {
|
||||
id: 'updating-user',
|
||||
}
|
||||
user = await factory.create('User', userParams)
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('creates a Location node with localised city/state/country names', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'updating-user',
|
||||
name: 'Updating user',
|
||||
locationName: 'Hamburg, Germany',
|
||||
}
|
||||
await mutate({ mutation: updateUserMutation, variables })
|
||||
const locations = await neode.cypher(
|
||||
`MATCH (city:Location)-[:IS_IN]->(country:Location) return city, country`,
|
||||
)
|
||||
expect(
|
||||
locations.records.map(record => {
|
||||
return {
|
||||
city: record.get('city').properties,
|
||||
country: record.get('country').properties,
|
||||
}
|
||||
}),
|
||||
).toEqual(newlyCreatedNodesWithLocales)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -4,7 +4,7 @@ const COMMENT_MIN_LENGTH = 1
|
||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||
const NO_CATEGORIES_ERR_MESSAGE =
|
||||
'You cannot save a post without at least one category or more than three'
|
||||
|
||||
const USERNAME_MIN_LENGTH = 3
|
||||
const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||
const { postId } = args
|
||||
@ -14,14 +14,15 @@ const validateCreateComment = async (resolve, root, args, context, info) => {
|
||||
}
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const postQueryRes = await session.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post`,
|
||||
{
|
||||
postId,
|
||||
},
|
||||
)
|
||||
const postQueryRes = await session.readTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
RETURN post
|
||||
`,
|
||||
{ postId },
|
||||
)
|
||||
})
|
||||
const [post] = postQueryRes.records.map(record => {
|
||||
return record.get('post')
|
||||
})
|
||||
@ -72,8 +73,8 @@ const validateReview = async (resolve, root, args, context, info) => {
|
||||
const { user, driver } = context
|
||||
if (resourceId === user.id) throw new Error('You cannot review yourself!')
|
||||
const session = driver.session()
|
||||
const reportReadTxPromise = session.writeTransaction(async txc => {
|
||||
const validateReviewTransactionResponse = await txc.run(
|
||||
const reportReadTxPromise = session.readTransaction(async transaction => {
|
||||
const validateReviewTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})
|
||||
WHERE resource:User OR resource:Post OR resource:Comment
|
||||
@ -115,12 +116,31 @@ const validateReview = async (resolve, root, args, context, info) => {
|
||||
return resolve(root, args, context, info)
|
||||
}
|
||||
|
||||
export const validateNotifyUsers = async (label, reason) => {
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) throw new Error('Notification reason is not allowed!')
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
}
|
||||
|
||||
const validateUpdateUser = async (resolve, root, params, context, info) => {
|
||||
const { name } = params
|
||||
if (typeof name === 'string' && name.trim().length < USERNAME_MIN_LENGTH)
|
||||
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} character long!`)
|
||||
return resolve(root, params, context, info)
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateComment: validateCreateComment,
|
||||
UpdateComment: validateUpdateComment,
|
||||
CreatePost: validatePost,
|
||||
UpdatePost: validateUpdatePost,
|
||||
UpdateUser: validateUpdateUser,
|
||||
fileReport: validateReport,
|
||||
review: validateReview,
|
||||
},
|
||||
|
||||
@ -71,6 +71,14 @@ const reviewMutation = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String) {
|
||||
UpdateUser(id: $id, name: $name) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
@ -397,4 +405,33 @@ describe('validateReview', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUpdateUser', () => {
|
||||
let userParams, variables, updatingUser
|
||||
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
id: 'updating-user',
|
||||
name: 'John Doe',
|
||||
}
|
||||
|
||||
variables = {
|
||||
id: 'updating-user',
|
||||
name: 'John Doughnut',
|
||||
}
|
||||
updatingUser = await factory.create('User', userParams)
|
||||
authenticatedUser = await updatingUser.toJson()
|
||||
})
|
||||
|
||||
it('with name too short', async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
name: ' ',
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: null },
|
||||
errors: [{ message: 'Username must be at least 3 character long!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,6 +5,7 @@ export default {
|
||||
Mutation: {
|
||||
CreateComment: async (object, params, context, resolveInfo) => {
|
||||
const { postId } = params
|
||||
const { user, driver } = context
|
||||
// Adding relationship from comment to post by passing in the postId,
|
||||
// but we do not want to create the comment with postId as an attribute
|
||||
// because we use relationships for this. So, we are deleting it from params
|
||||
@ -12,26 +13,28 @@ export default {
|
||||
delete params.postId
|
||||
params.id = params.id || uuid()
|
||||
|
||||
const session = context.driver.session()
|
||||
const session = driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const createCommentTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
MATCH (author:User {id: $userId})
|
||||
WITH post, author
|
||||
CREATE (comment:Comment {params})
|
||||
SET comment.createdAt = toString(datetime())
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||
RETURN comment
|
||||
`,
|
||||
{ userId: user.id, postId, params },
|
||||
)
|
||||
return createCommentTransactionResponse.records.map(
|
||||
record => record.get('comment').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const createCommentCypher = `
|
||||
MATCH (post:Post {id: $postId})
|
||||
MATCH (author:User {id: $userId})
|
||||
WITH post, author
|
||||
CREATE (comment:Comment {params})
|
||||
SET comment.createdAt = toString(datetime())
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
MERGE (post)<-[:COMMENTS]-(comment)<-[:WROTE]-(author)
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(createCommentCypher, {
|
||||
userId: context.user.id,
|
||||
postId,
|
||||
params,
|
||||
})
|
||||
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
|
||||
const [comment] = await writeTxResultPromise
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
@ -39,15 +42,22 @@ export default {
|
||||
},
|
||||
UpdateComment: async (_parent, params, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateCommentTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $params.id})
|
||||
SET comment += $params
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
RETURN comment
|
||||
`,
|
||||
{ params },
|
||||
)
|
||||
return updateCommentTransactionResponse.records.map(
|
||||
record => record.get('comment').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const updateCommentCypher = `
|
||||
MATCH (comment:Comment {id: $params.id})
|
||||
SET comment += $params
|
||||
SET comment.updatedAt = toString(datetime())
|
||||
RETURN comment
|
||||
`
|
||||
const transactionRes = await session.run(updateCommentCypher, { params })
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
const [comment] = await writeTxResultPromise
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
@ -55,18 +65,23 @@ export default {
|
||||
},
|
||||
DeleteComment: async (_parent, args, context, _resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $commentId})
|
||||
SET comment.deleted = TRUE
|
||||
SET comment.content = 'UNAVAILABLE'
|
||||
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||
RETURN comment
|
||||
`,
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deleteCommentTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (comment:Comment {id: $commentId})
|
||||
SET comment.deleted = TRUE
|
||||
SET comment.content = 'UNAVAILABLE'
|
||||
SET comment.contentExcerpt = 'UNAVAILABLE'
|
||||
RETURN comment
|
||||
`,
|
||||
{ commentId: args.id },
|
||||
)
|
||||
const [comment] = transactionRes.records.map(record => record.get('comment').properties)
|
||||
return deleteCommentTransactionResponse.records.map(
|
||||
record => record.get('comment').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const [comment] = await writeTxResultPromise
|
||||
return comment
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -10,7 +10,8 @@ const factory = Factory()
|
||||
|
||||
let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await factory.cleanDatabase()
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
@ -19,8 +20,7 @@ beforeAll(() => {
|
||||
}
|
||||
},
|
||||
})
|
||||
const client = createTestClient(server)
|
||||
mutate = client.mutate
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -100,6 +100,7 @@ describe('CreateComment', () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { content: "I'm authorised to comment" } },
|
||||
errors: undefined,
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -108,6 +109,7 @@ describe('CreateComment', () => {
|
||||
await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject(
|
||||
{
|
||||
data: { CreateComment: { author: { name: 'Author' } } },
|
||||
errors: undefined,
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -157,6 +159,7 @@ describe('UpdateComment', () => {
|
||||
it('updates the comment', async () => {
|
||||
const expected = {
|
||||
data: { UpdateComment: { id: 'c456', content: 'The comment is updated' } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -172,6 +175,7 @@ describe('UpdateComment', () => {
|
||||
createdAt: expect.any(String),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateCommentMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
|
||||
@ -5,24 +5,29 @@ export default async function createPasswordReset(options) {
|
||||
const normalizedEmail = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
||||
CREATE(pr:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||
MERGE (u)-[:REQUESTED]->(pr)
|
||||
RETURN e, pr, u
|
||||
`
|
||||
const transactionRes = await session.run(cypher, {
|
||||
issuedAt: issuedAt.toISOString(),
|
||||
nonce,
|
||||
email: normalizedEmail,
|
||||
const createPasswordResetTxPromise = session.writeTransaction(async transaction => {
|
||||
const createPasswordResetTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User)-[:PRIMARY_EMAIL]->(email:EmailAddress {email:$email})
|
||||
CREATE(passwordReset:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||
MERGE (user)-[:REQUESTED]->(passwordReset)
|
||||
RETURN email, passwordReset, user
|
||||
`,
|
||||
{
|
||||
issuedAt: issuedAt.toISOString(),
|
||||
nonce,
|
||||
email: normalizedEmail,
|
||||
},
|
||||
)
|
||||
return createPasswordResetTransactionResponse.records.map(record => {
|
||||
const { email } = record.get('email').properties
|
||||
const { nonce } = record.get('passwordReset').properties
|
||||
const { name } = record.get('user').properties
|
||||
return { email, nonce, name }
|
||||
})
|
||||
})
|
||||
const records = transactionRes.records.map(record => {
|
||||
const { email } = record.get('e').properties
|
||||
const { nonce } = record.get('pr').properties
|
||||
const { name } = record.get('u').properties
|
||||
return { email, nonce, name }
|
||||
})
|
||||
return records[0] || {}
|
||||
const [records] = await createPasswordResetTxPromise
|
||||
return records || {}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import createPasswordReset from './createPasswordReset'
|
||||
|
||||
describe('createPasswordReset', () => {
|
||||
const issuedAt = new Date()
|
||||
const nonce = 'abcdef'
|
||||
|
||||
describe('email lookup', () => {
|
||||
let driver
|
||||
let mockSession
|
||||
beforeEach(() => {
|
||||
mockSession = {
|
||||
close() {},
|
||||
run: jest.fn().mockReturnValue({
|
||||
records: {
|
||||
map: jest.fn(() => []),
|
||||
},
|
||||
}),
|
||||
}
|
||||
driver = { session: () => mockSession }
|
||||
})
|
||||
|
||||
it('lowercases email address', async () => {
|
||||
const email = 'stRaNGeCaSiNG@ExAmplE.ORG'
|
||||
await createPasswordReset({ driver, email, issuedAt, nonce })
|
||||
expect(mockSession.run.mock.calls).toEqual([
|
||||
[
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
email: 'strangecasing@example.org',
|
||||
}),
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,25 +1,29 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
export default async function alreadyExistingMail({ args, context }) {
|
||||
const cypher = `
|
||||
MATCH (email:EmailAddress {email: $email})
|
||||
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
|
||||
RETURN email, user
|
||||
`
|
||||
let transactionRes
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
transactionRes = await session.run(cypher, { email: args.email })
|
||||
const existingEmailAddressTxPromise = session.writeTransaction(async transaction => {
|
||||
const existingEmailAddressTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (email:EmailAddress {email: $email})
|
||||
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
|
||||
RETURN email, user
|
||||
`,
|
||||
{ email: args.email },
|
||||
)
|
||||
return existingEmailAddressTransactionResponse.records.map(record => {
|
||||
return {
|
||||
alreadyExistingEmail: record.get('email').properties,
|
||||
user: record.get('user') && record.get('user').properties,
|
||||
}
|
||||
})
|
||||
})
|
||||
const [emailBelongsToUser] = await existingEmailAddressTxPromise
|
||||
const { alreadyExistingEmail, user } = emailBelongsToUser || {}
|
||||
if (user) throw new UserInputError('A user account with this email already exists.')
|
||||
return alreadyExistingEmail
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const [result] = transactionRes.records.map(record => {
|
||||
return {
|
||||
alreadyExistingEmail: record.get('email').properties,
|
||||
user: record.get('user') && record.get('user').properties,
|
||||
}
|
||||
})
|
||||
const { alreadyExistingEmail, user } = result || {}
|
||||
if (user) throw new UserInputError('A user account with this email already exists.')
|
||||
return alreadyExistingEmail
|
||||
}
|
||||
|
||||
@ -76,16 +76,21 @@ export default {
|
||||
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const markNotificationAsReadTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
SET notification.read = TRUE
|
||||
RETURN resource, notification, user
|
||||
`,
|
||||
{ resourceId: args.id, id: currentUser.id },
|
||||
)
|
||||
log(markNotificationAsReadTransactionResponse)
|
||||
return markNotificationAsReadTransactionResponse.records.map(transformReturnType)
|
||||
})
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
SET notification.read = TRUE
|
||||
RETURN resource, notification, user
|
||||
`
|
||||
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||
log(result)
|
||||
const notifications = await result.records.map(transformReturnType)
|
||||
return notifications[0]
|
||||
const [notifications] = await writeTxResultPromise
|
||||
return notifications
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -12,25 +12,29 @@ export default {
|
||||
const stillValid = new Date()
|
||||
stillValid.setDate(stillValid.getDate() - 1)
|
||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||
const cypher = `
|
||||
MATCH (pr:PasswordReset {nonce: $nonce})
|
||||
MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr)
|
||||
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
||||
SET pr.usedAt = datetime()
|
||||
SET u.encryptedPassword = $encryptedNewPassword
|
||||
RETURN pr
|
||||
`
|
||||
const session = driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(cypher, {
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
const passwordResetTxPromise = session.writeTransaction(async transaction => {
|
||||
const passwordResetTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (passwordReset:PasswordReset {nonce: $nonce})
|
||||
MATCH (email:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(user:User)-[:REQUESTED]->(passwordReset)
|
||||
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
|
||||
SET passwordReset.usedAt = datetime()
|
||||
SET user.encryptedPassword = $encryptedNewPassword
|
||||
RETURN passwordReset
|
||||
`,
|
||||
{
|
||||
stillValid,
|
||||
email,
|
||||
nonce,
|
||||
encryptedNewPassword,
|
||||
},
|
||||
)
|
||||
return passwordResetTransactionResponse.records.map(record => record.get('passwordReset'))
|
||||
})
|
||||
const [reset] = transactionRes.records.map(record => record.get('pr'))
|
||||
const response = !!(reset && reset.properties.usedAt)
|
||||
return response
|
||||
const [reset] = await passwordResetTxPromise
|
||||
return !!(reset && reset.properties.usedAt)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -14,14 +14,11 @@ let authenticatedUser
|
||||
let variables
|
||||
|
||||
const getAllPasswordResets = async () => {
|
||||
const session = driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
|
||||
const resets = transactionRes.records.map(record => record.get('r'))
|
||||
return resets
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
const passwordResetQuery = await neode.cypher(
|
||||
'MATCH (passwordReset:PasswordReset) RETURN passwordReset',
|
||||
)
|
||||
const resets = passwordResetQuery.records.map(record => record.get('passwordReset'))
|
||||
return resets
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -57,17 +57,20 @@ export default {
|
||||
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||
const { postId, data } = params
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||
`,
|
||||
const readTxResultPromise = session.readTransaction(async transaction => {
|
||||
const emotionsCountTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||
`,
|
||||
{ postId, data },
|
||||
)
|
||||
|
||||
const [emotionsCount] = transactionRes.records.map(record => {
|
||||
return record.get('emotionsCount').low
|
||||
})
|
||||
return emotionsCountTransactionResponse.records.map(
|
||||
record => record.get('emotionsCount').low,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const [emotionsCount] = await readTxResultPromise
|
||||
return emotionsCount
|
||||
} finally {
|
||||
session.close()
|
||||
@ -76,16 +79,18 @@ export default {
|
||||
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||
const { postId } = params
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
RETURN collect(emoted.emotion) as emotion`,
|
||||
const readTxResultPromise = session.readTransaction(async transaction => {
|
||||
const emotionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||
RETURN collect(emoted.emotion) as emotion
|
||||
`,
|
||||
{ userId: context.user.id, postId },
|
||||
)
|
||||
|
||||
const [emotions] = transactionRes.records.map(record => {
|
||||
return record.get('emotion')
|
||||
})
|
||||
return emotionsTransactionResponse.records.map(record => record.get('emotion'))
|
||||
})
|
||||
try {
|
||||
const [emotions] = await readTxResultPromise
|
||||
return emotions
|
||||
} finally {
|
||||
session.close()
|
||||
@ -98,25 +103,29 @@ export default {
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
params.id = params.id || uuid()
|
||||
const createPostCypher = `CREATE (post:Post {params})
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
MATCH (author:User {id: $userId})
|
||||
MERGE (post)<-[:WROTE]-(author)
|
||||
WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
RETURN post`
|
||||
|
||||
const createPostVariables = { userId: context.user.id, categoryIds, params }
|
||||
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const createPostTransactionResponse = await transaction.run(
|
||||
`
|
||||
CREATE (post:Post {params})
|
||||
SET post.createdAt = toString(datetime())
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
MATCH (author:User {id: $userId})
|
||||
MERGE (post)<-[:WROTE]-(author)
|
||||
WITH post
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
RETURN post
|
||||
`,
|
||||
{ userId: context.user.id, categoryIds, params },
|
||||
)
|
||||
return createPostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
try {
|
||||
const transactionRes = await session.run(createPostCypher, createPostVariables)
|
||||
const posts = transactionRes.records.map(record => record.get('post').properties)
|
||||
return posts[0]
|
||||
const [post] = await writeTxResultPromise
|
||||
return post
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('Post with this slug already exists!')
|
||||
@ -129,38 +138,44 @@ export default {
|
||||
const { categoryIds } = params
|
||||
delete params.categoryIds
|
||||
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
|
||||
let updatePostCypher = `MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
let updatePostCypher = `
|
||||
MATCH (post:Post {id: $params.id})
|
||||
SET post += $params
|
||||
SET post.updatedAt = toString(datetime())
|
||||
WITH post
|
||||
`
|
||||
|
||||
if (categoryIds && categoryIds.length) {
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
|
||||
DELETE previousRelations
|
||||
RETURN post, category
|
||||
`
|
||||
`
|
||||
|
||||
await session.run(cypherDeletePreviousRelations, { params })
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(cypherDeletePreviousRelations, { params })
|
||||
})
|
||||
|
||||
updatePostCypher += `
|
||||
updatePostCypher += `
|
||||
UNWIND $categoryIds AS categoryId
|
||||
MATCH (category:Category {id: categoryId})
|
||||
MERGE (post)-[:CATEGORIZED]->(category)
|
||||
WITH post
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
|
||||
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
|
||||
const [post] = transactionRes.records.map(record => {
|
||||
return record.get('post').properties
|
||||
updatePostCypher += `RETURN post`
|
||||
const updatePostVariables = { categoryIds, params }
|
||||
try {
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updatePostTransactionResponse = await transaction.run(
|
||||
updatePostCypher,
|
||||
updatePostVariables,
|
||||
)
|
||||
return updatePostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
const [post] = await writeTxResultPromise
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
@ -169,23 +184,25 @@ export default {
|
||||
|
||||
DeletePost: async (object, args, context, resolveInfo) => {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionRes = await session.run(
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deletePostTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
SET post.content = 'UNAVAILABLE'
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
SET post.title = 'UNAVAILABLE'
|
||||
SET comment.deleted = TRUE
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
`,
|
||||
MATCH (post:Post {id: $postId})
|
||||
OPTIONAL MATCH (post)<-[:COMMENTS]-(comment:Comment)
|
||||
SET post.deleted = TRUE
|
||||
SET post.content = 'UNAVAILABLE'
|
||||
SET post.contentExcerpt = 'UNAVAILABLE'
|
||||
SET post.title = 'UNAVAILABLE'
|
||||
SET comment.deleted = TRUE
|
||||
REMOVE post.image
|
||||
RETURN post
|
||||
`,
|
||||
{ postId: args.id },
|
||||
)
|
||||
const [post] = transactionRes.records.map(record => record.get('post').properties)
|
||||
return deletePostTransactionResponse.records.map(record => record.get('post').properties)
|
||||
})
|
||||
try {
|
||||
const [post] = await writeTxResultPromise
|
||||
return post
|
||||
} finally {
|
||||
session.close()
|
||||
@ -195,21 +212,24 @@ export default {
|
||||
const { to, data } = params
|
||||
const { user } = context
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
||||
RETURN userFrom, postTo, emotedRelation`,
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const addPostEmotionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
|
||||
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
|
||||
RETURN userFrom, postTo, emotedRelation`,
|
||||
{ user, to, data },
|
||||
)
|
||||
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return addPostEmotionsTransactionResponse.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
...record.get('emotedRelation').properties,
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
const [emoted] = await writeTxResultPromise
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
@ -219,20 +239,25 @@ export default {
|
||||
const { to, data } = params
|
||||
const { id: from } = context.user
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
DELETE emotedRelation
|
||||
RETURN userFrom, postTo`,
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const removePostEmotionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
|
||||
DELETE emotedRelation
|
||||
RETURN userFrom, postTo
|
||||
`,
|
||||
{ from, to, data },
|
||||
)
|
||||
const [emoted] = transactionRes.records.map(record => {
|
||||
return removePostEmotionsTransactionResponse.records.map(record => {
|
||||
return {
|
||||
from: { ...record.get('userFrom').properties },
|
||||
to: { ...record.get('postTo').properties },
|
||||
emotion: data.emotion,
|
||||
}
|
||||
})
|
||||
})
|
||||
try {
|
||||
const [emoted] = await writeTxResultPromise
|
||||
return emoted
|
||||
} finally {
|
||||
session.close()
|
||||
@ -345,21 +370,28 @@ export default {
|
||||
relatedContributions: async (parent, params, context, resolveInfo) => {
|
||||
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
||||
const { id } = parent
|
||||
const statement = `
|
||||
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
WHERE NOT post.deleted AND NOT post.disabled
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
`
|
||||
let relatedContributions
|
||||
const session = context.driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const relatedContributionsTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
WHERE NOT post.deleted AND NOT post.disabled
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
`,
|
||||
{ id },
|
||||
)
|
||||
return relatedContributionsTransactionResponse.records.map(
|
||||
record => record.get('post').properties,
|
||||
)
|
||||
})
|
||||
try {
|
||||
const result = await session.run(statement, { id })
|
||||
relatedContributions = result.records.map(r => r.get('post').properties)
|
||||
const relatedContributions = await writeTxResultPromise
|
||||
return relatedContributions
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return relatedContributions
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -383,7 +383,10 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
|
||||
it('updates a post', async () => {
|
||||
const expected = { data: { UpdatePost: { id: 'p9876', content: 'New content' } } }
|
||||
const expected = {
|
||||
data: { UpdatePost: { id: 'p9876', content: 'New content' } },
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
@ -394,6 +397,7 @@ describe('UpdatePost', () => {
|
||||
data: {
|
||||
UpdatePost: { id: 'p9876', content: 'New content', createdAt: expect.any(String) },
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -421,6 +425,7 @@ describe('UpdatePost', () => {
|
||||
categories: expect.arrayContaining([{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -441,6 +446,7 @@ describe('UpdatePost', () => {
|
||||
categories: expect.arrayContaining([{ id: 'cat27' }]),
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
@ -722,6 +728,7 @@ describe('UpdatePost', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
variables = { orderBy: ['pinned_desc', 'createdAt_desc'] }
|
||||
await expect(query({ query: postOrderingQuery, variables })).resolves.toMatchObject(
|
||||
|
||||
@ -24,18 +24,19 @@ export default {
|
||||
const { user } = await getUserAndBadge(params)
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
// silly neode cannot remove relationships
|
||||
await session.run(
|
||||
`
|
||||
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
||||
DELETE reward
|
||||
RETURN rewardedUser
|
||||
`,
|
||||
{
|
||||
badgeKey,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
||||
DELETE reward
|
||||
RETURN rewardedUser
|
||||
`,
|
||||
{
|
||||
badgeKey,
|
||||
userId,
|
||||
},
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
shout: async (_object, params, context, _resolveInfo) => {
|
||||
@ -5,22 +7,24 @@ export default {
|
||||
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
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{createdAt:toString(datetime())}]->(node)
|
||||
RETURN COUNT(relation) > 0 as isShouted`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
const shoutWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const shoutTransactionResponse = await transaction.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{createdAt:toString(datetime())}]->(node)
|
||||
RETURN COUNT(relation) > 0 as isShouted
|
||||
`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
log(shoutTransactionResponse)
|
||||
return shoutTransactionResponse.records.map(record => record.get('isShouted'))
|
||||
})
|
||||
|
||||
const [isShouted] = await shoutWriteTxResultPromise
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
@ -31,20 +35,24 @@ export default {
|
||||
const { id, type } = params
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const transactionRes = await session.run(
|
||||
`MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||
WHERE $type IN labels(node)
|
||||
DELETE relation
|
||||
RETURN COUNT(relation) > 0 as isShouted`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
const [isShouted] = transactionRes.records.map(record => {
|
||||
return record.get('isShouted')
|
||||
const unshoutWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const unshoutTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})-[relation:SHOUTED]->(node {id: $id})
|
||||
WHERE $type IN labels(node)
|
||||
DELETE relation
|
||||
RETURN COUNT(relation) > 0 as isShouted
|
||||
`,
|
||||
{
|
||||
id,
|
||||
type,
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
log(unshoutTransactionResponse)
|
||||
return unshoutTransactionResponse.records.map(record => record.get('isShouted'))
|
||||
})
|
||||
const [isShouted] = await unshoutWriteTxResultPromise
|
||||
return isShouted
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
statistics: async (_parent, _args, { driver }) => {
|
||||
const session = driver.session()
|
||||
const response = {}
|
||||
const counts = {}
|
||||
try {
|
||||
const mapping = {
|
||||
countUsers: 'User',
|
||||
@ -13,27 +15,28 @@ export default {
|
||||
countFollows: 'FOLLOWS',
|
||||
countShouts: 'SHOUTED',
|
||||
}
|
||||
const cypher = `
|
||||
CALL apoc.meta.stats() YIELD labels, relTypesCount
|
||||
RETURN labels, relTypesCount
|
||||
`
|
||||
const result = await session.run(cypher)
|
||||
const [statistics] = await result.records.map(record => {
|
||||
return {
|
||||
...record.get('labels'),
|
||||
...record.get('relTypesCount'),
|
||||
}
|
||||
const statisticsReadTxResultPromise = session.readTransaction(async transaction => {
|
||||
const statisticsTransactionResponse = await transaction.run(
|
||||
`
|
||||
CALL apoc.meta.stats() YIELD labels, relTypesCount
|
||||
RETURN labels, relTypesCount
|
||||
`,
|
||||
)
|
||||
log(statisticsTransactionResponse)
|
||||
return statisticsTransactionResponse.records.map(record => {
|
||||
return {
|
||||
...record.get('labels'),
|
||||
...record.get('relTypesCount'),
|
||||
}
|
||||
})
|
||||
})
|
||||
const [statistics] = await statisticsReadTxResultPromise
|
||||
Object.keys(mapping).forEach(key => {
|
||||
const stat = statistics[mapping[key]]
|
||||
response[key] = stat ? stat.toNumber() : 0
|
||||
counts[key] = stat ? stat.toNumber() : 0
|
||||
})
|
||||
|
||||
/*
|
||||
* Note: invites count is calculated this way because invitation codes are not in use yet
|
||||
*/
|
||||
response.countInvites = response.countEmails - response.countUsers
|
||||
return response
|
||||
counts.countInvites = counts.countEmails - counts.countUsers
|
||||
return counts
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import bcrypt from 'bcryptjs'
|
||||
import { AuthenticationError } from 'apollo-server'
|
||||
import { getNeode } from '../../bootstrap/neo4j'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -25,17 +26,18 @@ export default {
|
||||
email = normalizeEmail(email)
|
||||
const session = driver.session()
|
||||
try {
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
||||
RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
|
||||
`,
|
||||
{ userEmail: email },
|
||||
)
|
||||
const [currentUser] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
const loginReadTxResultPromise = session.readTransaction(async transaction => {
|
||||
const loginTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {deleted: false})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})
|
||||
RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1
|
||||
`,
|
||||
{ userEmail: email },
|
||||
)
|
||||
log(loginTransactionResponse)
|
||||
return loginTransactionResponse.records.map(record => record.get('user'))
|
||||
})
|
||||
|
||||
const [currentUser] = await loginReadTxResultPromise
|
||||
if (
|
||||
currentUser &&
|
||||
(await bcrypt.compareSync(password, currentUser.encryptedPassword)) &&
|
||||
|
||||
@ -3,6 +3,7 @@ import fileUpload from './fileUpload'
|
||||
import { getNeode } from '../../bootstrap/neo4j'
|
||||
import { UserInputError, ForbiddenError } from 'apollo-server'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -100,72 +101,89 @@ export default {
|
||||
const blockedUser = await neode.find('User', args.id)
|
||||
return blockedUser.toJson()
|
||||
},
|
||||
UpdateUser: async (object, args, context, resolveInfo) => {
|
||||
const { termsAndConditionsAgreedVersion } = args
|
||||
UpdateUser: async (_parent, params, context, _resolveInfo) => {
|
||||
const { termsAndConditionsAgreedVersion } = params
|
||||
if (termsAndConditionsAgreedVersion) {
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
throw new ForbiddenError('Invalid version format!')
|
||||
}
|
||||
args.termsAndConditionsAgreedAt = new Date().toISOString()
|
||||
params.termsAndConditionsAgreedAt = new Date().toISOString()
|
||||
}
|
||||
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
||||
params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' })
|
||||
const session = context.driver.session()
|
||||
|
||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const updateUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $params.id})
|
||||
SET user += $params
|
||||
SET user.updatedAt = toString(datetime())
|
||||
RETURN user
|
||||
`,
|
||||
{ params },
|
||||
)
|
||||
return updateUserTransactionResponse.records.map(record => record.get('user').properties)
|
||||
})
|
||||
try {
|
||||
const user = await neode.find('User', args.id)
|
||||
if (!user) return null
|
||||
await user.update({ ...args, updatedAt: new Date().toISOString() })
|
||||
return user.toJson()
|
||||
} catch (e) {
|
||||
throw new UserInputError(e.message)
|
||||
const [user] = await writeTxResultPromise
|
||||
return user
|
||||
} catch (error) {
|
||||
throw new UserInputError(error.message)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
DeleteUser: async (object, params, context, resolveInfo) => {
|
||||
const { resource } = params
|
||||
const session = context.driver.session()
|
||||
|
||||
let user
|
||||
try {
|
||||
if (resource && resource.length) {
|
||||
await Promise.all(
|
||||
resource.map(async node => {
|
||||
await session.run(
|
||||
await session.writeTransaction(transaction => {
|
||||
resource.map(node => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
RETURN author`,
|
||||
MATCH (resource:${node})<-[:WROTE]-(author:User {id: $userId})
|
||||
OPTIONAL MATCH (resource)<-[:COMMENTS]-(comment:Comment)
|
||||
SET resource.deleted = true
|
||||
SET resource.content = 'UNAVAILABLE'
|
||||
SET resource.contentExcerpt = 'UNAVAILABLE'
|
||||
SET comment.deleted = true
|
||||
RETURN author
|
||||
`,
|
||||
{
|
||||
userId: context.user.id,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// we cannot set slug to 'UNAVAILABE' because of unique constraints
|
||||
const transactionResult = await session.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})
|
||||
SET user.deleted = true
|
||||
SET user.name = 'UNAVAILABLE'
|
||||
SET user.about = 'UNAVAILABLE'
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
|
||||
DETACH DELETE email
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
|
||||
DETACH DELETE socialMedia
|
||||
RETURN user`,
|
||||
{ userId: context.user.id },
|
||||
)
|
||||
user = transactionResult.records.map(r => r.get('user').properties)[0]
|
||||
const deleteUserTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const deleteUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (user:User {id: $userId})
|
||||
SET user.deleted = true
|
||||
SET user.name = 'UNAVAILABLE'
|
||||
SET user.about = 'UNAVAILABLE'
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress)
|
||||
DETACH DELETE email
|
||||
WITH user
|
||||
OPTIONAL MATCH (user)<-[:OWNED_BY]-(socialMedia:SocialMedia)
|
||||
DETACH DELETE socialMedia
|
||||
RETURN user
|
||||
`,
|
||||
{ userId: context.user.id },
|
||||
)
|
||||
log(deleteUserTransactionResponse)
|
||||
return deleteUserTransactionResponse.records.map(record => record.get('user').properties)
|
||||
})
|
||||
const [user] = await deleteUserTxResultPromise
|
||||
return user
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return user
|
||||
},
|
||||
},
|
||||
User: {
|
||||
|
||||
@ -68,6 +68,7 @@ describe('User', () => {
|
||||
it('is permitted', async () => {
|
||||
await expect(query({ query: userQuery, variables })).resolves.toMatchObject({
|
||||
data: { User: [{ name: 'Johnny' }] },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
@ -90,8 +91,7 @@ describe('User', () => {
|
||||
})
|
||||
|
||||
describe('UpdateUser', () => {
|
||||
let userParams
|
||||
let variables
|
||||
let userParams, variables
|
||||
|
||||
beforeEach(async () => {
|
||||
userParams = {
|
||||
@ -111,16 +111,23 @@ describe('UpdateUser', () => {
|
||||
})
|
||||
|
||||
const updateUserMutation = gql`
|
||||
mutation($id: ID!, $name: String, $termsAndConditionsAgreedVersion: String) {
|
||||
mutation(
|
||||
$id: ID!
|
||||
$name: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$locationName: String
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
name: $name
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
locationName: $locationName
|
||||
) {
|
||||
id
|
||||
name
|
||||
termsAndConditionsAgreedVersion
|
||||
termsAndConditionsAgreedAt
|
||||
locationName
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -152,7 +159,7 @@ describe('UpdateUser', () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
it('name within specifications', async () => {
|
||||
it('updates the name', async () => {
|
||||
const expected = {
|
||||
data: {
|
||||
UpdateUser: {
|
||||
@ -160,36 +167,13 @@ describe('UpdateUser', () => {
|
||||
name: 'John Doughnut',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('with `null` as name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: null,
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty(
|
||||
'message',
|
||||
'child "name" fails because ["name" contains an invalid value, "name" must be a string]',
|
||||
)
|
||||
})
|
||||
|
||||
it('with too short name', async () => {
|
||||
const variables = {
|
||||
id: 'u47',
|
||||
name: ' ',
|
||||
}
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty(
|
||||
'message',
|
||||
'child "name" fails because ["name" length must be at least 3 characters long]',
|
||||
)
|
||||
})
|
||||
|
||||
describe('given a new agreed version of terms and conditions', () => {
|
||||
beforeEach(async () => {
|
||||
variables = { ...variables, termsAndConditionsAgreedVersion: '0.0.2' }
|
||||
@ -202,6 +186,7 @@ describe('UpdateUser', () => {
|
||||
termsAndConditionsAgreedAt: expect.any(String),
|
||||
}),
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
@ -222,6 +207,7 @@ describe('UpdateUser', () => {
|
||||
termsAndConditionsAgreedAt: null,
|
||||
}),
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject(
|
||||
@ -238,6 +224,14 @@ describe('UpdateUser', () => {
|
||||
const { errors } = await mutate({ mutation: updateUserMutation, variables })
|
||||
expect(errors[0]).toHaveProperty('message', 'Invalid version format!')
|
||||
})
|
||||
|
||||
it('supports updating location', async () => {
|
||||
variables = { ...variables, locationName: 'Hamburg, New Jersey, United States of America' }
|
||||
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
|
||||
data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -372,6 +366,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(mutate({ mutation: deleteUserMutation, variables })).resolves.toMatchObject(
|
||||
expectedResponse,
|
||||
@ -418,6 +413,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
@ -465,6 +461,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
@ -511,6 +508,7 @@ describe('DeleteUser', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}
|
||||
await expect(
|
||||
mutate({ mutation: deleteUserMutation, variables }),
|
||||
|
||||
@ -29,10 +29,16 @@ const factories = {
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
const { driver = getDriver() } = options
|
||||
const cypher = 'MATCH (n) DETACH DELETE n'
|
||||
const session = driver.session()
|
||||
try {
|
||||
return await session.run(cypher)
|
||||
await session.writeTransaction(transaction => {
|
||||
return transaction.run(
|
||||
`
|
||||
MATCH (everything)
|
||||
DETACH DELETE everything
|
||||
`,
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -1034,10 +1034,10 @@
|
||||
url-regex "~4.1.1"
|
||||
video-extensions "~1.1.0"
|
||||
|
||||
"@metascraper/helpers@^5.8.7":
|
||||
version "5.8.7"
|
||||
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.8.7.tgz#b05f83f2a90001f7753c18a8b1bb978bd7c2f9d9"
|
||||
integrity sha512-gDErMAA3d1CdkGxvAG4cDi7D2+fReZpD6lzYNJ/gsq45U3Pdz7ltsAvbp4amK92bGKYYPZtnUq85Wrr+Q+e06Q==
|
||||
"@metascraper/helpers@^5.8.10", "@metascraper/helpers@^5.8.7":
|
||||
version "5.8.10"
|
||||
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.8.10.tgz#efaae1d57afca6db1f0846852fe88d1608601f13"
|
||||
integrity sha512-o7vrlNC+wzfArTkQcQfHKT4iHUYEQYs6hoORTWN7A1dj5v8P1wl5oOs0oAc7MNGJ3nWnex3/bq/5SUWV301Arg==
|
||||
dependencies:
|
||||
audio-extensions "0.0.0"
|
||||
chrono-node "~1.3.11"
|
||||
@ -1625,26 +1625,26 @@ apollo-cache-inmemory@~1.6.3:
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-cache@1.3.2, apollo-cache@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a"
|
||||
integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg==
|
||||
apollo-cache@1.3.4, apollo-cache@^1.3.2:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.4.tgz#0c9f63c793e1cd6e34c450f7668e77aff58c9a42"
|
||||
integrity sha512-7X5aGbqaOWYG+SSkCzJNHTz2ZKDcyRwtmvW4mGVLRqdQs+HxfXS4dUS2CcwrAj449se6tZ6NLUMnjko4KMt3KA==
|
||||
dependencies:
|
||||
apollo-utilities "^1.3.2"
|
||||
tslib "^1.9.3"
|
||||
apollo-utilities "^1.3.3"
|
||||
tslib "^1.10.0"
|
||||
|
||||
apollo-client@~2.6.4:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140"
|
||||
integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ==
|
||||
apollo-client@~2.6.8:
|
||||
version "2.6.8"
|
||||
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.8.tgz#01cebc18692abf90c6b3806414e081696b0fa537"
|
||||
integrity sha512-0zvJtAcONiozpa5z5zgou83iEKkBaXhhSSXJebFHRXs100SecDojyUWKjwTtBPn9HbM6o5xrvC5mo9VQ5fgAjw==
|
||||
dependencies:
|
||||
"@types/zen-observable" "^0.8.0"
|
||||
apollo-cache "1.3.2"
|
||||
apollo-cache "1.3.4"
|
||||
apollo-link "^1.0.0"
|
||||
apollo-utilities "1.3.2"
|
||||
apollo-utilities "1.3.3"
|
||||
symbol-observable "^1.0.2"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
tslib "^1.10.0"
|
||||
zen-observable "^0.8.0"
|
||||
|
||||
apollo-datasource@^0.6.3:
|
||||
@ -1847,15 +1847,15 @@ apollo-tracing@^0.8.8:
|
||||
apollo-server-env "^2.4.3"
|
||||
graphql-extensions "^0.10.7"
|
||||
|
||||
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
|
||||
integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
|
||||
apollo-utilities@1.3.3, apollo-utilities@^1.0.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2, apollo-utilities@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.3.tgz#f1854715a7be80cd810bc3ac95df085815c0787c"
|
||||
integrity sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw==
|
||||
dependencies:
|
||||
"@wry/equality" "^0.1.2"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
tslib "^1.10.0"
|
||||
|
||||
aproba@^1.0.3:
|
||||
version "1.2.0"
|
||||
@ -3412,10 +3412,10 @@ eslint-plugin-node@~10.0.0:
|
||||
resolve "^1.10.1"
|
||||
semver "^6.1.0"
|
||||
|
||||
eslint-plugin-prettier@~3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz#507b8562410d02a03f0ddc949c616f877852f2ba"
|
||||
integrity sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==
|
||||
eslint-plugin-prettier@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
|
||||
integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
@ -5810,12 +5810,12 @@ 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.8.7:
|
||||
version "5.8.7"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.8.7.tgz#ce27b1f4056c1d1cbaa2cec0e819c3704f38fff4"
|
||||
integrity sha512-ew9KZKOIl3u0500j7qIR/ZNiVtSohuyyiIWSxJVEeeguEOwAhMpOrpYAEkvKRo5CB89F2PNBIsXJIzMC4BWFrw==
|
||||
metascraper-audio@^5.8.10:
|
||||
version "5.8.10"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.8.10.tgz#bc7bc0471ee178ab747baec4fb9bf7443078980d"
|
||||
integrity sha512-uR4PCG7mxz7GLZ3I3x83sTCAaD/+MMTSf5rtP+shfdGJCm6h3mNmUpZm6hlBunmBx/PpDpwdI34rkl2A8SUjnQ==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.8.7"
|
||||
"@metascraper/helpers" "^5.8.10"
|
||||
|
||||
metascraper-author@^5.8.7:
|
||||
version "5.8.7"
|
||||
@ -5839,12 +5839,12 @@ metascraper-date@^5.8.7:
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.8.7"
|
||||
|
||||
metascraper-description@^5.8.7:
|
||||
version "5.8.7"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.8.7.tgz#e85ce218daf33b74813b1523ad7dc7dc3fb128af"
|
||||
integrity sha512-KOv5gnQVvGF1CgpUczu7KJm76rWJ7SH5UFcqFST60hRNgR9xy0y3aHbVDOhZkjNN4UKqnxMF6XTS/WaQxCK/AA==
|
||||
metascraper-description@^5.8.10:
|
||||
version "5.8.10"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.8.10.tgz#1b69f59fa76263fcd2c15f8ce73052b81900177a"
|
||||
integrity sha512-0stYkl5OPpM0yM6Dl3WcXxLjl2gY5k77E4seeHOqHAUx1EKXNgrSrtO0I3PX9p6vcxP+WBtK6zlqHYU4qAMlSA==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.8.7"
|
||||
"@metascraper/helpers" "^5.8.10"
|
||||
|
||||
metascraper-image@^5.8.7:
|
||||
version "5.8.7"
|
||||
@ -5869,12 +5869,12 @@ metascraper-lang@^5.8.9:
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.8.7"
|
||||
|
||||
metascraper-logo@^5.8.7:
|
||||
version "5.8.7"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.8.7.tgz#5efb7e6c5f91ccad812e2d9ec3facfef179f40b6"
|
||||
integrity sha512-QudGVJBBeXLWU54Xw2PmnsTf+qPUnbyYaOl4aFLg2wkLLza1GbuvOYGMiH9Y8k0WcRoesi9sQk+P0a/611blew==
|
||||
metascraper-logo@^5.8.10:
|
||||
version "5.8.10"
|
||||
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-5.8.10.tgz#8e0dc0296d71db03307584ecdb57cd3fcbad1d4b"
|
||||
integrity sha512-l5LkzZcVzrKclzf3JGx2cnCtPI/8Rf+EQV/SfXUqz7FUwgfT3uzRw9wBbqP25056ukh6aOuywGClTdnEu2PJcw==
|
||||
dependencies:
|
||||
"@metascraper/helpers" "^5.8.7"
|
||||
"@metascraper/helpers" "^5.8.10"
|
||||
|
||||
metascraper-publisher@^5.8.7:
|
||||
version "5.8.7"
|
||||
@ -6161,10 +6161,10 @@ 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.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.10.0.tgz#4298793756d839dedb98bc3e50a2bd40a311874d"
|
||||
integrity sha512-jRdIyw+DHg9gfB6pWKb1ZHMR9rXIl7qf51efjUHIRHRbVR3RCcw1cKyONkq4LE8v2bHc7QDrKwJs+GQ1SRxDug==
|
||||
neo4j-graphql-js@^2.10.1:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.10.1.tgz#e470d067db681bac8f4daa755f697000110aca4b"
|
||||
integrity sha512-D6Gimu39lkg+3pXKWR3qEY6yMXOv/JOdKSizsYSAE73lj9CubJAYx4hdtmNXJ0Tyy+C9LxcPZwWZEzg0P9niEw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
"@babel/runtime-corejs2" "^7.5.5"
|
||||
@ -6270,15 +6270,15 @@ nodemailer-html-to-text@^3.1.0:
|
||||
dependencies:
|
||||
html-to-text "^5.1.1"
|
||||
|
||||
nodemailer@^6.4.1:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.1.tgz#f70b40355b7b08f1f80344b353970a4f8f664370"
|
||||
integrity sha512-mSQAzMim8XIC1DemK9TifDTIgASfoJEllG5aC1mEtZeZ+FQyrSOdGBRth6JRA1ERzHQCET3QHVSd9Kc6mh356g==
|
||||
nodemailer@^6.4.2:
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.2.tgz#7147550e32cdc37453380ab78d2074533966090a"
|
||||
integrity sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ==
|
||||
|
||||
nodemon@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.1.tgz#cec436f8153ad5d3e6c27c304849a06cabea71cc"
|
||||
integrity sha512-UC6FVhNLXjbbV4UzaXA3wUdbEkUZzLGgMGzmxvWAex5nzib/jhcSHVFlQODdbuUHq8SnnZ4/EABBAbC3RplvPg==
|
||||
nodemon@~2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.2.tgz#9c7efeaaf9b8259295a97e5d4585ba8f0cbe50b0"
|
||||
integrity sha512-GWhYPMfde2+M0FsHnggIHXTqPDHXia32HRhh6H0d75Mt9FKUoCBvumNHr7LdrpPBTKxsWmIEOjoN+P4IU6Hcaw==
|
||||
dependencies:
|
||||
chokidar "^3.2.2"
|
||||
debug "^3.2.6"
|
||||
@ -8245,7 +8245,7 @@ ts-invariant@^0.4.0:
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
tslib@1.10.0, tslib@^1.9.0, tslib@^1.9.3:
|
||||
tslib@1.10.0, tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
||||
814
locale/ru.json
Normal file
814
locale/ru.json
Normal file
@ -0,0 +1,814 @@
|
||||
{
|
||||
"actions": {
|
||||
"cancel": "Отменить",
|
||||
"create": "Создать",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"loading": "загрузка",
|
||||
"loadMore": "Загрузить ещё",
|
||||
"save": "Сохранить"
|
||||
},
|
||||
"admin": {
|
||||
"categories": {
|
||||
"categoryName": "Имя",
|
||||
"name": "Категории",
|
||||
"postCount": "Посты"
|
||||
},
|
||||
"dashboard": {
|
||||
"comments": "Комментарии",
|
||||
"follows": "Подписки",
|
||||
"invites": "Приглашения",
|
||||
"name": "Панель управления",
|
||||
"notifications": "Уведомления",
|
||||
"organizations": "Организации",
|
||||
"posts": "Посты",
|
||||
"projects": "Проекты",
|
||||
"shouts": "Выкрики",
|
||||
"users": "Пользователи"
|
||||
},
|
||||
"donations": {
|
||||
"goal": "Необходимы ежемесячные пожертвования",
|
||||
"name": "Информация о пожертвованиях",
|
||||
"progress": "Пожертвования собраны",
|
||||
"successfulUpdate": "Информация о пожертвованиях успешно обновлена!"
|
||||
},
|
||||
"hashtags": {
|
||||
"name": "Хэштеги",
|
||||
"nameOfHashtag": "Имя",
|
||||
"number": "№",
|
||||
"tagCount": "Сообщений",
|
||||
"tagCountUnique": "Пользователи"
|
||||
},
|
||||
"invites": {
|
||||
"description": "Приглашения — это замечательный способ завести друзей в своей сети ...",
|
||||
"name": "Пригласить пользователей",
|
||||
"title": "Пригласить людей"
|
||||
},
|
||||
"name": "Администрирование",
|
||||
"notifications": {
|
||||
"name": "Уведомления"
|
||||
},
|
||||
"organizations": {
|
||||
"name": "Организации"
|
||||
},
|
||||
"pages": {
|
||||
"name": "Страницы"
|
||||
},
|
||||
"settings": {
|
||||
"name": "Настройки"
|
||||
},
|
||||
"tags": {
|
||||
"name": "Теги",
|
||||
"tagCount": "Сообщения",
|
||||
"tagCountUnique": "Пользователи"
|
||||
},
|
||||
"users": {
|
||||
"empty": "Пользователи не найдены",
|
||||
"form": {
|
||||
"placeholder": "Электронная почта, имя или описание"
|
||||
},
|
||||
"name": "Пользователи",
|
||||
"table": {
|
||||
"columns": {
|
||||
"createdAt": "Дата создания",
|
||||
"email": "Эл. почта",
|
||||
"name": "Имя",
|
||||
"number": "№",
|
||||
"role": "Роль",
|
||||
"slug": "Slug"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"code-of-conduct": {
|
||||
"consequences": {
|
||||
"description": "Если участник сообщества проявляет неприемлемое поведение, ответственные операторы, модераторы и администраторы сети могут принять соответствующие меры, включая, но не ограничиваясь:",
|
||||
"list": {
|
||||
"0": "Просьба о немедленном прекращении неприемлемого поведения",
|
||||
"1": "Блокирование или удаление комментариев",
|
||||
"2": "Временное исключение из соответствующего поста или другого контента",
|
||||
"3": "Блокирование или удаление контента",
|
||||
"4": "Временный запрет на добавление контента",
|
||||
"5": "Временное исключение из сети",
|
||||
"6": "Окончательное исключение из сети",
|
||||
"7": "Передача сведений о нарушениях немецкого законодательства.",
|
||||
"8": "Пропаганда или поощрение такого поведения."
|
||||
},
|
||||
"title": "Последствия неприемлемого поведения"
|
||||
},
|
||||
"expected-behaviour": {
|
||||
"description": "Мы ожидаем и требуем от всех членов сообщества предерживаться следующих правил поведения:",
|
||||
"list": {
|
||||
"0": "Будьте внимательны и уважительны к тому, что пишете и делаете.",
|
||||
"1": "Пытайтесь сотрудничать, прежде чем возникнет конфликт.",
|
||||
"2": "Воздерживайтесь от поведения и высказываний, унижающих достоинство, дискриминационного или преследующего характера.",
|
||||
"3": "Будьте внимательны к своему окружению и другим участникам. Информируйте лидеров сообщества об опасных ситуациях, когда кто-либо попал в беду или нарушает настоящий Кодекс поведения, даже если они кажутся незначительными."
|
||||
},
|
||||
"title": "Ожидаемое поведение"
|
||||
},
|
||||
"get-help": "Если вы стали жертвой или свидетелем неприемлемого поведения или у вас возникли какие-либо другие проблемы, пожалуйста, как можно скорее сообщите об этом организатору сообщества и укажите ссылку на соответствующий контент:",
|
||||
"preamble": {
|
||||
"description": "Human Connection - это некоммерческая социальная сеть знаний и действий следующего поколения. Создана людьми – для людей. С открытым исходным кодом, справедливая и прозрачная. Для позитивных локальных и глобальных изменений во всех сферах жизни. Мы полностью перестраиваем публичный обмен знаниями, идеями и проектами. Функции Human Connection объединяют людей – офлайн и онлайн – так что мы можем сделать мир лучше.",
|
||||
"title": "Преамбула"
|
||||
},
|
||||
"purpose": {
|
||||
"description": "С помощью этих правил поведения мы регулируем основные принципы поведения в нашей социальной сети. При этом Устав ООН по правам человека является нашей ориентацией и лежит в основе нашего понимания ценностей. Правила поведения служат руководящими принципами для личного выступления и общения друг с другом. Любой, кто является активным пользователем в сети Human Connection, публикует сообщения, комментирует или контактирует с другими пользователями, в том числе за пределами сети, признает эти правила поведения обязательными.",
|
||||
"title": "Цель"
|
||||
},
|
||||
"subheader": "социальной сети \"Human Connection gGmbH\"",
|
||||
"unacceptable-behaviour": {
|
||||
"description": "В нашем сообществе неприемлемо следующее поведение:",
|
||||
"list": {
|
||||
"0": "Дискриминационные сообщения, комментарии, высказывания или оскорбления, в частности, касающиеся пола, сексуальной ориентации, расы, религии, политической или мировоззренческой ориентации, или инвалидности.",
|
||||
"1": "Публикация или ссылка на явно порнографические материалы.",
|
||||
"2": "Прославление или умаление жестоких, или бесчеловечных актов насилия.",
|
||||
"3": "Публикация персональных данных других лиц без их согласия или угрозы (\"Доксинг\").",
|
||||
"4": "Преднамеренное запугивание или преследование.",
|
||||
"5": "Рекламировать продукты и услуги с коммерческим намерением.",
|
||||
"6": "Преступное поведение или нарушение немецкого права.",
|
||||
"7": "Одобрение или поощрение недопустимого поведения."
|
||||
},
|
||||
"title": "Недопустимое поведение"
|
||||
}
|
||||
},
|
||||
"comment": {
|
||||
"content": {
|
||||
"unavailable-placeholder": "...этот комментарий больше не доступен"
|
||||
},
|
||||
"delete": "Удалить комментарий",
|
||||
"edit": "Редактировать комментарий",
|
||||
"edited": "Изменен",
|
||||
"menu": {
|
||||
"delete": "Удалить комментарий",
|
||||
"edit": "Редактировать комментарий"
|
||||
},
|
||||
"show": {
|
||||
"less": "показать меньше",
|
||||
"more": "показать больше"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"category": "Категория ::: Категории ::: Категории",
|
||||
"comment": "Комментарий::: Комментарии::: Комментарии",
|
||||
"letsTalk": "Давай поговорим",
|
||||
"loading": "загрузка",
|
||||
"loadMore": "Загрузить ещё",
|
||||
"moreInfo": "Больше информации",
|
||||
"name": "Имя",
|
||||
"organization": "Организация ::: Организации ::: Организации",
|
||||
"post": "Пост ::: Посты ::: Посты",
|
||||
"project": "Проект ::: Проекты ::: Проекты",
|
||||
"reportContent": "Отчет",
|
||||
"shout": "Выкрик ::: Выкрики ::: Выкрики",
|
||||
"tag": "Тег ::: Теги ::: Теги",
|
||||
"takeAction": "Принять меры",
|
||||
"user": "Пользователь ::: Пользователи ::: Пользователи",
|
||||
"validations": {
|
||||
"categories": "Выберите от одной то трех категорий",
|
||||
"email": "должен быть корректный адрес электронной почты",
|
||||
"url": "должен быть корректный URL"
|
||||
},
|
||||
"versus": "Против"
|
||||
},
|
||||
"components": {
|
||||
"enter-nonce": {
|
||||
"form": {
|
||||
"description": "Откройте папку \\\"Входящие\\\" и введите код из сообщения.",
|
||||
"next": "Продолжить",
|
||||
"nonce": "Введите код",
|
||||
"validations": {
|
||||
"length": "длина должна быть 6 символов"
|
||||
}
|
||||
}
|
||||
},
|
||||
"password-reset": {
|
||||
"change-password": {
|
||||
"error": "Смена пароля не удалась. Может быть, код безопасности был неправильным?",
|
||||
"help": "В случае возникновения проблем, не стесняйся обращаться за помощью, отправив нам письмо по адресу:",
|
||||
"success": "Смена пароля прошла успешно!"
|
||||
},
|
||||
"request": {
|
||||
"form": {
|
||||
"description": "На указанный адрес электронной почты будет отправлено сообщение с инструкциями для сброса пароля.",
|
||||
"submit": "Отправить запрос",
|
||||
"submitted": "На адрес <b>{email}<\/b>было отправлено электронное письмо с дальнейшими инструкциями"
|
||||
},
|
||||
"title": "Сбросить пароль"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
"create-user-account": {
|
||||
"error": "Не удалось создать учетную запись!",
|
||||
"help": "Может быть, подтверждение было недействительным? В случае возникновения проблем, не стесняйтесь обращаться за помощью, отправив нам письмо по электронной почте:",
|
||||
"success": "Учетная запись успешно создана!",
|
||||
"title": "Создать учетную запись"
|
||||
},
|
||||
"signup": {
|
||||
"form": {
|
||||
"data-privacy": "Я прочитал и понял <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\"><ds-text bold color=\"primary\" >Заявление о конфиденциальности<\/ds-text><\/a>",
|
||||
"description": "Для начала работы введите свой адрес электронной почты:",
|
||||
"errors": {
|
||||
"email-exists": "Уже есть учетная запись пользователя с этим адресом электронной почты!",
|
||||
"invalid-invitation-token": "Похоже, что приглашение уже было использовано. Ссылку из приглашения можно использовать только один раз."
|
||||
},
|
||||
"invitation-code": "Код приглашения: <b>{code}<\/b>",
|
||||
"minimum-age": "Мне 18 лет или более",
|
||||
"no-commercial": "У меня нет коммерческих намерений, и я не представляю коммерческое предприятие или организацию.",
|
||||
"no-political": "Я не от имени какой-либо партии или политической организации в сети.",
|
||||
"submit": "Создать учетную запись",
|
||||
"success": "Письмо со ссылкой для завершения регистрации было отправлено на <b> {email} <\/b>",
|
||||
"terms-and-condition": "Принимаю <a href=\"\/terms-and-conditions\"><ds-text bold color=\"primary\" >Условия и положения<\/ds-text><\/a>."
|
||||
},
|
||||
"title": "Присоединяйся к Human Connection!",
|
||||
"unavailable": "К сожалению, публичная регистрация пользователей на этом сервере сейчас недоступна."
|
||||
}
|
||||
}
|
||||
},
|
||||
"contribution": {
|
||||
"categories": {
|
||||
"infoSelectedNoOfMaxCategories": "Выбрано {chosen} из {max} категорий"
|
||||
},
|
||||
"category": {
|
||||
"name": {
|
||||
"animal-protection": "Защита животных",
|
||||
"art-culture-sport": "Искусство, культура и спорт",
|
||||
"consumption-sustainability": "Потребление и стабильность",
|
||||
"cooperation-development": "Сотрудничество и развитие",
|
||||
"democracy-politics": "Демократия и политика",
|
||||
"economy-finances": "Экономика и финансы",
|
||||
"education-sciences": "Образование и наука",
|
||||
"energy-technology": "Энергия и технологии",
|
||||
"environment-nature": "Окружающая среда и природа",
|
||||
"freedom-of-speech": "Свобода слова",
|
||||
"global-peace-nonviolence": "Глобальный мир и борьба с насилием",
|
||||
"happiness-values": "Счастье и ценности",
|
||||
"health-wellbeing": "Здоровье и благополучие",
|
||||
"human-rights-justice": "Права человека и справедливость",
|
||||
"it-internet-data-privacy": "ИТ, интернет и конфиденциальность",
|
||||
"just-for-fun": "Просто для удовольствия"
|
||||
}
|
||||
},
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"emotions-label": {
|
||||
"angry": "Возмутительно",
|
||||
"cry": "Плачу",
|
||||
"funny": "Смешно",
|
||||
"happy": "Счастлив",
|
||||
"surprised": "Удивлен"
|
||||
},
|
||||
"filterALL": "Просмотреть все посты",
|
||||
"filterFollow": "Показать сообщения пользователей, на которых я подписан",
|
||||
"languageSelectLabel": "Язык",
|
||||
"languageSelectText": "Выберите язык",
|
||||
"newPost": "Создать пост",
|
||||
"success": "Сохранено!",
|
||||
"teaserImage": {
|
||||
"cropperConfirm": "Подтвердить"
|
||||
},
|
||||
"title": "Заголовок"
|
||||
},
|
||||
"delete": {
|
||||
"cancel": "Отменить",
|
||||
"comment": {
|
||||
"message": "Ты уверены, что хочешь удалить комментарий \"<b>{name}<\/b>\"?",
|
||||
"success": "Комментарий успешно удален!",
|
||||
"title": "Удалить комментарий",
|
||||
"type": "Комментарий"
|
||||
},
|
||||
"contribution": {
|
||||
"message": "Вы уверены, что хотите удалить пост \"<b>{name}<\/b>\"?",
|
||||
"success": "Пост успешно удален!",
|
||||
"title": "Удалить пост",
|
||||
"type": "Пост"
|
||||
},
|
||||
"submit": "Удалить"
|
||||
},
|
||||
"disable": {
|
||||
"cancel": "Отменить",
|
||||
"comment": {
|
||||
"message": "Вы действительно хотите отключить комментарий от «<b>{name}<\/b>»?",
|
||||
"title": "Отключить комментарий",
|
||||
"type": "Комментарий"
|
||||
},
|
||||
"contribution": {
|
||||
"message": "Вы действительно хотите отключить пост «<b>{name}<\/b>»?",
|
||||
"title": "Отключить пост",
|
||||
"type": "Пост"
|
||||
},
|
||||
"submit": "Отключить",
|
||||
"success": "Успешно отключен",
|
||||
"user": {
|
||||
"message": "Вы действительно хотите отключить пользователя «<b>{name}<\/b>»?",
|
||||
"title": "Отключить пользователя",
|
||||
"type": "Пользователь"
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
"amount-of-total": "{amount} из {total} € собрано",
|
||||
"donate-now": "Пожертвуйте сейчас",
|
||||
"donations-for": "Пожертвования для"
|
||||
},
|
||||
"editor": {
|
||||
"embed": {
|
||||
"always_allow": "Всегда отображать содержимое сторонних производителей (эту настройку можно изменить в любое время).",
|
||||
"data_privacy_info": "Ваши данные еще не были переданы третьим лицам. Если вы воспроизведёте это видео, следующий провайдер, вероятно, зарегистрирует ваши данные пользователя:",
|
||||
"data_privacy_warning": "Предупреждение о конфиденциальности данных!",
|
||||
"play_now": "Смотреть сейчас"
|
||||
},
|
||||
"hashtag": {
|
||||
"addHashtag": "Новый хэштег",
|
||||
"addLetter": "Введите букву",
|
||||
"noHashtagsFound": "Хэштеги не найдены"
|
||||
},
|
||||
"mention": {
|
||||
"noUsersFound": "Пользователи не найдены"
|
||||
},
|
||||
"placeholder": "Поделитесь своими вдохновляющими мыслями ..."
|
||||
},
|
||||
"filter-menu": {
|
||||
"clearSearch": "Очистить поиск",
|
||||
"hashtag-search": "Поиск по #{hashtag}",
|
||||
"title": "Ваш фильтр пузыря"
|
||||
},
|
||||
"filter-posts": {
|
||||
"categories": {
|
||||
"all": "Все",
|
||||
"header": "Категории"
|
||||
},
|
||||
"followers": {
|
||||
"label": "Мои подписки"
|
||||
},
|
||||
"general": {
|
||||
"header": "Другие фильтры"
|
||||
},
|
||||
"language": {
|
||||
"all": "Все",
|
||||
"header": "Языки"
|
||||
}
|
||||
},
|
||||
"followButton": {
|
||||
"follow": "Подписаться",
|
||||
"following": "Вы подписаны"
|
||||
},
|
||||
"index": {
|
||||
"change-filter-settings": "Измените настройки фильтра, чтобы получить больше результатов.",
|
||||
"no-results": "Посты не найдены."
|
||||
},
|
||||
"login": {
|
||||
"copy": "Авторизуйтесь, если у вас уже есть учетная запись Human Connection.",
|
||||
"email": "Электронная почта",
|
||||
"failure": "Неверный адрес электронной почты или пароль.",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"hello": "Привет",
|
||||
"login": "Вход",
|
||||
"logout": "Выйти",
|
||||
"moreInfo": "Что такое Human Connection?",
|
||||
"moreInfoHint": "на страницу проекта",
|
||||
"moreInfoURL": "https:\/\/human-connection.org\/en\/",
|
||||
"no-account": "У вас нет аккаунта?",
|
||||
"password": "Пароль",
|
||||
"register": "Зарегистрируйтесь",
|
||||
"success": "Вы вошли в систему!"
|
||||
},
|
||||
"maintenance": {
|
||||
"explanation": "В данный момент мы проводим плановое техническое обслуживание, пожалуйста, повторите попытку позже.",
|
||||
"questions": "Любые вопросы или сообщения о проблемах отправляйте на электронную почту",
|
||||
"title": "Human Connection на техническом обслуживании"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "Модерация",
|
||||
"reports": {
|
||||
"author": "Автор",
|
||||
"content": "Содержа́ние",
|
||||
"decideButton": "Подтвердить",
|
||||
"decided": "Решил",
|
||||
"decideModal": {
|
||||
"cancel": "Отменить",
|
||||
"Comment": {
|
||||
"disable": {
|
||||
"message": "Ты действительно хочешь, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
|
||||
"title": "Окончательно отключить комментарий"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Ты действительно хочешь, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?",
|
||||
"title": "Окончательно включить комментарий"
|
||||
}
|
||||
},
|
||||
"Post": {
|
||||
"disable": {
|
||||
"message": "Ты действительно хочешь, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
|
||||
"title": "Окончательно отключить пост"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Ты действительно хочешь, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?",
|
||||
"title": "Окончательно включить пост"
|
||||
}
|
||||
},
|
||||
"submit": "Подтвердить решение",
|
||||
"User": {
|
||||
"disable": {
|
||||
"message": "Ты действительно хочешь, чтобы пользователь \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?",
|
||||
"title": "Окончательно отключить пользователя"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Ты уверены, что хочешь поделиться пользователем \"<b>{name}<\/b>\"?",
|
||||
"title": "Окончательно включить пост"
|
||||
}
|
||||
}
|
||||
},
|
||||
"decision": "Решение",
|
||||
"DecisionSuccess": "Решил успешно!",
|
||||
"disabled": "Отключен",
|
||||
"disabledAt": "Отключено на",
|
||||
"disabledBy": "Отключил(а)",
|
||||
"empty": "Поздравляю, модерировать нечего.",
|
||||
"enabled": "Включен",
|
||||
"enabledAt": "Включено на",
|
||||
"enabledBy": "Включено с",
|
||||
"filterLabel": {
|
||||
"all": "Все",
|
||||
"closed": "Закрыто",
|
||||
"reviewed": "Рассмотренный",
|
||||
"unreviewed": "Нерассмотренный"
|
||||
},
|
||||
"moreDetails": "Посмотреть подробности",
|
||||
"name": "Отчеты",
|
||||
"noDecision": "Нет решения!",
|
||||
"numberOfUsers": "{count} пользователи",
|
||||
"previousDecision": "Предыдущее решение:",
|
||||
"reasonCategory": "Категория",
|
||||
"reasonDescription": "Описание",
|
||||
"reportedOn": "Дата",
|
||||
"reporter": "Сообщил(а)",
|
||||
"status": "Текущее состояние",
|
||||
"submitter": "Сообщил(а)"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"comment": "Комментарий",
|
||||
"content": "Контент",
|
||||
"empty": "Извините, на данный момент у вас нет уведомлений.",
|
||||
"filterLabel": {
|
||||
"all": "Все",
|
||||
"read": "Прочитанные",
|
||||
"unread": "Непрочитанные"
|
||||
},
|
||||
"pageLink": "Все уведомления",
|
||||
"post": "Пост",
|
||||
"reason": {
|
||||
"commented_on_post": "Комментарий к посту...",
|
||||
"mentioned_in_comment": "Упоминание в комментарии....",
|
||||
"mentioned_in_post": "Упоминание в посте...."
|
||||
},
|
||||
"title": "Уведомления",
|
||||
"user": "Пользователь"
|
||||
},
|
||||
"post": {
|
||||
"comment": {
|
||||
"submit": "Комментировать",
|
||||
"submitted": "Комментарий отправлен",
|
||||
"updated": "Изменения сохраненные"
|
||||
},
|
||||
"edited": "Изменен",
|
||||
"menu": {
|
||||
"delete": "Удалить пост",
|
||||
"edit": "Редактировать пост",
|
||||
"pin": "Закрепить пост",
|
||||
"pinnedSuccessfully": "Пост больше не закреплен!",
|
||||
"unpin": "Открепить пост",
|
||||
"unpinnedSuccessfully": "Сообщение успешно не закреплено!"
|
||||
},
|
||||
"moreInfo": {
|
||||
"description": "Здесь содержится дополнительная информация по теме.",
|
||||
"name": "Дополнительная информация",
|
||||
"title": "Дополнительная информация",
|
||||
"titleOfCategoriesSection": "Категории",
|
||||
"titleOfHashtagsSection": "Хэштеги",
|
||||
"titleOfRelatedContributionsSection": "Похожие посты"
|
||||
},
|
||||
"name": "Пост",
|
||||
"pinned": "Объявление",
|
||||
"takeAction": {
|
||||
"name": "Действовать"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"commented": "Прокомментированные",
|
||||
"follow": "Подписаться",
|
||||
"followers": "Подписчики",
|
||||
"following": "Подписки",
|
||||
"invites": {
|
||||
"description": "Введите адрес электронной почты для приглашения.",
|
||||
"emailPlaceholder": "Электронная почта для приглашения",
|
||||
"title": "Пригласите кого-нибудь в Human Connection!"
|
||||
},
|
||||
"memberSince": "Участник с",
|
||||
"name": "Мой профиль",
|
||||
"network": {
|
||||
"andMore": "и ещё {number} человек... ::: и ещё {number} человека... ::: и ещё {number} человек...",
|
||||
"followedBy": "ваши подписчики:",
|
||||
"followedByNobody": "у вас нет подписчиков.",
|
||||
"following": "подписан на:",
|
||||
"followingNobody": "ни на кого не подписан.",
|
||||
"title": "Сеть"
|
||||
},
|
||||
"shouted": "С выкриками",
|
||||
"socialMedia": "Где еще я могу найти",
|
||||
"userAnonym": "Анонимный"
|
||||
},
|
||||
"quotes": {
|
||||
"african": {
|
||||
"author": "Африканская пословица",
|
||||
"quote": "Много маленьких людей делают много маленьких вещей во многих маленьких местах, что может изменить мир до неузнаваемости."
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"cancel": "Отменить",
|
||||
"comment": {
|
||||
"error": "Вы уже сообщили о комментарии!",
|
||||
"message": "Вы уверены, что хотите показать комментарий \"<b>{name}<\/b>\"?",
|
||||
"title": "Показать комментарий",
|
||||
"type": "Комментарий"
|
||||
},
|
||||
"contribution": {
|
||||
"error": "Вы уже сообщили о посте!",
|
||||
"message": "Вы уверены, что хотите показать пост \"<b>{name}<\/b>\"?",
|
||||
"title": "Показать пост",
|
||||
"type": "Пост"
|
||||
},
|
||||
"submit": "Показать",
|
||||
"success": "Успешно показан!",
|
||||
"user": {
|
||||
"error": "Вы уже сообщили о пользователе!",
|
||||
"message": "Вы уверены, что хотите показать пользователя \"<b>{name}<\/b>\"?",
|
||||
"title": "Показать пользователя",
|
||||
"type": "Пользователь"
|
||||
}
|
||||
},
|
||||
"report": {
|
||||
"cancel": "Отменить",
|
||||
"comment": {
|
||||
"error": "Вы уже сообщили о посте!",
|
||||
"message": "Вы действительно хотите сообщить о посте \"<b> {name} <\/b>\"?",
|
||||
"title": "Пожаловаться на комментарий",
|
||||
"type": "Комментарий"
|
||||
},
|
||||
"contribution": {
|
||||
"error": "Вы уже сообщили о посте!",
|
||||
"message": "Вы действительно хотите сообщить о посте \"<b>{name}<\/b>\"?",
|
||||
"title": "Пожаловаться на пост",
|
||||
"type": "Пожаловаться на пост"
|
||||
},
|
||||
"reason": {
|
||||
"category": {
|
||||
"invalid": "Пожалуйста, выберите подходящую категорию",
|
||||
"label": "Выберите категорию:",
|
||||
"options": {
|
||||
"advert_products_services_commercial": "Реклама продуктов и услуг с коммерческим намерением.",
|
||||
"criminal_behavior_violation_german_law": "Уголовное поведение или нарушении немецкого права.",
|
||||
"discrimination_etc": "Дискриминационные посты, комментарии, заявления или оскорбления.",
|
||||
"doxing": "Публикация персональных данных других лиц без их согласия или угроза публикации (\"Доксинг\").",
|
||||
"glorific_trivia_of_cruel_inhuman_acts": "Прославление или умаление жестоких, или бесчеловечных актов насилия.",
|
||||
"intentional_intimidation_stalking_persecution": "Преднамеренное запугивание или преследование.",
|
||||
"other": "Другое ...",
|
||||
"pornographic_content_links": "Публикация или ссылка на явно порнографический материал."
|
||||
},
|
||||
"placeholder": "Категория ..."
|
||||
},
|
||||
"description": {
|
||||
"label": "Пожалуйста, объясните, почему хотите об этом сообщить?",
|
||||
"placeholder": "Дополнительная информация ..."
|
||||
}
|
||||
},
|
||||
"submit": "Отправить",
|
||||
"success": "Спасибо за сообщение!",
|
||||
"user": {
|
||||
"error": "Вы уже сообщили о пользователе!",
|
||||
"message": "Вы действительно хотите сообщить о пользователе \"<b>{name}<\/b>\"?",
|
||||
"title": "Пожаловаться на пользователя",
|
||||
"type": "Пользователь"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"failed": "Ничего не найдено",
|
||||
"hint": "Что вы хотите найти?",
|
||||
"placeholder": "Поиск"
|
||||
},
|
||||
"settings": {
|
||||
"blocked-users": {
|
||||
"block": "Блокировать",
|
||||
"columns": {
|
||||
"name": "Имя",
|
||||
"slug": "Псевдоним",
|
||||
"unblock": "Разблокировать"
|
||||
},
|
||||
"empty": "Вы пока никого не блокировали.",
|
||||
"explanation": {
|
||||
"closing": "На данный момент этого должно быть достаточно, чтобы заблокированные пользователи больше вас не беспокоили.",
|
||||
"intro": "Если блокируете другого пользователя, происходит следующее:",
|
||||
"notifications": "Заблокированные пользователи больше не будут получать уведомления об упоминаниях в ваших постах.",
|
||||
"search": "Посты заблокированных пользователей не отображаются в результатах поиска.",
|
||||
"their-perspective": "И наоборот — заблокированный пользователь больше не видит ваши посты в своей ленте.",
|
||||
"your-perspective": "Посты заблокированного пользователя не отображаются в персональной ленте."
|
||||
},
|
||||
"how-to": "Вы можете блокировать других пользователей на странице их профиля с помощью меню профиля.",
|
||||
"name": "Заблокированные пользователи",
|
||||
"unblock": "Разблокировать пользователей",
|
||||
"unblocked": "{name} - снова разблокирован"
|
||||
},
|
||||
"data": {
|
||||
"labelBio": "О себе",
|
||||
"labelCity": "Город или регион",
|
||||
"labelName": "Имя",
|
||||
"labelSlug": "Уникальное имя пользователя",
|
||||
"name": "Персональные данные",
|
||||
"namePlaceholder": "Маша Медведева",
|
||||
"success": "Персональные данные были успешно обновлены!"
|
||||
},
|
||||
"delete": {
|
||||
"name": "Удалить аккаунт"
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Обратите внимание, что ваши сообщения и комментарии важны для сообщества. Если вы все равно хотите их удалить, то вы должны отметить соответствующие опции ниже.",
|
||||
"accountWarning": "Вы <b>НЕ СМОЖЕТЕ<\/b> восстановить свой аккаунт, посты или комментарии после удаления.",
|
||||
"commentedCount": "Удалить мои комментарии: {count}",
|
||||
"contributionsCount": "Удалить мои посты: {count}",
|
||||
"name": "Удалить данные",
|
||||
"pleaseConfirm": "<b class='is-danger'>Разрушительное действие!<\/b> Введите <b>{confirm}<\/b> для подтверждения.",
|
||||
"success": "Аккаунт успешно удален!"
|
||||
},
|
||||
"download": {
|
||||
"name": "Скачать данные"
|
||||
},
|
||||
"email": {
|
||||
"change-successful": "Адрес электронной почты был успешно изменен.",
|
||||
"labelEmail": "Адрес электронной почты",
|
||||
"labelNewEmail": "Новый адрес электронной почты",
|
||||
"labelNonce": "Введите свой код",
|
||||
"name": "Электронная почта",
|
||||
"submitted": "Электронное письмо с подтверждением отправлено на <b>{email}<\/b>.",
|
||||
"success": "Новый адрес электронной почты был зарегистрирован.",
|
||||
"validation": {
|
||||
"same-email": "Это текущий адрес электронной почты."
|
||||
},
|
||||
"verification-error": {
|
||||
"explanation": "Причины могут быть разными:",
|
||||
"message": "Адрес электронной почты не может быть изменен.",
|
||||
"reason": {
|
||||
"invalid-nonce": "Правильно ли указан код подтверждения?",
|
||||
"no-email-request": "Вы уверены, что отправляли запрос на изменение своего адреса электронной почты?"
|
||||
},
|
||||
"support": "Если проблема сохраняется, пожалуйста, свяжитесь с нами по электронной почте"
|
||||
}
|
||||
},
|
||||
"embeds": {
|
||||
"info-description": "Вот список сторонних провайдеров, чей контент может отображаться в форме вставок кода, например, в виде встроенных видео:",
|
||||
"name": "Сторонний контент",
|
||||
"status": {
|
||||
"change": {
|
||||
"allow": "Конечно.",
|
||||
"deny": "Нет, не надо",
|
||||
"question": "Вы хотите, чтобы вставки кода сторонних провайдеров всегда отображались?"
|
||||
},
|
||||
"description": "Значение по умолчанию -",
|
||||
"disabled": {
|
||||
"off": "сначала не отображать вставки кода сторонних провайдеров",
|
||||
"on": "сразу отображать вставки кода сторонних провайдеров"
|
||||
}
|
||||
}
|
||||
},
|
||||
"invites": {
|
||||
"name": "Приглашения"
|
||||
},
|
||||
"languages": {
|
||||
"name": "Языки"
|
||||
},
|
||||
"name": "Настройки",
|
||||
"organizations": {
|
||||
"name": "Мои организации"
|
||||
},
|
||||
"privacy": {
|
||||
"make-shouts-public": "Публиковать в моем публичном профиле статьи в которых я участвовал",
|
||||
"name": "Конфиденциальность",
|
||||
"success-update": "Настройки приватности сохранены"
|
||||
},
|
||||
"security": {
|
||||
"change-password": {
|
||||
"button": "Изменить пароль",
|
||||
"label-new-password": "Новый пароль",
|
||||
"label-new-password-confirm": "Подтверждение пароля",
|
||||
"label-old-password": "Старый пароль",
|
||||
"message-new-password-confirm-required": "Требуется подтверждение пароля",
|
||||
"message-new-password-missmatch": "Пароли не совпадают",
|
||||
"message-new-password-required": "Требуется новый пароль",
|
||||
"message-old-password-required": "Требуется свой старый пароль",
|
||||
"passwordSecurity": "Безопасность пароля",
|
||||
"passwordStrength0": "Очень небезопасный",
|
||||
"passwordStrength1": "Небезопасный",
|
||||
"passwordStrength2": "Посредственный",
|
||||
"passwordStrength3": "Надежный",
|
||||
"passwordStrength4": "Очень надежный",
|
||||
"success": "Пароль успешно изменен!"
|
||||
},
|
||||
"name": "Безопасность"
|
||||
},
|
||||
"social-media": {
|
||||
"name": "Социальные Медиа",
|
||||
"placeholder": "Ссылка на профиль социальной сети",
|
||||
"requireUnique": "Cсылка уже существует",
|
||||
"submit": "Добавить ссылку",
|
||||
"successAdd": "Добавлены социальные медиа. Профиль обновлен!",
|
||||
"successDelete": "Социальные мадиа удалены. Профиль обновлен!"
|
||||
},
|
||||
"validation": {
|
||||
"slug": {
|
||||
"alreadyTaken": "Это имя пользователя уже занято.",
|
||||
"regex": "Допускаются только строчные буквы, цифры, подчеркивания или дефисы."
|
||||
}
|
||||
}
|
||||
},
|
||||
"shoutButton": {
|
||||
"shouted": "выкрикнули"
|
||||
},
|
||||
"site": {
|
||||
"back-to-login": "Вернуться на страницу входа",
|
||||
"bank": "банковский счет",
|
||||
"changelog": "Изменения",
|
||||
"code-of-conduct": "Кодекс поведения",
|
||||
"contact": "Контакт",
|
||||
"data-privacy": "Конфиденциальность",
|
||||
"director": "Управляющий директор",
|
||||
"error-occurred": "Произошла ошибка.",
|
||||
"faq": "ЧаВо (FAQ)",
|
||||
"germany": "Германия",
|
||||
"imprint": "Импрессум",
|
||||
"made": "Сделано с ❤",
|
||||
"register": "Регистрационный номер",
|
||||
"responsible": "ответственный за содержание этой страницы (§ 55 Abs. 2 RStV)",
|
||||
"taxident": "UST-ID. в соответствии с §27a Закона о налоге с продаж Германии:",
|
||||
"termsAndConditions": "Условия и положения",
|
||||
"thanks": "Спасибо!",
|
||||
"tribunal": "Суд регистрации"
|
||||
},
|
||||
"store": {
|
||||
"posts": {
|
||||
"orderBy": {
|
||||
"newest": {
|
||||
"label": "Сначала новые"
|
||||
},
|
||||
"oldest": {
|
||||
"label": "Сначала старые"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"termsAndConditions": {
|
||||
"addition": {
|
||||
"description": "<a href=\"https:\/\/human-connection.org\/events\/\" target=\"_blank\" > https:\/\/human-connection.org\/events\/ <\/a>",
|
||||
"title": "Кроме того, мы регулярно проводим мероприятия, где вы также можете\\nподелиться своими впечатлениями и задать вопросы. Информацию о текущих событиях можно найти здесь:"
|
||||
},
|
||||
"agree": "Я согласен(на)!",
|
||||
"code-of-conduct": {
|
||||
"description": "Наш кодекс поведения служит руководством для личного поведения и взаимодействия друг с другом. Каждый пользователь социальной сети Human Connection, который пишет статьи, комментирует или вступает в контакт с другими пользователями, даже за пределами сети, признает эти правила поведения обязательными. <a href=\"https:\/\/alpha.human-connection.org\/code-of-conduct\" target=\"_blank\"> https:\/\/alpha.human-connection.org\/code-of-conduct<\/a>",
|
||||
"title": "Кодекс поведения"
|
||||
},
|
||||
"errors-and-feedback": {
|
||||
"description": "Мы прилагаем все усилия для обеспечения безопасности и доступности нашей сети и данных. Каждый новый выпуск программного обеспечения проходит как автоматическое, так и ручное тестирование. Однако могут возникнуть непредвиденные ошибки. Поэтому мы благодарны за любые обнаруженные ошибки. Вы можете сообщить о любых обнаруженных ошибках, отправив электронное письмо в службу поддержки по адресу support@human-connection.org",
|
||||
"title": "Ошибки и обратная связь"
|
||||
},
|
||||
"help-and-questions": {
|
||||
"description": "Для справки и вопросов мы собрали для вас исчерпывающую подборку часто задаваемых вопросов и ответов (FAQ). Вы можете найти их здесь: <a href=\"https:\/\/support.human-connection.org\/kb\/\" target=\"_blank\" > https:\/\/support.human-connection.org\/kb\/ <\/a>",
|
||||
"title": "Помощь и вопросы"
|
||||
},
|
||||
"moderation": {
|
||||
"description": "Пока наши финансовые возможности не позволяют нам реализовать полноценную систему модерации, поэтому мы осуществляем упрощенную модерацию собственными силами и с помощью волонтёров. Мы специально обучаем этих модераторов, поэтому только они принимают соответствующие решения. Модераторы действуют анонимно. Вы можете сообщать нам о постах, комментариях и пользователях (например, если они предоставляют информацию в своем профиле или имеют изображения, которые нарушают настоящие Условия использования). При обращении вы можете указать причину и дать краткое пояснение. Мы рассмотрим обращение и применим санкции в случае необходимости, например, путем блокировки постов, комментариев или пользователей. К сожалению, в настоящее время ни вы ни пострадавший пользователь не получите от нас обратной связи, но мы планируем ряд улучшений в этом направлении. Несмотря на это, мы оставляем за собой право на применение санкций по причинам, которые не могут быть или ещё не указаны в нашем Кодексе поведения или настоящих Условиях использования.",
|
||||
"title": "Модерация"
|
||||
},
|
||||
"newTermsAndConditions": "Новые условия и положения",
|
||||
"no-commercial-use": {
|
||||
"description": "Использование Human Connection сети не допускается в коммерческих целях. Это включает, но не ограничивается рекламой продуктов с коммерческими целями, размещением партнерских ссылок, прямым привлечением пожертвований или предоставлением финансовой поддержки для целей, которые не признаются благотворительными для целей налогообложения.",
|
||||
"title": "Нет коммерческого использования"
|
||||
},
|
||||
"privacy-statement": {
|
||||
"description": "Наша сеть — это социальная сеть знаний и действий. Поэтому для нас особенно важно, чтобы как можно больше контента было общедоступным. В процессе развития нашей сети будет добавлено больше возможностей для управления видимостью личных данных. Об этих новых функциях мы сообщим дополнительно. В противном случае вы должны думать о том, какие личные данные вы раскрываете о себе (или других). Это особенно актуально для содержания постов и комментариев, поскольку они имеют в основном общедоступный характер. Позже появятся возможности ограничения видимости вашего профиля. Часть условий использования — это наша политика конфиденциальности, которая информирует вас об обработке персональных данных в нашей сети: <a href=\"https:\/\/human-connection.org\/datenschutz\/#netzwerk\" target=\"_blank\">https:\/\/human-connection.org\/datenschutz\/#netzwerk<\/a> или <a href=\"https:\/\/human-connection.org\/datenschutz\/\" target=\"_blank\">https:\/\/human-connection.org\/datenschutz<\/a>. Наше заявление о конфиденциальности корректируется в соответствии с законодательством и характеристиками нашей сети и является действительной в настоящей версии.",
|
||||
"title": "Заявление о конфиденциальности"
|
||||
},
|
||||
"terms-of-service": {
|
||||
"description": "Следующие условия использования являются основой для использования нашей сети. При регистрации вы должны принять их, а мы при необходимости сообщим вам об изменениях. Сеть Human Connection работает в Германии и поэтому регулируется немецким законодательством. Место юрисдикции - Kirchheim \/ Teck. Подробности в выходных данных: <a href=\"https:\/\/human-connection.org\/en\/imprint\" target=\"_blank\">https:\/\/human-connection.org\/en\/imprint<\/a>.",
|
||||
"title": "Условия обслуживания"
|
||||
},
|
||||
"termsAndConditionsConfirmed": "Я прочитал(а) и подтверждаю <a href=\"\/terms-and-conditions\" target=\"_blank\">Условия и положения<\/a>.",
|
||||
"termsAndConditionsNewConfirm": "Я прочитал(а) и согласен(на) с новыми условиями.",
|
||||
"termsAndConditionsNewConfirmText": "Пожалуйста, ознакомьтесь с новыми условиями использования!",
|
||||
"use-and-license": {
|
||||
"description": "Если размещаемый в сети контент защищен правами на интеллектуальную собственность, вы предоставляете нам неисключительную, передаваемую, сублицензируемую и всемирную лицензию на использование этого контента для публикации в нашей сети. Эта лицензия заканчивается, как только вы удаляете свой контент или учетную запись. Помните, что другие пользователи могут продолжать делиться вашим контентом, и мы не можем его удалить.",
|
||||
"title": "Использование и лицензия"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Успешная загрузка!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "human-connection",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||
"author": "Human Connection gGmbh",
|
||||
"license": "MIT",
|
||||
@ -29,7 +29,7 @@
|
||||
"codecov": "^3.6.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"cucumber": "^6.0.5",
|
||||
"cypress": "^3.7.0",
|
||||
"cypress": "^3.8.0",
|
||||
"cypress-cucumber-preprocessor": "^1.18.0",
|
||||
"cypress-file-upload": "^3.5.1",
|
||||
"cypress-plugin-retries": "^1.5.0",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
ROOT_DIR=$(dirname "$0")/..
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
# BUILD_COMMIT=${TRAVIS_COMMIT:-$(git rev-parse HEAD)}
|
||||
|
||||
IFS='.' read -r major minor patch < $ROOT_DIR/VERSION
|
||||
@ -24,7 +23,7 @@ do
|
||||
for tag in "${tags[@]}"
|
||||
do
|
||||
TARGET="humanconnection/${app}:${tag}"
|
||||
if docker manifest inspect $TARGET >/dev/null; then
|
||||
if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect $TARGET >/dev/null; then
|
||||
echo "docker image ${TARGET} already present, skipping ..."
|
||||
else
|
||||
echo -e "docker tag $SOURCE $TARGET\ndocker push $TARGET"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:13.1.0-alpine as base
|
||||
FROM node:13.3.0-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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM node:13.1.0-alpine as build
|
||||
FROM node:13.3.0-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
|
||||
|
||||
@ -22,10 +22,6 @@ describe('ContentMenu.vue', () => {
|
||||
locale: () => 'en',
|
||||
},
|
||||
$router: {
|
||||
resolve: jest.fn(obj => {
|
||||
obj.href = '/post/edit/d23a4265-f5f7-4e17-9f86-85f714b4b9f8'
|
||||
return obj
|
||||
}),
|
||||
push: jest.fn(),
|
||||
},
|
||||
}
|
||||
@ -76,7 +72,7 @@ describe('ContentMenu.vue', () => {
|
||||
.at(0)
|
||||
.find('span.ds-menu-item-link')
|
||||
.attributes('to'),
|
||||
).toBe('/post/edit/d23a4265-f5f7-4e17-9f86-85f714b4b9f8')
|
||||
).toBe('/post-edit-id')
|
||||
})
|
||||
|
||||
it('can delete the contribution', () => {
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<base-icon :name="item.route.icon" />
|
||||
{{ item.route.name }}
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
</div>
|
||||
@ -58,17 +58,15 @@ export default {
|
||||
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,
|
||||
label: this.$t(`post.menu.edit`),
|
||||
name: 'post-edit-id',
|
||||
params: {
|
||||
id: this.resource.id,
|
||||
},
|
||||
icon: 'edit',
|
||||
})
|
||||
routes.push({
|
||||
name: this.$t(`post.menu.delete`),
|
||||
label: this.$t(`post.menu.delete`),
|
||||
callback: () => {
|
||||
this.openModal('confirm', 'delete')
|
||||
},
|
||||
@ -79,7 +77,7 @@ export default {
|
||||
if (this.isAdmin) {
|
||||
if (!this.resource.pinnedBy) {
|
||||
routes.push({
|
||||
name: this.$t(`post.menu.pin`),
|
||||
label: this.$t(`post.menu.pin`),
|
||||
callback: () => {
|
||||
this.$emit('pinPost', this.resource)
|
||||
},
|
||||
@ -87,7 +85,7 @@ export default {
|
||||
})
|
||||
} else {
|
||||
routes.push({
|
||||
name: this.$t(`post.menu.unpin`),
|
||||
label: this.$t(`post.menu.unpin`),
|
||||
callback: () => {
|
||||
this.$emit('unpinPost', this.resource)
|
||||
},
|
||||
@ -99,14 +97,14 @@ export default {
|
||||
|
||||
if (this.isOwner && this.resourceType === 'comment') {
|
||||
routes.push({
|
||||
name: this.$t(`comment.menu.edit`),
|
||||
label: this.$t(`comment.menu.edit`),
|
||||
callback: () => {
|
||||
this.$emit('showEditCommentMenu', true)
|
||||
},
|
||||
icon: 'edit',
|
||||
})
|
||||
routes.push({
|
||||
name: this.$t(`comment.menu.delete`),
|
||||
label: this.$t(`comment.menu.delete`),
|
||||
callback: () => {
|
||||
this.openModal('confirm', 'delete')
|
||||
},
|
||||
@ -116,7 +114,7 @@ export default {
|
||||
|
||||
if (!this.isOwner) {
|
||||
routes.push({
|
||||
name: this.$t(`report.${this.resourceType}.title`),
|
||||
label: this.$t(`report.${this.resourceType}.title`),
|
||||
callback: () => {
|
||||
this.openModal('report')
|
||||
},
|
||||
@ -127,7 +125,7 @@ export default {
|
||||
if (!this.isOwner && this.isModerator) {
|
||||
if (!this.resource.disabled) {
|
||||
routes.push({
|
||||
name: this.$t(`disable.${this.resourceType}.title`),
|
||||
label: this.$t(`disable.${this.resourceType}.title`),
|
||||
callback: () => {
|
||||
this.openModal('disable')
|
||||
},
|
||||
@ -135,7 +133,7 @@ export default {
|
||||
})
|
||||
} else {
|
||||
routes.push({
|
||||
name: this.$t(`release.${this.resourceType}.title`),
|
||||
label: this.$t(`release.${this.resourceType}.title`),
|
||||
callback: () => {
|
||||
this.openModal('release')
|
||||
},
|
||||
@ -147,14 +145,14 @@ export default {
|
||||
if (this.resourceType === 'user') {
|
||||
if (this.isOwner) {
|
||||
routes.push({
|
||||
name: this.$t(`settings.name`),
|
||||
label: this.$t(`settings.name`),
|
||||
path: '/settings',
|
||||
icon: 'edit',
|
||||
})
|
||||
} else {
|
||||
if (this.resource.isBlocked) {
|
||||
routes.push({
|
||||
name: this.$t(`settings.blocked-users.unblock`),
|
||||
label: this.$t(`settings.blocked-users.unblock`),
|
||||
callback: () => {
|
||||
this.$emit('unblock', this.resource)
|
||||
},
|
||||
@ -162,7 +160,7 @@ export default {
|
||||
})
|
||||
} else {
|
||||
routes.push({
|
||||
name: this.$t(`settings.blocked-users.block`),
|
||||
label: this.$t(`settings.blocked-users.block`),
|
||||
callback: () => {
|
||||
this.$emit('block', this.resource)
|
||||
},
|
||||
@ -186,7 +184,7 @@ export default {
|
||||
if (route.callback) {
|
||||
route.callback()
|
||||
} else {
|
||||
this.$router.push(route.path)
|
||||
this.$router.push(route)
|
||||
}
|
||||
toggleMenu()
|
||||
},
|
||||
|
||||
@ -51,23 +51,21 @@
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
|
||||
</ds-text>
|
||||
<client-only>
|
||||
<hc-editor
|
||||
:users="users"
|
||||
:value="form.content"
|
||||
:hashtags="hashtags"
|
||||
@input="updateEditorContent"
|
||||
/>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.content" color="danger" size="base">
|
||||
{{ contentLength }}
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">
|
||||
{{ contentLength }}
|
||||
</ds-chip>
|
||||
</ds-text>
|
||||
</client-only>
|
||||
<hc-editor
|
||||
:users="users"
|
||||
:value="form.content"
|
||||
:hashtags="hashtags"
|
||||
@input="updateEditorContent"
|
||||
/>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.content" color="danger" size="base">
|
||||
{{ contentLength }}
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">
|
||||
{{ contentLength }}
|
||||
</ds-chip>
|
||||
</ds-text>
|
||||
<ds-space margin-bottom="small" />
|
||||
<hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
|
||||
<ds-text align="right">
|
||||
|
||||
@ -24,7 +24,6 @@
|
||||
import { Editor, EditorContent } from 'tiptap'
|
||||
import { History } from 'tiptap-extensions'
|
||||
import linkify from 'linkify-it'
|
||||
import stringHash from 'string-hash'
|
||||
import { replace, build } from 'xregexp/xregexp-all.js'
|
||||
|
||||
import * as key from '../../constants/keycodes'
|
||||
@ -108,17 +107,6 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler: function(content, old) {
|
||||
const contentHash = stringHash(content)
|
||||
if (!content || contentHash === this.lastValueHash) {
|
||||
return
|
||||
}
|
||||
this.lastValueHash = contentHash
|
||||
this.$nextTick(() => this.editor.setContent(content))
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
immediate: true,
|
||||
handler: function(val) {
|
||||
@ -129,7 +117,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
content: this.value || '',
|
||||
doc: this.doc,
|
||||
@ -247,11 +235,7 @@ export default {
|
||||
},
|
||||
onUpdate(e) {
|
||||
const content = e.getHTML()
|
||||
const contentHash = stringHash(content)
|
||||
if (contentHash !== this.lastValueHash) {
|
||||
this.lastValueHash = contentHash
|
||||
this.$emit('input', content)
|
||||
}
|
||||
this.$emit('input', content)
|
||||
},
|
||||
toggleLinkInput(attrs, element) {
|
||||
if (!this.isLinkInputActive && attrs && element) {
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { allowEmbedIframesMutation } from '~/graphql/User.js'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
name: 'embed-component',
|
||||
@ -129,7 +129,7 @@ export default {
|
||||
async updateEmbedSettings(allowEmbedIframes) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: allowEmbedIframesMutation(),
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
allowEmbedIframes,
|
||||
|
||||
@ -33,12 +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'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -87,14 +87,7 @@ export default {
|
||||
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
|
||||
}
|
||||
}
|
||||
`,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
locale: this.$i18n.locale(),
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import vueDropzone from 'nuxt-dropzone'
|
||||
import gql from 'graphql-tag'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -62,14 +62,7 @@ export default {
|
||||
const avatarUpload = file[0]
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation($id: ID!, $avatarUpload: Upload) {
|
||||
UpdateUser(id: $id, avatarUpload: $avatarUpload) {
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
`,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
avatarUpload,
|
||||
id: this.user.id,
|
||||
|
||||
17
webapp/components/utils/UpdateQuery.js
Normal file
17
webapp/components/utils/UpdateQuery.js
Normal file
@ -0,0 +1,17 @@
|
||||
import unionBy from 'lodash/unionBy'
|
||||
|
||||
export default function UpdateQuery(component, { $state, pageKey }) {
|
||||
if (!pageKey) throw new Error('No key given for the graphql query { data } object')
|
||||
return (previousResult, { fetchMoreResult }) => {
|
||||
const oldData = (previousResult && previousResult[pageKey]) || []
|
||||
const newData = (fetchMoreResult && fetchMoreResult[pageKey]) || []
|
||||
if (newData.length < component.pageSize) {
|
||||
component.hasMore = false
|
||||
$state.complete()
|
||||
}
|
||||
const result = {}
|
||||
result[pageKey] = unionBy(oldData, newData, item => item.id)
|
||||
$state.loaded()
|
||||
return result
|
||||
}
|
||||
}
|
||||
86
webapp/components/utils/UpdateQuery.spec.js
Normal file
86
webapp/components/utils/UpdateQuery.spec.js
Normal file
@ -0,0 +1,86 @@
|
||||
import UpdateQuery from './UpdateQuery'
|
||||
|
||||
let $state
|
||||
let component
|
||||
let pageKey
|
||||
let updateQuery
|
||||
let previousResult
|
||||
let fetchMoreResult
|
||||
|
||||
beforeEach(() => {
|
||||
component = {
|
||||
hasMore: true,
|
||||
pageSize: 1,
|
||||
}
|
||||
|
||||
$state = {
|
||||
complete: jest.fn(),
|
||||
loaded: jest.fn(),
|
||||
}
|
||||
previousResult = { Post: [{ id: 1, foo: 'bar' }] }
|
||||
fetchMoreResult = { Post: [{ id: 2, foo: 'baz' }] }
|
||||
updateQuery = () => UpdateQuery(component, { $state, pageKey })
|
||||
})
|
||||
|
||||
describe('UpdateQuery', () => {
|
||||
it('throws error because no key is given', () => {
|
||||
expect(() => {
|
||||
updateQuery()({ Post: [] }, { fetchMoreResult: { Post: [] } })
|
||||
}).toThrow(/No key given/)
|
||||
})
|
||||
|
||||
describe('with a page key', () => {
|
||||
beforeEach(() => (pageKey = 'Post'))
|
||||
|
||||
describe('given two arrays of things', () => {
|
||||
it('merges the arrays', () => {
|
||||
expect(updateQuery()(previousResult, { fetchMoreResult })).toEqual({
|
||||
Post: [
|
||||
{ id: 1, foo: 'bar' },
|
||||
{ id: 2, foo: 'baz' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('does not create duplicates', () => {
|
||||
fetchMoreResult = { Post: [{ id: 1, foo: 'baz' }] }
|
||||
expect(updateQuery()(previousResult, { fetchMoreResult })).toEqual({
|
||||
Post: [{ id: 1, foo: 'bar' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('does not call $state.complete()', () => {
|
||||
expect(updateQuery()(previousResult, { fetchMoreResult }))
|
||||
expect($state.complete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('in case of fewer records than pageSize', () => {
|
||||
beforeEach(() => (component.pageSize = 10))
|
||||
it('calls $state.complete()', () => {
|
||||
expect(updateQuery()(previousResult, { fetchMoreResult }))
|
||||
expect($state.complete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('changes component.hasMore to `false`', () => {
|
||||
expect(component.hasMore).toBe(true)
|
||||
expect(updateQuery()(previousResult, { fetchMoreResult }))
|
||||
expect(component.hasMore).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('given one array is undefined', () => {
|
||||
describe('does not crash', () => {
|
||||
it('neither if the previous data was undefined', () => {
|
||||
expect(updateQuery()(undefined, { fetchMoreResult })).toEqual({
|
||||
Post: [{ id: 2, foo: 'baz' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('not if the new data is undefined', () => {
|
||||
expect(updateQuery()(previousResult, {})).toEqual({ Post: [{ id: 1, foo: 'bar' }] })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const linkableUserFragment = lang => gql`
|
||||
export const userFragment = gql`
|
||||
fragment user on User {
|
||||
id
|
||||
slug
|
||||
@ -10,19 +10,8 @@ export const linkableUserFragment = lang => gql`
|
||||
deleted
|
||||
}
|
||||
`
|
||||
export const userFragment = lang => gql`
|
||||
fragment user on User {
|
||||
id
|
||||
slug
|
||||
name
|
||||
avatar
|
||||
disabled
|
||||
deleted
|
||||
shoutedCount
|
||||
contributionsCount
|
||||
commentedCount
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
export const locationAndBadgesFragment = lang => gql`
|
||||
fragment locationAndBadges on User {
|
||||
location {
|
||||
name: name${lang}
|
||||
}
|
||||
@ -33,15 +22,17 @@ export const userFragment = lang => gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const postCountsFragment = gql`
|
||||
fragment postCounts on Post {
|
||||
commentsCount
|
||||
export const userCountsFragment = gql`
|
||||
fragment userCounts on User {
|
||||
shoutedCount
|
||||
shoutedByCurrentUser
|
||||
emotionsCount
|
||||
contributionsCount
|
||||
commentedCount
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
}
|
||||
`
|
||||
export const postFragment = lang => gql`
|
||||
|
||||
export const postFragment = gql`
|
||||
fragment post on Post {
|
||||
id
|
||||
title
|
||||
@ -58,6 +49,22 @@ export const postFragment = lang => gql`
|
||||
author {
|
||||
...user
|
||||
}
|
||||
pinnedAt
|
||||
imageAspectRatio
|
||||
}
|
||||
`
|
||||
|
||||
export const postCountsFragment = gql`
|
||||
fragment postCounts on Post {
|
||||
commentsCount
|
||||
shoutedCount
|
||||
shoutedByCurrentUser
|
||||
emotionsCount
|
||||
}
|
||||
`
|
||||
|
||||
export const tagsCategoriesAndPinnedFragment = gql`
|
||||
fragment tagsCategoriesAndPinned on Post {
|
||||
tags {
|
||||
id
|
||||
}
|
||||
@ -72,11 +79,10 @@ export const postFragment = lang => gql`
|
||||
name
|
||||
role
|
||||
}
|
||||
pinnedAt
|
||||
imageAspectRatio
|
||||
}
|
||||
`
|
||||
export const commentFragment = lang => gql`
|
||||
|
||||
export const commentFragment = gql`
|
||||
fragment comment on Comment {
|
||||
id
|
||||
createdAt
|
||||
@ -85,8 +91,5 @@ export const commentFragment = lang => gql`
|
||||
deleted
|
||||
content
|
||||
contentExcerpt
|
||||
author {
|
||||
...user
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,20 +1,42 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { userFragment, postFragment, commentFragment, postCountsFragment } from './Fragments'
|
||||
import {
|
||||
userFragment,
|
||||
postFragment,
|
||||
commentFragment,
|
||||
postCountsFragment,
|
||||
userCountsFragment,
|
||||
locationAndBadgesFragment,
|
||||
tagsCategoriesAndPinnedFragment,
|
||||
} from './Fragments'
|
||||
|
||||
export default i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${commentFragment(lang)}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
${commentFragment}
|
||||
|
||||
query Post($id: ID!) {
|
||||
Post(id: $id) {
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
comments(orderBy: createdAt_asc) {
|
||||
...comment
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,14 +46,23 @@ export default i18n => {
|
||||
export const filterPosts = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
|
||||
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -40,9 +71,12 @@ export const filterPosts = i18n => {
|
||||
export const profilePagePosts = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
|
||||
query profilePagePosts(
|
||||
$filter: _PostFilter
|
||||
@ -53,6 +87,12 @@ export const profilePagePosts = i18n => {
|
||||
profilePagePosts(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -69,17 +109,32 @@ export const PostsEmotionsByCurrentUser = () => {
|
||||
export const relatedContributions = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
${postFragment}
|
||||
${postCountsFragment}
|
||||
${tagsCategoriesAndPinnedFragment}
|
||||
|
||||
query Post($slug: String!) {
|
||||
Post(slug: $slug) {
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
relatedContributions(first: 2) {
|
||||
...post
|
||||
...postCounts
|
||||
...tagsCategoriesAndPinned
|
||||
author {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,38 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { linkableUserFragment, userFragment, postFragment, commentFragment } from './Fragments'
|
||||
import {
|
||||
userCountsFragment,
|
||||
locationAndBadgesFragment,
|
||||
userFragment,
|
||||
postFragment,
|
||||
commentFragment,
|
||||
} from './Fragments'
|
||||
|
||||
export default i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
${locationAndBadgesFragment(lang)}
|
||||
|
||||
query User($id: ID!) {
|
||||
User(id: $id) {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
about
|
||||
locationName
|
||||
createdAt
|
||||
badgesCount
|
||||
followingCount
|
||||
following(first: 7) {
|
||||
...user
|
||||
}
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
isBlocked
|
||||
following(first: 7) {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
followedBy(first: 7) {
|
||||
...user
|
||||
...userCounts
|
||||
...locationAndBadges
|
||||
}
|
||||
socialMedia {
|
||||
id
|
||||
@ -47,11 +58,10 @@ export const minimisedUserQuery = () => {
|
||||
}
|
||||
|
||||
export const notificationQuery = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${linkableUserFragment()}
|
||||
${commentFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
${userFragment}
|
||||
${commentFragment}
|
||||
${postFragment}
|
||||
|
||||
query($read: Boolean, $orderBy: NotificationOrdering, $first: Int, $offset: Int) {
|
||||
notifications(read: $read, orderBy: $orderBy, first: $first, offset: $offset) {
|
||||
@ -63,11 +73,20 @@ export const notificationQuery = i18n => {
|
||||
__typename
|
||||
... on Post {
|
||||
...post
|
||||
author {
|
||||
...user
|
||||
}
|
||||
}
|
||||
... on Comment {
|
||||
...comment
|
||||
author {
|
||||
...user
|
||||
}
|
||||
post {
|
||||
...post
|
||||
author {
|
||||
...user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,11 +96,10 @@ export const notificationQuery = i18n => {
|
||||
}
|
||||
|
||||
export const markAsReadMutation = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${linkableUserFragment()}
|
||||
${commentFragment(lang)}
|
||||
${postFragment(lang)}
|
||||
${userFragment}
|
||||
${commentFragment}
|
||||
${postFragment}
|
||||
|
||||
mutation($id: ID!) {
|
||||
markAsRead(id: $id) {
|
||||
@ -93,11 +111,17 @@ export const markAsReadMutation = i18n => {
|
||||
__typename
|
||||
... on Post {
|
||||
...post
|
||||
author {
|
||||
...user
|
||||
}
|
||||
}
|
||||
... on Comment {
|
||||
...comment
|
||||
post {
|
||||
...post
|
||||
author {
|
||||
...user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -107,16 +131,19 @@ export const markAsReadMutation = i18n => {
|
||||
}
|
||||
|
||||
export const followUserMutation = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
|
||||
mutation($id: ID!) {
|
||||
followUser(id: $id) {
|
||||
name
|
||||
...user
|
||||
...userCounts
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
followedBy(first: 7) {
|
||||
...user
|
||||
...userCounts
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -124,39 +151,61 @@ export const followUserMutation = i18n => {
|
||||
}
|
||||
|
||||
export const unfollowUserMutation = i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
${userFragment(lang)}
|
||||
${userFragment}
|
||||
${userCountsFragment}
|
||||
|
||||
mutation($id: ID!) {
|
||||
unfollowUser(id: $id) {
|
||||
name
|
||||
...user
|
||||
...userCounts
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
followedBy(first: 7) {
|
||||
...user
|
||||
...userCounts
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const allowEmbedIframesMutation = () => {
|
||||
export const updateUserMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $allowEmbedIframes: Boolean) {
|
||||
UpdateUser(id: $id, allowEmbedIframes: $allowEmbedIframes) {
|
||||
mutation(
|
||||
$id: ID!
|
||||
$slug: String
|
||||
$name: String
|
||||
$locationName: String
|
||||
$about: String
|
||||
$allowEmbedIframes: Boolean
|
||||
$showShoutsPublicly: Boolean
|
||||
$locale: String
|
||||
$termsAndConditionsAgreedVersion: String
|
||||
$avatarUpload: Upload
|
||||
) {
|
||||
UpdateUser(
|
||||
id: $id
|
||||
slug: $slug
|
||||
name: $name
|
||||
locationName: $locationName
|
||||
about: $about
|
||||
allowEmbedIframes: $allowEmbedIframes
|
||||
showShoutsPublicly: $showShoutsPublicly
|
||||
locale: $locale
|
||||
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
|
||||
avatarUpload: $avatarUpload
|
||||
) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
locationName
|
||||
about
|
||||
allowEmbedIframes
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const showShoutsPubliclyMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $showShoutsPublicly: Boolean) {
|
||||
UpdateUser(id: $id, showShoutsPublicly: $showShoutsPublicly) {
|
||||
id
|
||||
showShoutsPublicly
|
||||
locale
|
||||
termsAndConditionsAgreedVersion
|
||||
avatar
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -169,14 +218,3 @@ export const checkSlugAvailableQuery = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const localeMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $locale: String) {
|
||||
UpdateUser(id: $id, locale: $locale) {
|
||||
id
|
||||
locale
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
}
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"name": "Daten löschen",
|
||||
"name": "Benutzerkonto löschen",
|
||||
"contributionsCount": "Meine {count} Beiträge löschen",
|
||||
"commentedCount": "Meine {count} Kommentare löschen",
|
||||
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
|
||||
|
||||
@ -271,10 +271,10 @@
|
||||
"name": "Download Data"
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"name": "Delete data",
|
||||
"name": "Delete user account",
|
||||
"contributionsCount": "Delete my {count} posts",
|
||||
"commentedCount": "Delete my {count} comments",
|
||||
"accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||
"accountDescription": "Be aware that your Posts and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||
"accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!",
|
||||
"success": "Account successfully deleted!",
|
||||
"pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm"
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
}
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"name": "Borrar datos",
|
||||
"name": "Eliminar cuenta de usuario",
|
||||
"contributionsCount": "Eliminar mis {count} contribuciones",
|
||||
"commentedCount": "Eliminar mis {count} comentarios",
|
||||
"accountDescription": "Tenga en cuenta que su contribución y sus comentarios son importantes para nuestra comunidad. Si aún decide borrarlos, debe marcarlos a continuación.",
|
||||
@ -321,7 +321,63 @@
|
||||
"disabledBy": "desactivado por",
|
||||
"reasonCategory": "Categoría",
|
||||
"reasonDescription": "Descripción",
|
||||
"createdAt": "Fecha"
|
||||
"status": "Estado actual",
|
||||
"content": "Contenido",
|
||||
"author": "Autor",
|
||||
"decision": "Decisión",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Deshabilitado",
|
||||
"decided": "Decidido",
|
||||
"noDecision": "¡No hay decisión!",
|
||||
"decideButton": "Confirmar",
|
||||
"DecisionSuccess": "Decidido con éxito!",
|
||||
"enabledBy": "Habilitado por",
|
||||
"previousDecision": "Decisión previa:",
|
||||
"enabledAt": "Habilitado el",
|
||||
"disabledAt": "Deshabilitado el",
|
||||
"numberOfUsers": "{count} usuarios",
|
||||
"filterLabel": {
|
||||
"all": "Todos",
|
||||
"unreviewed": "Sin revisar",
|
||||
"reviewed": "Revisado",
|
||||
"closed": "Cerrado"
|
||||
},
|
||||
"reportedOn": "Fecha",
|
||||
"moreDetails": "Ver Detalles",
|
||||
"decideModal": {
|
||||
"submit": "Confirmar decisión",
|
||||
"cancel": "Cancelar",
|
||||
"User": {
|
||||
"disable": {
|
||||
"title": "Finalmente Deshabilitar Usuario",
|
||||
"message": "¿Realmente quiere que el usuario \"<b>{nombre}<\/b>\" permanezca <b>desactivado<\/b>?"
|
||||
},
|
||||
"enable": {
|
||||
"title": "Finalmente Habilitar Usuario",
|
||||
"message": "¿Realmente quiere que el usuario \"<b>{nombre}<\/b>\" permanezca <b>habilitado<\/b>?"
|
||||
}
|
||||
},
|
||||
"Post": {
|
||||
"disable": {
|
||||
"title": "Finalmente Desactivar Contribución",
|
||||
"message": "¿Realmente quiere que la entrada \"<b>{nombre}<\/b>\" permanezca <b>desactivada<\/b>?"
|
||||
},
|
||||
"enable": {
|
||||
"title": "Finalmente Habilitar Contribución",
|
||||
"message": "¿Realmente quiere que la contribución \"<b>{nombre}<\/b>\" permanezca <b>activada<\/b>?"
|
||||
}
|
||||
},
|
||||
"Comment": {
|
||||
"disable": {
|
||||
"title": "Desactivar finalmente Comentario",
|
||||
"message": "¿Realmente quiere que el comentario \"<b>{nombre}<\/b>\" permanezca <b>desactivado<\/b>?"
|
||||
},
|
||||
"enable": {
|
||||
"title": "Finalmente Habilitar Comentario",
|
||||
"message": "¿Realmente quiere que el comentario \"<b>{nombre}<\/b>\" permanezca <b>habilitado<\/b>?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
@ -501,7 +557,9 @@
|
||||
"invalid-invitation-token": "Parece que el código de invitación ya ha sido canjeado. Cada código sólo se puede utilizar una vez."
|
||||
},
|
||||
"submit": "Crear una cuenta",
|
||||
"success": "Se ha enviado un correo electrónico con un enlace de confirmación para el registro a <b>{email}<\/b>."
|
||||
"success": "Se ha enviado un correo electrónico con un enlace de confirmación para el registro a <b>{email}<\/b>.",
|
||||
"no-commercial": "No tengo intensiones comerciales y no represento una empresa u organización comercial.",
|
||||
"no-political": "No estoy en la red en nombre de un partido o una organización política."
|
||||
}
|
||||
},
|
||||
"create-user-account": {
|
||||
@ -742,6 +800,10 @@
|
||||
"addition": {
|
||||
"title": "Además, regularmente celebramos eventos donde también puede dar impresiones y hacer preguntas. Puede encontrar un resumen actualizado aquí:",
|
||||
"description": "<a href=\"https:\/\/human-connection.org\/events\/\" target=\"_blank\" > https:\/\/human-connection.org\/events\/ <\/a>"
|
||||
},
|
||||
"no-commercial-use": {
|
||||
"title": "Sin uso comercial",
|
||||
"description": "El uso de la red Human Connection no está permitido para fines comerciales. Esto incluye, pero no se limita a, publicitar productos con intención comercial, publicar enlaces de afiliados, solicitar donaciones directamente o brindar apoyo financiero para fines que no se reconocen como caritativos para fines fiscales."
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
@ -749,4 +811,4 @@
|
||||
"donate-now": "Donar ahora",
|
||||
"amount-of-total": "{amount} de {total} € recaudados"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
}
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"name": "Effacer les données",
|
||||
"name": "Supprimer un compte utilisateur",
|
||||
"contributionsCount": "Supprimer mes {count} postes",
|
||||
"commentedCount": "Supprimer mes {count} commentaires",
|
||||
"accountDescription": "Sachez que vos postes et commentaires sont importants pour notre communauté. Si vous voulez quand même les supprimer, vous devez les marquer ci-dessous.",
|
||||
@ -749,4 +749,4 @@
|
||||
"donate-now": "Faites un don",
|
||||
"amount-of-total": "{amount} de {total} € collectés"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,6 +114,8 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"name": "Cancellare l'account utente",
|
||||
"contributionsCount": "Cancellare i miei {count} messaggi",
|
||||
"commentedCount": "Cancella i miei {count} commenti",
|
||||
"accountDescription": "Essere consapevoli che i tuoi post e commenti sono importanti per la nostra comunità. Se cancelli il tuo account utente, tutto scomparirà per sempre - e sarebbe un vero peccato!",
|
||||
"accountWarning": "Attenzione!Tu <b>Non puoi gestire</b> e <b>Non puoi recuperare il tuo account, i tuoi messaggi o commenti dopo aver cancellato il tuo account!",
|
||||
"success": "Account eliminato con successo!",
|
||||
@ -236,10 +238,10 @@
|
||||
"description": ""
|
||||
},
|
||||
"donations": {
|
||||
"name": "",
|
||||
"goal": "",
|
||||
"progress": "",
|
||||
"successfulUpdate": ""
|
||||
"name": "Info donazioni",
|
||||
"goal": "Donazioni mensili necessarie",
|
||||
"progress": "Donazioni raccolte finora",
|
||||
"successfulUpdate": "Informazioni sulle donazioni aggiornate con successo!"
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
@ -743,8 +745,8 @@
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
"donations-for": "",
|
||||
"donate-now": "",
|
||||
"amount-of-total": ""
|
||||
"donations-for": "Donazioni per",
|
||||
"donate-now": "Dona ora",
|
||||
"amount-of-total": "{amount} of {total} € collezionato"
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,7 +321,63 @@
|
||||
"disabledBy": "отключены",
|
||||
"reasonCategory": "Категория",
|
||||
"reasonDescription": "Описание",
|
||||
"createdAt": "Дата"
|
||||
"status": "Текущее состояние",
|
||||
"content": "Содержа́ние",
|
||||
"author": "Автор",
|
||||
"decision": "Решение",
|
||||
"enabled": "Включен",
|
||||
"disabled": "Отключен",
|
||||
"decided": "Решил",
|
||||
"noDecision": "Нет решения!",
|
||||
"decideButton": "Подтвердить",
|
||||
"DecisionSuccess": "Решил успешно!",
|
||||
"enabledBy": "Включено с",
|
||||
"previousDecision": "Предыдущее решение:",
|
||||
"enabledAt": "Включено на",
|
||||
"disabledAt": "Отключено на",
|
||||
"numberOfUsers": "{count} пользователи",
|
||||
"filterLabel": {
|
||||
"all": "Все",
|
||||
"unreviewed": "Нерассмотренный",
|
||||
"reviewed": "Рассмотренный",
|
||||
"closed": "Закрыто"
|
||||
},
|
||||
"reportedOn": "Дата",
|
||||
"moreDetails": "Посмотреть подробности",
|
||||
"decideModal": {
|
||||
"submit": "Подтвердить решение",
|
||||
"cancel": "Отменить",
|
||||
"User": {
|
||||
"disable": {
|
||||
"title": "Окончательно отключить пользователя",
|
||||
"message": "Ты действительно хочешь, чтобы пользователь \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?"
|
||||
},
|
||||
"enable": {
|
||||
"title": "Окончательно включить пост",
|
||||
"message": "Ты уверены, что хочешь поделиться пользователем \"<b>{name}<\/b>\"?"
|
||||
}
|
||||
},
|
||||
"Post": {
|
||||
"disable": {
|
||||
"title": "Окончательно отключить пост",
|
||||
"message": "Ты действительно хочешь, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?"
|
||||
},
|
||||
"enable": {
|
||||
"title": "Окончательно включить пост",
|
||||
"message": "Ты действительно хочешь, чтобы пост \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?"
|
||||
}
|
||||
},
|
||||
"Comment": {
|
||||
"disable": {
|
||||
"title": "Окончательно отключить комментарий",
|
||||
"message": "Ты действительно хочешь, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>отключен<\/b>?"
|
||||
},
|
||||
"enable": {
|
||||
"title": "Окончательно включить комментарий",
|
||||
"message": "Ты действительно хочешь, чтобы комментарий \"<b>{name}<\/b>\" остановиться и <b>включен<\/b>?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
@ -501,7 +557,9 @@
|
||||
"invalid-invitation-token": "Похоже, что приглашение уже использовалось. Ссылки на приглашения можно использовать только один раз."
|
||||
},
|
||||
"submit": "Создать учетную запись",
|
||||
"success": "Письмо со ссылкой для завершения регистрации было отправлено на <b> {email} <\/b>"
|
||||
"success": "Письмо со ссылкой для завершения регистрации было отправлено на <b> {email} <\/b>",
|
||||
"no-commercial": "У меня нет коммерческих намерений, и я не представляю коммерческое предприятие или организацию.",
|
||||
"no-political": "Я не от имени какой-либо партии или политической организации в сети."
|
||||
}
|
||||
},
|
||||
"create-user-account": {
|
||||
@ -742,6 +800,10 @@
|
||||
"addition": {
|
||||
"title": "Кроме того, мы регулярно проводим мероприятия, где ты также можешь\nподелиться своими впечатлениями и задать вопросы. Здесь ты можешь найти текущий обзор:",
|
||||
"description": "<a href=\"https:\/\/human-connection.org\/events\/\" target=\"_blank\" > https:\/\/human-connection.org\/events\/ <\/a>"
|
||||
},
|
||||
"no-commercial-use": {
|
||||
"title": "Нет коммерческого использования",
|
||||
"description": "Использование Human Connection сети не допускается в коммерческих целях. Это включает, но не ограничивается рекламой продуктов с коммерческими целями, размещением партнерских ссылок, прямым привлечением пожертвований или предоставлением финансовой поддержки для целей, которые не признаются благотворительными для целей налогообложения."
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@human-connection/styleguide": "0.5.22",
|
||||
"@nuxtjs/apollo": "^4.0.0-rc18",
|
||||
"@nuxtjs/apollo": "^4.0.0-rc19",
|
||||
"@nuxtjs/axios": "~5.8.0",
|
||||
"@nuxtjs/dotenv": "~1.4.1",
|
||||
"@nuxtjs/pwa": "^3.0.0-beta.19",
|
||||
@ -80,7 +80,6 @@
|
||||
"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.5",
|
||||
@ -116,7 +115,7 @@
|
||||
"babel-plugin-require-context-hook": "^1.0.0",
|
||||
"babel-preset-vue": "~2.0.2",
|
||||
"core-js": "~2.6.10",
|
||||
"css-loader": "~3.3.0",
|
||||
"css-loader": "~3.3.2",
|
||||
"eslint": "~6.7.2",
|
||||
"eslint-config-prettier": "~6.7.0",
|
||||
"eslint-config-standard": "~14.1.0",
|
||||
|
||||
@ -68,6 +68,7 @@ import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -151,27 +152,7 @@ export default {
|
||||
first: this.pageSize,
|
||||
orderBy: ['pinned_asc', this.orderBy],
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult || fetchMoreResult.Post.length < this.pageSize) {
|
||||
this.hasMore = false
|
||||
$state.complete()
|
||||
}
|
||||
|
||||
const { Post = [] } = previousResult
|
||||
const result = {
|
||||
...previousResult,
|
||||
Post: [
|
||||
...Post.filter(prevPost => {
|
||||
return (
|
||||
fetchMoreResult.Post.filter(newPost => newPost.id === prevPost.id).length === 0
|
||||
)
|
||||
}),
|
||||
...fetchMoreResult.Post,
|
||||
],
|
||||
}
|
||||
$state.loaded()
|
||||
return result
|
||||
},
|
||||
updateQuery: UpdateQuery(this, { $state, pageKey: 'Post' }),
|
||||
})
|
||||
},
|
||||
deletePost(deletedPost) {
|
||||
|
||||
@ -283,6 +283,7 @@ import { profilePagePosts } from '~/graphql/PostQuery'
|
||||
import UserQuery from '~/graphql/User'
|
||||
import { Block, Unblock } from '~/graphql/settings/BlockedUsers'
|
||||
import PostMutations from '~/graphql/PostMutations'
|
||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
|
||||
const tabToFilterMapping = ({ tab, id }) => {
|
||||
return {
|
||||
@ -385,27 +386,7 @@ export default {
|
||||
first: this.pageSize,
|
||||
orderBy: 'createdAt_desc',
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult || fetchMoreResult.profilePagePosts.length < this.pageSize) {
|
||||
this.hasMore = false
|
||||
$state.complete()
|
||||
}
|
||||
const { profilePagePosts = [] } = previousResult
|
||||
const result = {
|
||||
...previousResult,
|
||||
profilePagePosts: [
|
||||
...profilePagePosts.filter(prevPost => {
|
||||
return (
|
||||
fetchMoreResult.profilePagePosts.filter(newPost => newPost.id === prevPost.id)
|
||||
.length === 0
|
||||
)
|
||||
}),
|
||||
...fetchMoreResult.profilePagePosts,
|
||||
],
|
||||
}
|
||||
$state.loaded()
|
||||
return result
|
||||
},
|
||||
updateQuery: UpdateQuery(this, { $state, pageKey: 'profilePagePosts' }),
|
||||
})
|
||||
},
|
||||
resetPostList() {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { allowEmbedIframesMutation } from '~/graphql/User.js'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
head() {
|
||||
@ -69,7 +69,7 @@ export default {
|
||||
async submit() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: allowEmbedIframesMutation(),
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
allowEmbedIframes: !this.disabled,
|
||||
|
||||
@ -41,40 +41,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { CancelToken } from 'axios'
|
||||
import UniqueSlugForm from '~/components/utils/UniqueSlugForm'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
|
||||
let timeout
|
||||
const mapboxToken = process.env.MAPBOX_TOKEN
|
||||
|
||||
/*
|
||||
const query = gql`
|
||||
query getUser($id: ID) {
|
||||
User(id: $id) {
|
||||
id
|
||||
name
|
||||
locationName
|
||||
about
|
||||
}
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $slug: String, $name: String, $locationName: String, $about: String) {
|
||||
UpdateUser(id: $id, slug: $slug, name: $name, locationName: $locationName, about: $about) {
|
||||
id
|
||||
slug
|
||||
name
|
||||
locationName
|
||||
about
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@ -120,7 +94,7 @@ export default {
|
||||
locationName = locationName && (locationName.label || locationName)
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
name,
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { showShoutsPubliclyMutation } from '~/graphql/User'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@ -36,7 +36,7 @@ export default {
|
||||
async submit() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: showShoutsPubliclyMutation(),
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
showShoutsPublicly: this.shoutsAllowed,
|
||||
|
||||
@ -24,17 +24,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { VERSION } from '~/constants/terms-and-conditions-version.js'
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $termsAndConditionsAgreedVersion: String) {
|
||||
UpdateUser(id: $id, termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion) {
|
||||
id
|
||||
termsAndConditionsAgreedVersion
|
||||
}
|
||||
}
|
||||
`
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
layout: 'default',
|
||||
head() {
|
||||
@ -74,7 +67,7 @@ export default {
|
||||
async submit() {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation,
|
||||
mutation: updateUserMutation(),
|
||||
variables: {
|
||||
id: this.currentUser.id,
|
||||
termsAndConditionsAgreedVersion: VERSION,
|
||||
|
||||
@ -1574,14 +1574,14 @@
|
||||
webpack-node-externals "^1.7.2"
|
||||
webpackbar "^4.0.0"
|
||||
|
||||
"@nuxtjs/apollo@^4.0.0-rc18":
|
||||
version "4.0.0-rc18"
|
||||
resolved "https://registry.yarnpkg.com/@nuxtjs/apollo/-/apollo-4.0.0-rc18.tgz#0069cae64f414ed879d20de00881986dca6bb26c"
|
||||
integrity sha512-DTwRw9XLJKyphZiVwtKn4hE6Vfn6BlxEDWFBMTXpKE3XUKpg5+Qcgr8GstkiKtWbOuNQi660KdZReJ48R8bxgQ==
|
||||
"@nuxtjs/apollo@^4.0.0-rc19":
|
||||
version "4.0.0-rc19"
|
||||
resolved "https://registry.yarnpkg.com/@nuxtjs/apollo/-/apollo-4.0.0-rc19.tgz#145b50c8e0185dac83c37f48ab685861f9005850"
|
||||
integrity sha512-OCUxdhz09vTA7jq4KrhdYw23PRXS4yHWST99Ohc1oSUiZUyNrmQc+VUNAz9bhSVjfHABrP1NP2FzKnBE1iEZhA==
|
||||
dependencies:
|
||||
cross-fetch "^3.0.4"
|
||||
universal-cookie "^4.0.2"
|
||||
vue-apollo "^3.0.1"
|
||||
vue-apollo "^3.0.2"
|
||||
vue-cli-plugin-apollo "^0.21.3"
|
||||
webpack-node-externals "^1.7.2"
|
||||
|
||||
@ -6176,10 +6176,10 @@ css-has-pseudo@^0.10.0:
|
||||
postcss "^7.0.6"
|
||||
postcss-selector-parser "^5.0.0-rc.4"
|
||||
|
||||
css-loader@^3.0.0, css-loader@^3.2.0, css-loader@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.3.0.tgz#65f889807baec3197313965d6cda9899f936734d"
|
||||
integrity sha512-x9Y1vvHe5RR+4tzwFdWExPueK00uqFTCw7mZy+9aE/X1SKWOArm5luaOrtJ4d05IpOwJ6S86b/tVcIdhw1Bu4A==
|
||||
css-loader@^3.0.0, css-loader@^3.2.0, css-loader@~3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.3.2.tgz#41b2086528aa4fbf8c0692e874bc14f081129b21"
|
||||
integrity sha512-4XSiURS+YEK2fQhmSaM1onnUm0VKWNf6WWBYjkp9YbSDGCBTVZ5XOM6Gkxo8tLgQlzkZOBJvk9trHlDk4gjEYg==
|
||||
dependencies:
|
||||
camelcase "^5.3.1"
|
||||
cssesc "^3.0.0"
|
||||
@ -14302,11 +14302,6 @@ serve-static@1.14.1, serve-static@^1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
||||
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
||||
dependencies:
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "0.17.1"
|
||||
|
||||
server-destroy@^1.0.1:
|
||||
version "1.0.1"
|
||||
@ -15052,11 +15047,6 @@ strict-uri-encode@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
|
||||
|
||||
string-hash@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
|
||||
integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
@ -16283,10 +16273,10 @@ vscode-uri@^1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59"
|
||||
integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==
|
||||
|
||||
vue-apollo@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.1.tgz#b7c24b6d6032bf707be7872e6615d59aa6621241"
|
||||
integrity sha512-NM+kWbPGV5bnRMK7BmMJMxoT1NqPjVAYf+MsjPDyfQNgyVEHSIObRVqLQDIs56PYQSC6YOGa0luo6Ykjj6rrPw==
|
||||
vue-apollo@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.2.tgz#b198ecfa3765850a0b9f2b84ffaa7fbd8ec15f52"
|
||||
integrity sha512-lrKyTT1L5mjDEp7nyqnTRJwD/kTpLDBIqFfZ+TGQVivjlUz6o5VA0pLYGCx5cGa1gEF/ERWc0AEdNSdKgs7Ygg==
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
serialize-javascript "^2.1.0"
|
||||
|
||||
@ -2240,10 +2240,10 @@ cypress-plugin-retries@^1.5.0:
|
||||
dependencies:
|
||||
chalk "^3.0.0"
|
||||
|
||||
cypress@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.7.0.tgz#e2cd71b87b6ce0d4c72c6ea25da1005d75c1f231"
|
||||
integrity sha512-o+vfRxqAba8TduelzfZQ4WHmj2yNEjaoO2EuZ8dZ9pJpuW+WGtBGheKIp6zkoQsp8ZgFe8OoHh1i2mY8BDnMAw==
|
||||
cypress@^3.8.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.8.0.tgz#7d4cd08f81f9048ee36760cc9ee3b9014f9e84ab"
|
||||
integrity sha512-gtEbqCgKETRc3pQFMsELRgIBNgiQg7vbOWTrCi7WE7bgOwNCaW9PEX8Jb3UN8z/maIp9WwzoFfeySfelYY7nRA==
|
||||
dependencies:
|
||||
"@cypress/listr-verbose-renderer" "0.4.1"
|
||||
"@cypress/xvfb" "1.2.4"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user