Merge branch '779-tags-of-contribution-in-text' of https://github.com/Human-Connection/Human-Connection into 779-tags-of-contribution-in-text

This commit is contained in:
Wolfgang Huß 2019-06-25 08:53:47 +02:00
commit 58810c6add
56 changed files with 2001 additions and 395 deletions

View File

@ -5,6 +5,11 @@ GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
MOCKS=false
SMTP_HOST=
SMTP_PORT=
SMTP_IGNORE_TLS=true
SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"

View File

@ -44,6 +44,9 @@ or start the backend in production environment with:
yarn run start
```
For e-mail delivery, please configure at least `SMTP_HOST` and `SMTP_PORT` in
your `.env` configuration file.
Your backend is up and running at [http://localhost:4000/](http://localhost:4000/)
This will start the GraphQL service \(by default on localhost:4000\) where you
can issue GraphQL requests or access GraphQL Playground in the browser.

View File

@ -44,15 +44,15 @@
"dependencies": {
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.2",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14",
"apollo-server": "~2.6.3",
"apollo-client": "~2.6.3",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.6.4",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.33",
"date-fns": "2.0.0-alpha.35",
"debug": "~4.1.1",
"dotenv": "~8.0.0",
"express": "~4.17.1",
@ -61,9 +61,9 @@
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.2",
"graphql-shield": "~5.3.8",
"graphql-shield": "~5.7.1",
"graphql-tag": "~2.10.1",
"graphql-yoga": "~1.17.4",
"graphql-yoga": "~1.18.0",
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
@ -72,6 +72,7 @@
"neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "git+https://github.com/Human-Connection/neo4j-graphql-js.git#temporary_fixes",
"node-fetch": "~2.6.0",
"nodemailer": "^6.2.1",
"npm-run-all": "~4.1.5",
"request": "~2.88.0",
"sanitize-html": "~1.20.1",
@ -87,17 +88,17 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.6.3",
"apollo-server-testing": "~2.6.4",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-eslint": "~10.0.2",
"babel-jest": "~24.8.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~5.16.0",
"eslint-config-prettier": "~4.3.0",
"eslint-config-prettier": "~5.0.0",
"eslint-config-standard": "~12.0.0",
"eslint-plugin-import": "~2.17.3",
"eslint-plugin-jest": "~22.6.4",
"eslint-plugin-jest": "~22.7.0",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",

View File

@ -2,23 +2,33 @@ import dotenv from 'dotenv'
dotenv.config()
export const requiredConfigs = {
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
JWT_SECRET: process.env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
}
const {
MAPBOX_TOKEN,
JWT_SECRET,
PRIVATE_KEY_PASSPHRASE,
SMTP_IGNORE_TLS = true,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
NEO4J_URI = 'bolt://localhost:7687',
NEO4J_USERNAME = 'neo4j',
NEO4J_PASSWORD = 'neo4j',
GRAPHQL_PORT = 4000,
CLIENT_URI = 'http://localhost:3000',
GRAPHQL_URI = 'http://localhost:4000',
} = process.env
export const neo4jConfigs = {
NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
}
export const serverConfigs = {
GRAPHQL_PORT: process.env.GRAPHQL_PORT || 4000,
CLIENT_URI: process.env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
export const requiredConfigs = { MAPBOX_TOKEN, JWT_SECRET, PRIVATE_KEY_PASSPHRASE }
export const smtpConfigs = {
SMTP_HOST,
SMTP_PORT,
SMTP_IGNORE_TLS,
SMTP_USERNAME,
SMTP_PASSWORD,
}
export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
@ -29,6 +39,7 @@ export const developmentConfigs = {
export default {
...requiredConfigs,
...smtpConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,

View File

@ -1,51 +0,0 @@
const legacyUrls = [
'https://api-alpha.human-connection.org',
'https://staging-api.human-connection.org',
'http://localhost:3000',
]
export const fixUrl = url => {
legacyUrls.forEach(legacyUrl => {
url = url.replace(legacyUrl, '')
})
if (!url.startsWith('/')) {
url = `/${url}`
}
return url
}
const checkUrl = thing => {
return (
thing &&
typeof thing === 'string' &&
legacyUrls.find(legacyUrl => {
return thing.indexOf(legacyUrl) === 0
})
)
}
export const fixImageURLs = (result, recursive) => {
if (checkUrl(result)) {
result = fixUrl(result)
} else if (result && Array.isArray(result)) {
result.forEach((res, index) => {
result[index] = fixImageURLs(result[index], true)
})
} else if (result && typeof result === 'object') {
Object.keys(result).forEach(key => {
result[key] = fixImageURLs(result[key], true)
})
}
return result
}
export default {
Mutation: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
return fixImageURLs(result)
},
Query: async (resolve, root, args, context, info) => {
let result = await resolve(root, args, context, info)
return fixImageURLs(result)
},
}

View File

@ -1,43 +0,0 @@
import { fixImageURLs } from './fixImageUrlsMiddleware'
describe('fixImageURLs', () => {
describe('edge case: image url is exact match of legacy url', () => {
it('replaces it with `/`', () => {
const url = 'https://api-alpha.human-connection.org'
expect(fixImageURLs(url)).toEqual('/')
})
})
describe('image url of legacy alpha', () => {
it('removes domain', () => {
const url =
'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png'
expect(fixImageURLs(url)).toEqual(
'/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png',
)
})
})
describe('image url of legacy staging', () => {
it('removes domain', () => {
const url =
'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg'
expect(fixImageURLs(url)).toEqual(
'/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg',
)
})
})
describe('object', () => {
it('returns untouched', () => {
const object = { some: 'thing' }
expect(fixImageURLs(object)).toEqual(object)
})
})
describe('some string', () => {
it('returns untouched', () => {})
const string = "Yeah I'm a String"
expect(fixImageURLs(string)).toEqual(string)
})
})

View File

@ -3,7 +3,6 @@ import activityPub from './activityPubMiddleware'
import password from './passwordMiddleware'
import softDelete from './softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import fixImageUrls from './fixImageUrlsMiddleware'
import excerpt from './excerptMiddleware'
import dateTime from './dateTimeMiddleware'
import xss from './xssMiddleware'
@ -25,7 +24,6 @@ export default schema => {
excerpt: excerpt,
notifications: notifications,
xss: xss,
fixImageUrls: fixImageUrls,
softDelete: softDelete,
user: user,
includedFields: includedFields,
@ -42,7 +40,6 @@ export default schema => {
'excerpt',
'notifications',
'xss',
'fixImageUrls',
'softDelete',
'user',
'includedFields',

View File

@ -105,8 +105,8 @@ const permissions = shield(
Query: {
'*': deny,
findPosts: allow,
Category: isAdmin,
Tag: isAdmin,
Category: allow,
Tag: allow,
Report: isModerator,
Notification: isAdmin,
statistics: allow,
@ -147,6 +147,8 @@ const permissions = shield(
CreateComment: isAuthenticated,
DeleteComment: isAuthor,
DeleteUser: isDeletingOwnAccount,
requestPasswordReset: allow,
resetPassword: allow,
},
User: {
email: isMyOwn,

View File

@ -0,0 +1,72 @@
import uuid from 'uuid/v4'
import bcrypt from 'bcryptjs'
import CONFIG from '../../config'
import nodemailer from 'nodemailer'
import { resetPasswordMail, wrongAccountMail } from './passwordReset/emailTemplates'
const transporter = () => {
const configs = {
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: false, // true for 465, false for other ports
}
const { SMTP_USERNAME: user, SMTP_PASSWORD: pass } = CONFIG
if (user && pass) {
configs.auth = { user, pass }
}
return nodemailer.createTransport(configs)
}
export async function createPasswordReset(options) {
const { driver, code, email, issuedAt = new Date() } = options
const session = driver.session()
const cypher = `
MATCH (u:User) WHERE u.email = $email
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
MERGE (u)-[:REQUESTED]->(pr)
RETURN u
`
const transactionRes = await session.run(cypher, {
issuedAt: issuedAt.toISOString(),
code,
email,
})
const users = transactionRes.records.map(record => record.get('u'))
session.close()
return users
}
export default {
Mutation: {
requestPasswordReset: async (_, { email }, { driver }) => {
const code = uuid().substring(0, 6)
const [user] = await createPasswordReset({ driver, code, email })
if (CONFIG.SMTP_HOST && CONFIG.SMTP_PORT) {
const name = (user && user.name) || ''
const mailTemplate = user ? resetPasswordMail : wrongAccountMail
await transporter().sendMail(mailTemplate({ email, code, name }))
}
return true
},
resetPassword: async (_, { email, code, newPassword }, { driver }) => {
const session = driver.session()
const stillValid = new Date()
stillValid.setDate(stillValid.getDate() - 1)
const newHashedPassword = await bcrypt.hashSync(newPassword, 10)
const cypher = `
MATCH (pr:PasswordReset {code: $code})
MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
SET pr.usedAt = datetime()
SET u.password = $newHashedPassword
RETURN pr
`
let transactionRes = await session.run(cypher, { stillValid, email, code, newHashedPassword })
const [reset] = transactionRes.records.map(record => record.get('pr'))
const result = !!(reset && reset.properties.usedAt)
session.close()
return result
},
},
}

View File

@ -0,0 +1,180 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host } from '../../jest/helpers'
import { getDriver } from '../../bootstrap/neo4j'
import { createPasswordReset } from './passwordReset'
const factory = Factory()
let client
const driver = getDriver()
const getAllPasswordResets = async () => {
const session = driver.session()
let transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r')
const resets = transactionRes.records.map(record => record.get('r'))
session.close()
return resets
}
describe('passwordReset', () => {
beforeEach(async () => {
client = new GraphQLClient(host)
await factory.create('User', {
email: 'user@example.org',
role: 'user',
password: '1234',
})
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('requestPasswordReset', () => {
const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }`
describe('with invalid email', () => {
const variables = { email: 'non-existent@example.org' }
it('resolves anyways', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
requestPasswordReset: true,
})
})
it('creates no node', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
expect(resets).toHaveLength(0)
})
})
describe('with a valid email', () => {
const variables = { email: 'user@example.org' }
it('resolves', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
requestPasswordReset: true,
})
})
it('creates node with label `PasswordReset`', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
expect(resets).toHaveLength(1)
})
it('creates a reset code', async () => {
await client.request(mutation, variables)
const resets = await getAllPasswordResets()
const [reset] = resets
const { code } = reset.properties
expect(code).toHaveLength(6)
})
})
})
describe('resetPassword', () => {
const setup = async (options = {}) => {
const { email = 'user@example.org', issuedAt = new Date(), code = 'abcdef' } = options
const session = driver.session()
await createPasswordReset({ driver, email, issuedAt, code })
session.close()
}
const mutation = `mutation($code: String!, $email: String!, $newPassword: String!) { resetPassword(code: $code, email: $email, newPassword: $newPassword) }`
let email = 'user@example.org'
let code = 'abcdef'
let newPassword = 'supersecret'
let variables
describe('invalid email', () => {
it('resolves to false', async () => {
await setup()
variables = { newPassword, email: 'non-existent@example.org', code }
await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false })
})
})
describe('valid email', () => {
describe('but invalid code', () => {
it('resolves to false', async () => {
await setup()
variables = { newPassword, email, code: 'slkdjf' }
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: false,
})
})
})
describe('and valid code', () => {
beforeEach(() => {
variables = {
newPassword,
email: 'user@example.org',
code: 'abcdef',
}
})
describe('and code not expired', () => {
beforeEach(async () => {
await setup()
})
it('resolves to true', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: true,
})
})
it('updates PasswordReset `usedAt` property', async () => {
await client.request(mutation, variables)
const requests = await getAllPasswordResets()
const [request] = requests
const { usedAt } = request.properties
expect(usedAt).not.toBeFalsy()
})
it('updates password of the user', async () => {
await client.request(mutation, variables)
const checkLoginMutation = `
mutation($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
const expected = expect.objectContaining({ login: expect.any(String) })
await expect(
client.request(checkLoginMutation, {
email: 'user@example.org',
password: 'supersecret',
}),
).resolves.toEqual(expected)
})
})
describe('but expired code', () => {
beforeEach(async () => {
const issuedAt = new Date()
issuedAt.setDate(issuedAt.getDate() - 1)
await setup({ issuedAt })
})
it('resolves to false', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual({
resetPassword: false,
})
})
it('does not update PasswordReset `usedAt` property', async () => {
await client.request(mutation, variables)
const requests = await getAllPasswordResets()
const [request] = requests
const { usedAt } = request.properties
expect(usedAt).toBeUndefined()
})
})
})
})
})
})

View File

@ -0,0 +1,85 @@
import CONFIG from '../../../config'
export const from = '"Human Connection" <info@human-connection.org>'
export const resetPasswordMail = options => {
const {
name,
email,
code,
subject = 'Use this link to reset your password. The link is only valid for 24 hours.',
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('code', code)
actionUrl.searchParams.set('email', email)
return {
to: email,
subject,
text: `
Hi ${name}!
You recently requested to reset your password for your Human Connection account.
Use the link below to reset it. This password reset is only valid for the next
24 hours.
${actionUrl}
If you did not request a password reset, please ignore this email or contact
support if you have questions:
${supportUrl}
Thanks,
The Human Connection Team
If you're having trouble with the link above, you can manually copy and
paste the following code into your browser window:
${code}
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}
export const wrongAccountMail = options => {
const {
email,
subject = `We received a request to reset your password with this email address (${email})`,
supportUrl = 'https://human-connection.org/en/contact/',
} = options
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
to: email,
subject,
text: `
We received a request to reset the password to access Human Connection with your
email address, but we were unable to find an account associated with this
address.
If you use Human Connection and were expecting this email, consider trying to
request a password reset using the email address associated with your account.
Try a different email:
${actionUrl}
If you do not use Human Connection or did not request a password reset, please
ignore this email. Feel free to contact support if you have further questions:
${supportUrl}
Thanks,
The Human Connection Team
Human Connection gemeinnützige GmbH
Bahnhofstr. 11
73235 Weilheim / Teck
Deutschland
`,
}
}

View File

@ -59,7 +59,7 @@ export default {
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
const session = driver.session()
let result = await session.run(
`MATCH (user:User {email: $userEmail})
`MATCH (user:User {email: $userEmail})
RETURN user {.id, .email, .password}`,
{
userEmail: user.email,

View File

@ -25,6 +25,8 @@ type Mutation {
login(email: String!, password: String!): String!
signup(email: String!, password: String!): Boolean!
changePassword(oldPassword: String!, newPassword: String!): String!
requestPasswordReset(email: String!): Boolean!
resetPassword(email: String!, code: String!, newPassword: String!): Boolean!
report(id: ID!, description: String): Report
disable(id: ID!): ID
enable(id: ID!): ID

View File

@ -1020,16 +1020,7 @@
"@types/node" "*"
"@types/range-parser" "*"
"@types/express@*", "@types/express@^4.11.1":
version "4.16.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19"
integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/express@4.17.0":
"@types/express@*", "@types/express@4.17.0", "@types/express@^4.11.1":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287"
integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==
@ -1119,10 +1110,10 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0"
integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==
"@types/yup@0.26.16":
version "0.26.16"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.16.tgz#75c428236207c48d9f8062dd1495cda8c5485a15"
integrity sha512-E2RNc7DSeQ+2EIJ1H3+yFjYu6YiyQBUJ7yNpIxomrYJ3oFizLZ5yDS3T1JTUNBC2OCRkgnhLS0smob5UuCHfNA==
"@types/yup@0.26.17":
version "0.26.17"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc"
integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q==
"@types/zen-observable@^0.5.3":
version "0.5.4"
@ -1322,10 +1313,10 @@ apollo-cache@1.3.2, apollo-cache@^1.3.2:
apollo-utilities "^1.3.2"
tslib "^1.9.3"
apollo-client@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.2.tgz#03b6af651e09b6e413e486ddc87464c85bd6e514"
integrity sha512-oks1MaT5x7gHcPeC8vPC1UzzsKaEIC0tye+jg72eMDt5OKc7BobStTeS/o2Ib3e0ii40nKxGBnMdl/Xa/p56Yg==
apollo-client@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
dependencies:
"@types/zen-observable" "^0.8.0"
apollo-cache "1.3.2"
@ -1351,17 +1342,17 @@ apollo-engine-reporting-protobuf@0.3.1:
dependencies:
protobufjs "^6.8.6"
apollo-engine-reporting@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.1.tgz#f2c2c63f865871a57c15cdbb2a3bcd4b4af28115"
integrity sha512-e0Xp+0yite8DH/xm9fnJt42CxfWAcY6waiq3icCMAgO9T7saXzVOPpl84SkuA+hIJUBtfaKrTnC+7Jxi/I7OrQ==
apollo-engine-reporting@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.3.2.tgz#b2569f79eb1a7a7380f49340db61465f449284fe"
integrity sha512-Q9XUZ3CTqddjCswlbn+OD2oYxZ5p4lCAnsWOGMfYnSmCXLagyNK28UFFQodjFOy73p6nlTAg9cwaJ9yMOBeeXA==
dependencies:
apollo-engine-reporting-protobuf "0.3.1"
apollo-graphql "^0.3.0"
apollo-server-core "2.6.3"
apollo-graphql "^0.3.2"
apollo-server-core "2.6.4"
apollo-server-env "2.4.0"
async-retry "^1.2.1"
graphql-extensions "0.7.2"
graphql-extensions "0.7.3"
apollo-env@0.5.1:
version "0.5.1"
@ -1380,49 +1371,49 @@ apollo-errors@^1.9.0:
assert "^1.4.1"
extendable-error "^0.1.5"
apollo-graphql@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7"
integrity sha512-tbhtzNAAhNI34v4XY9OlZGnH7U0sX4BP1cJrUfSiNzQnZRg1UbQYZ06riHSOHpi5RSndFcA9LDM5C1ZKKOUeBg==
apollo-graphql@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.2.tgz#8881a87f1d5fcf80837b34dba90737e664eabe9a"
integrity sha512-YbzYGR14GV0023m//EU66vOzZ3i7c04V/SF8Qk+60vf1sOWyKgO6mxZJ4BKhw10qWUayirhSDxq3frYE+qSG0A==
dependencies:
apollo-env "0.5.1"
lodash.sortby "^4.7.0"
apollo-link-context@~1.0.14:
version "1.0.17"
resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.17.tgz#439272cfb43ec1891506dd175ed907845b7de36c"
integrity sha512-W5UUfHcrrlP5uqJs5X1zbf84AMXhPZGAqX/7AQDgR6wY/7//sMGfJvm36KDkpIeSOElztGtM9z6zdPN1NbT41Q==
apollo-link-context@~1.0.18:
version "1.0.18"
resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.18.tgz#9e700e3314da8ded50057fee0a18af2bfcedbfc3"
integrity sha512-aG5cbUp1zqOHHQjAJXG7n/izeMQ6LApd/whEF5z6qZp5ATvcyfSNkCfy3KRJMMZZ3iNfVTs6jF+IUA8Zvf+zeg==
dependencies:
apollo-link "^1.2.11"
apollo-link "^1.2.12"
tslib "^1.9.3"
apollo-link-http-common@^0.2.13:
version "0.2.13"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350"
integrity sha512-Uyg1ECQpTTA691Fwx5e6Rc/6CPSu4TB4pQRTGIpwZ4l5JDOQ+812Wvi/e3IInmzOZpwx5YrrOfXrtN8BrsDXoA==
apollo-link-http-common@^0.2.14:
version "0.2.14"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg==
dependencies:
apollo-link "^1.2.11"
ts-invariant "^0.3.2"
apollo-link "^1.2.12"
ts-invariant "^0.4.0"
tslib "^1.9.3"
apollo-link-http@~1.5.14:
version "1.5.14"
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.14.tgz#ed6292248d1819ccd16523e346d35203a1b31109"
integrity sha512-XEoPXmGpxFG3wioovgAlPXIarWaW4oWzt8YzjTYZ87R4R7d1A3wKR/KcvkdMV1m5G7YSAHcNkDLe/8hF2nH6cg==
apollo-link-http@~1.5.15:
version "1.5.15"
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf"
integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ==
dependencies:
apollo-link "^1.2.11"
apollo-link-http-common "^0.2.13"
apollo-link "^1.2.12"
apollo-link-http-common "^0.2.14"
tslib "^1.9.3"
apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3:
version "1.2.11"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d"
integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==
apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.3:
version "1.2.12"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==
dependencies:
apollo-utilities "^1.2.1"
ts-invariant "^0.3.2"
apollo-utilities "^1.3.0"
ts-invariant "^0.4.0"
tslib "^1.9.3"
zen-observable-ts "^0.8.18"
zen-observable-ts "^0.8.19"
apollo-server-caching@0.4.0:
version "0.4.0"
@ -1431,24 +1422,24 @@ apollo-server-caching@0.4.0:
dependencies:
lru-cache "^5.0.0"
apollo-server-core@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.3.tgz#786c8251c82cf29acb5cae9635a321f0644332ae"
integrity sha512-tfC0QO1NbJW3ShkB5pRCnUaYEkW2AwnswaTeedkfv//EO3yiC/9LeouCK5F22T8stQG+vGjvCqf0C8ldI/XsIA==
apollo-server-core@2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.4.tgz#0372e3a28f221b9db83bdfbb0fd0b2960cd29bab"
integrity sha512-GBF+tQoJ/ysaY2CYMkuuAwJM1nk1yLJumrsBTFfcvalSzS64VdS5VN/zox1eRI+LHQQzHM18HYEAgDGa/EX+gw==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
"@apollographql/graphql-playground-html" "1.6.20"
"@types/ws" "^6.0.0"
apollo-cache-control "0.7.2"
apollo-datasource "0.5.0"
apollo-engine-reporting "1.3.1"
apollo-engine-reporting "1.3.2"
apollo-server-caching "0.4.0"
apollo-server-env "2.4.0"
apollo-server-errors "2.3.0"
apollo-server-plugin-base "0.5.2"
apollo-server-plugin-base "0.5.3"
apollo-tracing "0.7.2"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.7.2"
graphql-extensions "0.7.3"
graphql-subscriptions "^1.0.0"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
@ -1479,10 +1470,10 @@ apollo-server-errors@2.3.0:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
apollo-server-express@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.3.tgz#62034c978f84207615c0430fb37ab006f71146fe"
integrity sha512-8ca+VpKArgNzFar0D3DesWnn0g9YDtFLhO56TQprHh2Spxu9WxTnYNjsYs2MCCNf+iV/uy7vTvEknErvnIcZaQ==
apollo-server-express@2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.4.tgz#fc1d661be73fc1880aa53a56e1abe3733d08eada"
integrity sha512-U6hiZxty/rait39V5d+QeueNHlwfl68WbYtsutDUVxnq2Jws2ZDrvIkaWWN6HQ77+nBy5gGVxycvWIyoHHfi+g==
dependencies:
"@apollographql/graphql-playground-html" "1.6.20"
"@types/accepts" "^1.3.5"
@ -1490,7 +1481,7 @@ apollo-server-express@2.6.3:
"@types/cors" "^2.8.4"
"@types/express" "4.17.0"
accepts "^1.3.5"
apollo-server-core "2.6.3"
apollo-server-core "2.6.4"
body-parser "^1.18.3"
cors "^2.8.4"
graphql-subscriptions "^1.0.0"
@ -1518,25 +1509,25 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
apollo-server-plugin-base@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.2.tgz#f97ba983f1e825fec49cba8ff6a23d00e1901819"
integrity sha512-j81CpadRLhxikBYHMh91X4aTxfzFnmmebEiIR9rruS6dywWCxV2aLW87l9ocD1MiueNam0ysdwZkX4F3D4csNw==
apollo-server-plugin-base@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.3.tgz#234c6330c412a2e83ff49305a0c2f991fb40a266"
integrity sha512-Ax043vQTzPgFeJk6m6hmPm9NMfogO3LlTKJfrWHuyZhPNeTLweHNK30vpdzzgPalcryyDMDfLYFzxuDm0W+rRQ==
apollo-server-testing@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.3.tgz#a0199a5d42000e60ecf0dea44b851f5f581e280e"
integrity sha512-LTkegcGVSkM+pA0FINDSYVl3TiFYKZyfjlKrEr/LN6wLiL6gbRgy6LMtk2j+qli/bnTDqqQREX8OEqmV8FKUoQ==
apollo-server-testing@~2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.4.tgz#615dfaa6ea840b6ea3ce4fd2297b28402f2d5208"
integrity sha512-s9AeZnnndhz4WRBmgFM388BFKqD2H90L6ax0e6uNEmtZk3/iODqd16RbTNHbx+PkxFvZ8BQbX1/4rbvQn6r9CA==
dependencies:
apollo-server-core "2.6.3"
apollo-server-core "2.6.4"
apollo-server@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.3.tgz#71235325449c6d3881a5143975ca44c07a07d2d7"
integrity sha512-pTIXE5xEMAikKLTIBIqLNvimMETiZbzmiqDb6BGzIUicAz4Rxa1/+bDi1ZeJWrZQjE/TfBLd2Si3qam7dZGrjw==
apollo-server@~2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.4.tgz#34b3a50135e20b8df8c194a14e4636eb9c2898b2"
integrity sha512-f0TZOc969XNNlSm8sVsU34D8caQfPNwS0oqmWUxb8xXl88HlFzB+HBmOU6ZEKdpMCksTNDbqYo0jXiGJ0rL/0g==
dependencies:
apollo-server-core "2.6.3"
apollo-server-express "2.6.3"
apollo-server-core "2.6.4"
apollo-server-express "2.6.4"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
@ -1566,7 +1557,7 @@ apollo-upload-server@^7.0.0:
http-errors "^1.7.0"
object-path "^0.11.4"
apollo-utilities@1.3.2, apollo-utilities@^1.0.1, apollo-utilities@^1.2.1, apollo-utilities@^1.3.2:
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==
@ -1790,10 +1781,10 @@ babel-core@~7.0.0-0:
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
babel-eslint@~10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
integrity sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==
babel-eslint@~10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/parser" "^7.0.0"
@ -2070,6 +2061,13 @@ busboy@^0.2.14:
dicer "0.2.5"
readable-stream "1.1.x"
busboy@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
dependencies:
dicer "0.3.0"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
@ -2586,10 +2584,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.33:
version "2.0.0-alpha.33"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.33.tgz#c2f73c3cc50ac301c9217eb93603c9bc40e891bf"
integrity sha512-tqUVEk3oxnJuNIvwAMKHAMo4uFRG0zXvjxZQll+BonoPt+m4NMcUgO14NDxbHuy7uYcrVErd2GdSsw02EDZQ7w==
date-fns@2.0.0-alpha.35:
version "2.0.0-alpha.35"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.35.tgz#185813cdc51b05cc1468a95116494bb3f3440934"
integrity sha512-dAY1ujqRtyUsa9mVeupyMzUluWo1d7kAMwyXTQHFImKYSHKvxDw/dipiY6fdswQOs8CwpGoiKysGfaaRP5r3bA==
debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
@ -2721,6 +2719,13 @@ dicer@0.2.5:
readable-stream "1.1.x"
streamsearch "0.1.2"
dicer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
dependencies:
streamsearch "0.1.2"
diff-sequences@^24.3.0:
version "24.3.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.3.0.tgz#0f20e8a1df1abddaf4d9c226680952e64118b975"
@ -2995,10 +3000,10 @@ escodegen@^1.9.1:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@~4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz#c55c1fcac8ce4518aeb77906984e134d9eb5a4f0"
integrity sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA==
eslint-config-prettier@~5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-5.0.0.tgz#f7a94b2b8ae7cbf25842c36fa96c6d32cd0a697c"
integrity sha512-c17Aqiz5e8LEqoc/QPmYnaxQFAHTx2KlCZBPxXXjEMmNchOLnV/7j0HoPZuC+rL/tDC9bazUYOKJW9bOhftI/w==
dependencies:
get-stdin "^6.0.0"
@ -3048,10 +3053,10 @@ eslint-plugin-import@~2.17.3:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.6.4:
version "22.6.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
eslint-plugin-jest@~22.7.0:
version "22.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.0.tgz#a1d325bccb024b04f5354c56fe790baba54a454c"
integrity sha512-0U9nBd9V6+GKpM/KvRDcmMuPsewSsdM7NxCozgJkVAh8IrwHmQ0aw44/eYuVkhT8Fcdhsz0zYiyPtKg147eXMQ==
eslint-plugin-node@~9.1.0:
version "9.1.0"
@ -3535,6 +3540,11 @@ fs-capacitor@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0"
integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ==
fs-capacitor@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
fs-minipass@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
@ -3718,6 +3728,13 @@ graphql-extensions@0.7.2:
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.3.tgz#2ab7331c72ae657e4cbfa4ff004c400b19f56cdf"
integrity sha512-D+FZM0t5gFntJUizeRCurZuUqsyVP13CRejRX+cDJivyGkE6OMWYkCWlzHcbye79q+hYN1m6a3NhlrJRaD9D0w==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@^0.0.x, graphql-extensions@~0.0.9:
version "0.0.10"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d"
@ -3772,12 +3789,12 @@ graphql-request@~1.8.2:
dependencies:
cross-fetch "2.2.2"
graphql-shield@~5.3.8:
version "5.3.8"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.8.tgz#f9e7ad2285f6cfbe20a8a49154ce6c1b184e3893"
integrity sha512-33rQ8U5jMurHIapctHk7hBcUg3nxC7fmMIMtyWiomJXhBmztFq/SG7jNaapnL5M7Q/0BmoaSQd3FLSpelP9KPw==
graphql-shield@~5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98"
integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA==
dependencies:
"@types/yup" "0.26.16"
"@types/yup" "0.26.17"
lightercollective "^0.3.0"
object-hash "^1.3.1"
yup "^0.27.0"
@ -3812,20 +3829,20 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.4:
iterall "^1.1.3"
uuid "^3.1.0"
graphql-upload@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.2.tgz#1c1f116f15b7f8485cf40ff593a21368f0f58856"
integrity sha512-u8a5tKPfJ0rU4MY+B3skabL8pEjMkm3tUzq25KBx6nT0yEWmqUO7Z5tdwvwYLFpkLwew94Gue0ARbZtar3gLTw==
graphql-upload@^8.0.0, graphql-upload@^8.0.2:
version "8.0.7"
resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.7.tgz#8644264e241529552ea4b3797e7ee15809cf01a3"
integrity sha512-gi2yygbDPXbHPC7H0PNPqP++VKSoNoJO4UrXWq4T0Bi4IhyUd3Ycop/FSxhx2svWIK3jdXR/i0vi91yR1aAF0g==
dependencies:
busboy "^0.2.14"
fs-capacitor "^1.0.0"
http-errors "^1.7.1"
busboy "^0.3.1"
fs-capacitor "^2.0.4"
http-errors "^1.7.2"
object-path "^0.11.4"
graphql-yoga@~1.17.4:
version "1.17.4"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.17.4.tgz#6d325a6270399edf0776fb5f60a2e9e19512e63c"
integrity sha512-zOXFtmS43xDLoECKiuA3xVWH/wLDvLH1D/5fBKcaMFdF43ifDfnA9N6dlGggqAoOhqBnrqHwDpoKlJ6sI1LuxA==
graphql-yoga@~1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.18.0.tgz#2668278e94a0bd1b2ff8c60f928c4e18d62e381a"
integrity sha512-WEibitQA2oFTmD7XBO8/ps8DWeVpkzOzgbB3EvtM2oIpyGhPCzRZYrC7OS9MmijvRwLRXsgHImHWUm82ZrIOWA==
dependencies:
"@types/cors" "^2.8.4"
"@types/express" "^4.11.1"
@ -3847,6 +3864,7 @@ graphql-yoga@~1.17.4:
graphql-playground-middleware-lambda "1.7.12"
graphql-subscriptions "^0.5.8"
graphql-tools "^4.0.0"
graphql-upload "^8.0.0"
subscriptions-transport-ws "^0.9.8"
"graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", graphql@^14.2.1, graphql@~14.3.1:
@ -4045,7 +4063,7 @@ htmlparser2@^3.10.0, htmlparser2@^3.9.1:
inherits "^2.0.1"
readable-stream "^3.0.6"
http-errors@1.7.2, http-errors@~1.7.2:
http-errors@1.7.2, http-errors@^1.7.2, http-errors@~1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@ -4056,7 +4074,7 @@ http-errors@1.7.2, http-errors@~1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@^1.7.0, http-errors@^1.7.1:
http-errors@^1.7.0:
version "1.7.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027"
integrity sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==
@ -5693,6 +5711,11 @@ node-releases@^1.1.19:
dependencies:
semver "^5.3.0"
nodemailer@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.2.1.tgz#20d773925eb8f7a06166a0b62c751dc8290429f3"
integrity sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==
nodemon@~1.19.1:
version "1.19.1"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071"
@ -7543,13 +7566,6 @@ trunc-text@1.0.1:
resolved "https://registry.yarnpkg.com/trunc-text/-/trunc-text-1.0.1.tgz#58f876d8ac59b224b79834bb478b8656e69622b5"
integrity sha1-WPh22KxZsiS3mDS7R4uGVuaWIrU=
ts-invariant@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0"
integrity sha512-QsY8BCaRnHiB5T6iE4DPlJMAKEG3gzMiUco9FEt1jUXQf0XP6zi0idT0i0rMTu8A326JqNSDsmlkA9dRSh1TRg==
dependencies:
tslib "^1.9.3"
ts-invariant@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.2.tgz#8685131b8083e67c66d602540e78763408be9113"
@ -8112,10 +8128,10 @@ yup@^0.27.0:
synchronous-promise "^2.0.6"
toposort "^2.0.2"
zen-observable-ts@^0.8.18:
version "0.8.18"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8"
integrity sha512-q7d05s75Rn1j39U5Oapg3HI2wzriVwERVo4N7uFGpIYuHB9ff02P/E92P9B8T7QVC93jCMHpbXH7X0eVR5LA7A==
zen-observable-ts@^0.8.19:
version "0.8.19"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"
integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==
dependencies:
tslib "^1.9.3"
zen-observable "^0.8.0"

View File

@ -5,6 +5,11 @@ data:
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
SMTP_HOST:
SMTP_PORT: 587
SMTP_USERNAME:
SMTP_PASSWORD:
SMTP_IGNORE_TLS:
metadata:
name: human-connection
namespace: human-connection

View File

@ -45,7 +45,7 @@ MERGE(b:Badge {id: badge._id["$oid"]})
ON CREATE SET
b.key = badge.key,
b.type = badge.type,
b.icon = badge.image.path,
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
b.status = badge.status,
b.createdAt = badge.createdAt.`$date`,
b.updatedAt = badge.updatedAt.`$date`

View File

@ -28,7 +28,7 @@
[?] unique: true, // Unique value is not enforced in Nitro?
[-] index: true
},
[ ] type: {
[ ] type: { // db.getCollection('contributions').distinct('type') -> 'DELETED', 'cando', 'post'
[ ] type: String,
[ ] required: true,
[-] index: true
@ -50,7 +50,7 @@
[?] required: true // Not required in Nitro
},
[ ] hasMore: { type: Boolean },
[?] teaserImg: { type: String }, // Path is incorrect in Nitro
[X] teaserImg: { type: String },
[ ] language: {
[ ] type: String,
[ ] required: true,
@ -131,7 +131,7 @@ MERGE (p:Post {id: post._id["$oid"]})
ON CREATE SET
p.title = post.title,
p.slug = post.slug,
p.image = post.teaserImg,
p.image = replace(post.teaserImg, 'https://api-alpha.human-connection.org', ''),
p.content = post.content,
p.contentExcerpt = post.contentExcerpt,
p.visibility = toLower(post.visibility),

View File

@ -49,8 +49,8 @@
}
},
[ ] timezone: { type: String },
[?] avatar: { type: String }, // Path is incorrect in Nitro
[?] coverImg: { type: String }, // Path is incorrect in Nitro, was not modeled in latest Nitro - do we want this?
[X] avatar: { type: String },
[X] coverImg: { type: String },
[ ] doiToken: { type: String },
[ ] confirmedAt: { type: Date },
[?] badgeIds: [], // Verify this is working properly
@ -102,8 +102,8 @@ u.name = user.name,
u.slug = user.slug,
u.email = user.email,
u.password = user.password,
u.avatar = user.avatar,
u.coverImg = user.coverImg,
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
u.wasInvited = user.wasInvited,
u.wasSeeded = user.wasSeeded,
u.role = toLower(user.role),

View File

@ -1,6 +1,12 @@
version: "3.4"
services:
mailserver:
image: djfarrelly/maildev
ports:
- 1080:80
networks:
- hc-network
webapp:
build:
context: webapp
@ -20,6 +26,10 @@ services:
- backend_node_modules:/nitro-backend/node_modules
- uploads:/nitro-backend/public/uploads
command: yarn run dev
environment:
- SMTP_HOST=mailserver
- SMTP_PORT=25
- SMTP_IGNORE_TLS=true
neo4j:
environment:
- NEO4J_AUTH=none

View File

@ -1,9 +1,11 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import ContributionForm from './index.vue'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['no-ssr'] = '<span><slot /></span>'
@ -55,8 +57,16 @@ describe('ContributionForm.vue', () => {
})
describe('mount', () => {
const getters = {
'editor/placeholder': () => {
return 'some cool placeholder'
},
}
const store = new Vuex.Store({
getters,
})
const Wrapper = () => {
return mount(ContributionForm, { mocks, localVue, computed })
return mount(ContributionForm, { mocks, localVue, computed, store })
}
beforeEach(() => {

View File

@ -4,7 +4,12 @@
<ds-card>
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
<no-ssr>
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
<hc-editor
:users="users"
:hashtags="hashtags"
:value="form.content"
@input="updateEditorContent"
/>
</no-ssr>
<ds-space margin-bottom="xxx-large" />
<ds-input
@ -89,6 +94,7 @@ export default {
disabled: false,
slug: null,
users: [],
hashtags: [],
}
},
watch: {
@ -173,17 +179,34 @@ export default {
apollo: {
User: {
query() {
return gql(`{
User(orderBy: slug_asc) {
id
slug
return gql`
{
User(orderBy: slug_asc) {
id
slug
}
}
}`)
`
},
result(result) {
this.users = result.data.User
},
},
Tag: {
query() {
return gql`
{
Tag(orderBy: name_asc) {
id
name
}
}
`
},
result(result) {
this.hashtags = result.data.Tag
},
},
},
}
</script>

View File

@ -83,6 +83,7 @@ export default {
deleteContributions: false,
deleteComments: false,
deleteEnabled: false,
enableDeletionValue: null,
}
},
computed: {

View File

@ -3,17 +3,19 @@
<div v-show="showSuggestions" ref="suggestions" class="suggestion-list">
<template v-if="hasResults">
<div
v-for="(user, index) in filteredUsers"
:key="user.id"
v-for="(item, index) in filteredItems"
:key="item.id"
class="suggestion-list__item"
:class="{ 'is-selected': navigatedUserIndex === index }"
@click="selectUser(user)"
:class="{ 'is-selected': navigatedItemIndex === index }"
@click="selectItem(item)"
>
@{{ user.slug }}
<div v-if="isMention">@{{ item.slug }}</div>
<div v-if="isHashtag">#{{ item.name }}</div>
</div>
</template>
<div v-else class="suggestion-list__item is-empty">
No users found
<div v-if="isMention">{{ $t('editor.mention.noUsersFound') }}</div>
<div v-if="isHashtag">{{ $t('editor.hashtag.noHashtagsFound') }}</div>
</div>
</div>
@ -175,6 +177,8 @@ import {
History,
} from 'tiptap-extensions'
import Mention from './nodes/Mention.js'
import Hashtag from './nodes/Hashtag.js'
import { mapGetters } from 'vuex'
let throttleInputEvent
@ -186,6 +190,7 @@ export default {
},
props: {
users: { type: Array, default: () => [] },
hashtags: { type: Array, default: () => [] },
value: { type: String, default: '' },
doc: { type: Object, default: () => {} },
},
@ -212,38 +217,40 @@ export default {
new ListItem(),
new Placeholder({
emptyNodeClass: 'is-empty',
emptyNodeText: this.$t('editor.placeholder'),
emptyNodeText: this.placeholder || this.$t('editor.placeholder'),
}),
new History(),
new Mention({
items: () => {
this.suggestionType = this.mentionSuggestionType
return this.users
},
onEnter: ({ items, query, range, command, virtualNode }) => {
this.query = query
this.filteredUsers = items
this.filteredItems = items
this.suggestionRange = range
this.renderPopup(virtualNode)
// we save the command for inserting a selected mention
// this allows us to call it inside of our custom popup
// via keyboard navigation and on click
this.insertMention = command
this.insertMentionOrHashtag = command
},
// is called when a suggestion has changed
onChange: ({ items, query, range, virtualNode }) => {
this.query = query
this.filteredUsers = items
this.filteredItems = items
this.suggestionRange = range
this.navigatedUserIndex = 0
this.navigatedItemIndex = 0
this.renderPopup(virtualNode)
},
// is called when a suggestion is cancelled
onExit: () => {
// reset all saved values
this.suggestionType = this.nullSuggestionType
this.query = null
this.filteredUsers = []
this.filteredItems = []
this.suggestionRange = null
this.navigatedUserIndex = 0
this.navigatedItemIndex = 0
this.destroyPopup()
},
// is called on every keyDown event while a suggestion is active
@ -260,7 +267,74 @@ export default {
}
// pressing enter
if (event.keyCode === 13) {
this.enterHandler()
this.enterHandler(this.mentionSuggestionType)
return true
}
return false
},
// is called when a suggestion has changed
// this function is optional because there is basic filtering built-in
// you can overwrite it if you prefer your own filtering
// in this example we use fuse.js with support for fuzzy search
onFilter: (items, query) => {
if (!query) {
return items
}
const fuse = new Fuse(items, {
threshold: 0.2,
keys: ['slug'],
})
return fuse.search(query)
},
}),
new Hashtag({
items: () => {
this.suggestionType = this.hashtagSuggestionType
return this.hashtags
},
onEnter: ({ items, query, range, command, virtualNode }) => {
this.query = query
this.filteredItems = items
this.suggestionRange = range
this.renderPopup(virtualNode)
// we save the command for inserting a selected mention
// this allows us to call it inside of our custom popup
// via keyboard navigation and on click
this.insertMentionOrHashtag = command
},
// is called when a suggestion has changed
onChange: ({ items, query, range, virtualNode }) => {
this.query = query
this.filteredItems = items
this.suggestionRange = range
this.navigatedItemIndex = 0
this.renderPopup(virtualNode)
},
// is called when a suggestion is cancelled
onExit: () => {
// reset all saved values
this.suggestionType = this.nullSuggestionType
this.query = null
this.filteredItems = []
this.suggestionRange = null
this.navigatedItemIndex = 0
this.destroyPopup()
},
// is called on every keyDown event while a suggestion is active
onKeyDown: ({ event }) => {
// pressing up arrow
if (event.keyCode === 38) {
this.upHandler()
return true
}
// pressing down arrow
if (event.keyCode === 40) {
this.downHandler()
return true
}
// pressing enter or pressing space
if (event.keyCode === 13 || event.keyCode === 32) {
this.enterHandler(this.hashtagSuggestionType)
return true
}
return false
@ -288,21 +362,32 @@ export default {
}),
linkUrl: null,
linkMenuIsActive: false,
nullSuggestionType: '',
mentionSuggestionType: 'mention',
hashtagSuggestionType: 'hashtag',
suggestionType: this.nullSuggestionType,
query: null,
suggestionRange: null,
filteredUsers: [],
navigatedUserIndex: 0,
insertMention: () => {},
filteredItems: [],
navigatedItemIndex: 0,
insertMentionOrHashtag: () => {},
observer: null,
}
},
computed: {
...mapGetters({ placeholder: 'editor/placeholder' }),
hasResults() {
return this.filteredUsers.length
return this.filteredItems.length
},
showSuggestions() {
return this.query || this.hasResults
},
isMention() {
return this.suggestionType === this.mentionSuggestionType
},
isHashtag() {
return this.suggestionType === this.hashtagSuggestionType
},
},
watch: {
value: {
@ -316,47 +401,58 @@ export default {
this.editor.setContent(content)
},
},
},
mounted() {
this.$root.$on('changeLanguage', () => {
this.changePlaceHolderText()
})
placeholder: {
immediate: true,
handler: function(val) {
if (!val) {
return
}
this.editor.extensions.options.placeholder.emptyNodeText = val
},
},
},
beforeDestroy() {
this.$root.$off('changeLanguage')
this.editor.destroy()
},
methods: {
changePlaceHolderText() {
this.editor.extensions.options.placeholder.emptyNodeText = this.$t('editor.placeholder')
},
// navigate to the previous item
// if it's the first item, navigate to the last one
upHandler() {
this.navigatedUserIndex =
(this.navigatedUserIndex + this.filteredUsers.length - 1) % this.filteredUsers.length
this.navigatedItemIndex =
(this.navigatedItemIndex + this.filteredItems.length - 1) % this.filteredItems.length
},
// navigate to the next item
// if it's the last item, navigate to the first one
downHandler() {
this.navigatedUserIndex = (this.navigatedUserIndex + 1) % this.filteredUsers.length
this.navigatedItemIndex = (this.navigatedItemIndex + 1) % this.filteredItems.length
},
// For hashtags handles pressing of space as enter.
enterHandler() {
const user = this.filteredUsers[this.navigatedUserIndex]
if (user) {
this.selectUser(user)
const item = this.filteredItems[this.navigatedItemIndex]
if (item) {
this.selectItem(item)
} else if (this.suggestionType === this.hashtagSuggestionType && this.query !== '') {
this.selectItem({ name: this.query }, this.hashtagSuggestionType)
}
},
// we have to replace our suggestion text with a mention
// so it's important to pass also the position of your suggestion text
selectUser(user) {
this.insertMention({
range: this.suggestionRange,
attrs: {
selectItem(item) {
const typeAttrs = {
mention: {
// TODO: use router here
url: `/profile/${user.id}`,
label: user.slug,
url: `/profile/${item.id}`,
label: item.slug,
},
hashtag: {
// TODO: Fill up with input hashtag in search field
url: `/search/hashtag:${item.name}`,
label: item.name,
},
}
this.insertMentionOrHashtag({
range: this.suggestionRange,
attrs: typeAttrs[this.suggestionType],
})
this.editor.focus()
},
@ -535,6 +631,12 @@ li > p {
.mention-suggestion {
color: $color-primary;
}
.hashtag {
color: $color-primary;
}
.hashtag-suggestion {
color: $color-primary;
}
&__floating-menu {
position: absolute;
margin-top: -0.25rem;

View File

@ -0,0 +1,44 @@
import { Mention as TipTapMention } from 'tiptap-extensions'
export default class Hashtag extends TipTapMention {
get name() {
return 'hashtag'
}
get defaultOptions() {
return {
matcher: {
char: '#',
allowSpaces: false,
startOfLine: false,
},
mentionClass: 'hashtag',
suggestionClass: 'hashtag-suggestion',
}
}
get schema() {
const patchedSchema = super.schema
patchedSchema.attrs = {
url: {},
label: {},
}
patchedSchema.toDOM = node => {
return [
'a',
{
class: this.options.mentionClass,
href: node.attrs.url,
target: '_blank',
// contenteditable: 'true',
},
`${this.options.matcher.char}${node.attrs.label}`,
]
}
patchedSchema.parseDOM = [
// this is not implemented
]
return patchedSchema
}
}

View File

@ -1,6 +1,10 @@
import { Mention as TipTapMention } from 'tiptap-extensions'
export default class Mention extends TipTapMention {
get name() {
return 'mention'
}
get schema() {
const patchedSchema = super.schema

View File

@ -1,31 +1,43 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Editor from './'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('Editor.vue', () => {
let wrapper
let propsData
let mocks
let getters
beforeEach(() => {
propsData = {}
mocks = {
$t: () => {},
}
getters = {
'editor/placeholder': () => {
return 'some cool placeholder'
},
}
})
describe('mount', () => {
let Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return (wrapper = mount(Editor, {
mocks,
propsData,
localVue,
sync: false,
stubs: { transition: false },
store,
}))
}
@ -43,5 +55,13 @@ describe('Editor.vue', () => {
expect(wrapper.find('.ProseMirror').text()).toContain('I am a piece of text')
})
})
describe('uses the placeholder', () => {
it('from the store', () => {
expect(wrapper.vm.editor.extensions.options.placeholder.emptyNodeText).toEqual(
'some cool placeholder',
)
})
})
})
})

View File

@ -1,10 +1,12 @@
import { mount, createLocalVue } from '@vue/test-utils'
import FilterMenu from './FilterMenu.vue'
import Styleguide from '@human-connection/styleguide'
import VTooltip from 'v-tooltip'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
describe('FilterMenu.vue', () => {
let wrapper

View File

@ -2,13 +2,16 @@
<ds-card>
<ds-flex>
<ds-flex-item class="filter-menu-title">
<ds-heading size="h3">
{{ $t('filter-menu.title') }}
</ds-heading>
<ds-heading size="h3">{{ $t('filter-menu.title') }}</ds-heading>
</ds-flex-item>
<ds-flex-item>
<div class="filter-menu-buttons">
<ds-button
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="!!filterAuthorIsFollowedById"

View File

@ -0,0 +1,68 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import VTooltip from 'v-tooltip'
import LocaleSwitch from './LocaleSwitch.vue'
import { mutations } from '~/store/editor'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
localVue.use(VTooltip)
describe('LocaleSwitch.vue', () => {
let wrapper
let mocks
let computed
let deutschLanguageItem
beforeEach(() => {
mocks = {
$i18n: {
locale: () => 'de',
set: jest.fn(),
},
$t: jest.fn(),
setPlaceholderText: jest.fn(),
}
computed = {
current: () => {
return { code: 'en' }
},
routes: () => {
return [
{
name: 'English',
path: 'en',
},
{
name: 'Deutsch',
path: 'de',
},
]
},
}
})
describe('mount', () => {
const store = new Vuex.Store({
mutations: {
'editor/SET_PLACEHOLDER_TEXT': mutations.SET_PLACEHOLDER_TEXT,
},
})
const Wrapper = () => {
return mount(LocaleSwitch, { mocks, localVue, store, computed })
}
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('.locale-menu').trigger('click')
deutschLanguageItem = wrapper.findAll('li').at(1)
deutschLanguageItem.trigger('click')
})
it("changes a user's locale", () => {
expect(mocks.$i18n.set).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -36,6 +36,7 @@
import Dropdown from '~/components/Dropdown'
import find from 'lodash/find'
import orderBy from 'lodash/orderBy'
import { mapMutations } from 'vuex'
export default {
components: {
@ -65,10 +66,11 @@ export default {
},
},
methods: {
...mapMutations({ setPlaceholderText: 'editor/SET_PLACEHOLDER_TEXT' }),
changeLanguage(locale, toggleMenu) {
this.$i18n.set(locale)
toggleMenu()
this.$root.$emit('changeLanguage')
this.setPlaceholderText(this.$t('editor.placeholder'))
},
matcher(locale) {
return locale === this.$i18n.locale()

View File

@ -11,18 +11,21 @@
id="oldPassword"
model="oldPassword"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-old-password')"
/>
<ds-input
id="newPassword"
model="newPassword"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.newPassword" />

View File

@ -0,0 +1,83 @@
import { mount, createLocalVue } from '@vue/test-utils'
import ChangePassword from './ChangePassword'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('ChangePassword ', () => {
let wrapper
let Wrapper
let mocks
let propsData
beforeEach(() => {
propsData = {}
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }),
},
}
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
return mount(ChangePassword, {
mocks,
propsData,
localVue,
})
}
describe('given email and verification code', () => {
beforeEach(() => {
propsData.email = 'mail@example.org'
propsData.code = '123456'
})
describe('submitting new password', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('input#newPassword').setValue('supersecret')
wrapper.find('input#confirmPassword').setValue('supersecret')
wrapper.find('form').trigger('submit')
})
it('calls resetPassword graphql mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('delivers new password to backend', () => {
const expected = expect.objectContaining({
variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('password reset successful', () => {
it('displays success message', () => {
const expected = 'verify-code.form.change-password.success'
expect(mocks.$t).toHaveBeenCalledWith(expected)
})
describe('after animation', () => {
beforeEach(jest.runAllTimers)
it('emits `change-password-sucess`', () => {
expect(wrapper.emitted('passwordResetResponse')).toEqual([['success']])
})
})
})
})
})
})
})

View File

@ -0,0 +1,140 @@
<template>
<ds-card class="verify-code">
<ds-space margin="large">
<template>
<ds-form
v-if="!changePasswordResult"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitPassword"
@input="handleInput"
@input-valid="handleInputValid"
class="change-password"
>
<ds-input
id="newPassword"
model="newPassword"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.newPassword" />
<ds-space margin-top="base">
<ds-button :loading="$apollo.loading" :disabled="disabled" primary>
{{ $t('settings.security.change-password.button') }}
</ds-button>
</ds-space>
</ds-form>
<ds-text v-else>
<template v-if="changePasswordResult === 'success'">
<sweetalert-icon icon="success" />
<ds-text>
{{ $t(`verify-code.form.change-password.success`) }}
</ds-text>
</template>
<template v-else>
<sweetalert-icon icon="error" />
<ds-text align="left">
{{ $t(`verify-code.form.change-password.error`) }}
{{ $t('verify-code.form.change-password.help') }}
</ds-text>
<a href="mailto:support@human-connection.org">support@human-connection.org</a>
</template>
</ds-text>
</template>
</ds-space>
</ds-card>
</template>
<script>
import PasswordStrength from '../Password/Strength'
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
PasswordStrength,
},
props: {
email: { type: String, required: true },
code: { type: String, required: true },
},
data() {
return {
formData: {
newPassword: '',
confirmPassword: '',
},
formSchema: {
newPassword: {
type: 'string',
required: true,
message: this.$t('settings.security.change-password.message-new-password-required'),
},
confirmPassword: [
{ validator: this.matchPassword },
{
type: 'string',
required: true,
message: this.$t(
'settings.security.change-password.message-new-password-confirm-required',
),
},
],
},
disabled: true,
changePasswordResult: null,
}
},
methods: {
async handleInput() {
this.disabled = true
},
async handleInputValid() {
this.disabled = false
},
async handleSubmitPassword() {
const mutation = gql`
mutation($code: String!, $email: String!, $newPassword: String!) {
resetPassword(code: $code, email: $email, newPassword: $newPassword)
}
`
const { newPassword } = this.formData
const { email, code } = this
const variables = { newPassword, email, code }
try {
const {
data: { resetPassword },
} = await this.$apollo.mutate({ mutation, variables })
this.changePasswordResult = resetPassword ? 'success' : 'error'
setTimeout(() => {
this.$emit('passwordResetResponse', this.changePasswordResult)
}, 3000)
this.formData = {
newPassword: '',
confirmPassword: '',
}
} catch (err) {
this.$toast.error(err.message)
}
},
matchPassword(rule, value, callback, source, options) {
var errors = []
if (this.formData.newPassword !== value) {
errors.push(
new Error(this.$t('settings.security.change-password.message-new-password-missmatch')),
)
}
callback(errors)
},
},
}
</script>

View File

@ -0,0 +1,77 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Request from './Request'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('Request', () => {
let wrapper
let Wrapper
let mocks
beforeEach(() => {
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn().mockResolvedValue({ data: { reqestPasswordReset: true } }),
},
}
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
return mount(Request, {
mocks,
localVue,
})
}
it('renders a password reset form', () => {
wrapper = Wrapper()
expect(wrapper.find('.password-reset').exists()).toBe(true)
})
describe('submit', () => {
beforeEach(async () => {
wrapper = Wrapper()
wrapper.find('input#email').setValue('mail@example.org')
await wrapper.find('form').trigger('submit')
})
it('calls requestPasswordReset graphql mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('delivers email to backend', () => {
const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('hides form to avoid re-submission', () => {
expect(wrapper.find('form').exists()).not.toBeTruthy()
})
it('displays a message that a password email was requested', () => {
const expected = ['password-reset.form.submitted', { email: 'mail@example.org' }]
expect(mocks.$t).toHaveBeenCalledWith(...expected)
})
describe('after animation', () => {
beforeEach(jest.runAllTimers)
it('emits `handleSubmitted`', () => {
expect(wrapper.emitted('handleSubmitted')).toEqual([[{ email: 'mail@example.org' }]])
})
})
})
})
})

View File

@ -0,0 +1,107 @@
<template>
<ds-card class="password-reset">
<ds-space margin="large">
<ds-form
v-if="!submitted"
@input="handleInput"
@input-valid="handleInputValid"
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
>
<ds-input
:placeholder="$t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<ds-space margin-botton="large">
<ds-text>
{{ $t('password-reset.form.description') }}
</ds-text>
</ds-space>
<ds-button
:disabled="disabled"
:loading="$apollo.loading"
primary
fullwidth
name="submit"
type="submit"
icon="envelope"
>
{{ $t('password-reset.form.submit') }}
</ds-button>
</ds-form>
<div v-else>
<transition name="ds-transition-fade">
<ds-flex centered>
<sweetalert-icon icon="info" />
</ds-flex>
</transition>
<ds-text v-html="submitMessage" />
</div>
</ds-space>
</ds-card>
</template>
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
},
data() {
return {
formData: {
email: '',
},
formSchema: {
email: {
type: 'email',
required: true,
message: this.$t('common.validations.email'),
},
},
disabled: true,
submitted: false,
}
},
computed: {
submitMessage() {
const { email } = this.formData
return this.$t('password-reset.form.submitted', { email })
},
},
methods: {
handleInput() {
this.disabled = true
},
handleInputValid() {
this.disabled = false
},
async handleSubmit() {
const mutation = gql`
mutation($email: String!) {
requestPasswordReset(email: $email)
}
`
const { email } = this.formData
try {
await this.$apollo.mutate({ mutation, variables: { email } })
this.submitted = true
setTimeout(() => {
this.$emit('handleSubmitted', { email })
}, 3000)
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -0,0 +1,53 @@
import { mount, createLocalVue } from '@vue/test-utils'
import VerifyCode from './VerifyCode'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('VerifyCode ', () => {
let wrapper
let Wrapper
let mocks
let propsData
beforeEach(() => {
mocks = {
$t: jest.fn(),
}
propsData = {
email: 'mail@example.org',
}
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
return mount(VerifyCode, {
mocks,
localVue,
propsData,
})
}
it('renders a verify code form', () => {
wrapper = Wrapper()
expect(wrapper.find('.verify-code').exists()).toBe(true)
})
describe('after verification code given', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('input#code').setValue('123456')
wrapper.find('form').trigger('submit')
})
it('emits `verifyCode`', () => {
const expected = [[{ code: '123456', email: 'mail@example.org' }]]
expect(wrapper.emitted('verification')).toEqual(expected)
})
})
})
})

View File

@ -0,0 +1,67 @@
<template>
<ds-card class="verify-code">
<ds-space margin="large">
<ds-form
v-model="formData"
:schema="formSchema"
@submit="handleSubmitVerify"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-input
:placeholder="$t('verify-code.form.code')"
model="code"
name="code"
id="code"
icon="question-circle"
/>
<ds-space margin-botton="large">
<ds-text>
{{ $t('verify-code.form.description') }}
</ds-text>
</ds-space>
<ds-button :disabled="disabled" primary fullwidth name="submit" type="submit">
{{ $t('verify-code.form.next') }}
</ds-button>
</ds-form>
</ds-space>
</ds-card>
</template>
<script>
export default {
props: {
email: { type: String, required: true },
},
data() {
return {
formData: {
code: '',
},
formSchema: {
code: {
type: 'string',
min: 6,
max: 6,
required: true,
message: this.$t('common.validations.verification-code'),
},
},
disabled: true,
}
},
methods: {
async handleInput() {
this.disabled = true
},
async handleInputValid() {
this.disabled = false
},
handleSubmitVerify() {
const { code } = this.formData
const email = this.email
this.$emit('verification', { email, code })
},
},
}
</script>

View File

@ -1,9 +1,10 @@
import { mount, createLocalVue, createWrapper } from '@vue/test-utils'
import CommentForm from './index.vue'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('CommentForm.vue', () => {
@ -35,8 +36,16 @@ describe('CommentForm.vue', () => {
})
describe('mount', () => {
const getters = {
'editor/placeholder': () => {
return 'some cool placeholder'
},
}
const store = new Vuex.Store({
getters,
})
const Wrapper = () => {
return mount(CommentForm, { mocks, localVue, propsData })
return mount(CommentForm, { mocks, localVue, propsData, store })
}
beforeEach(() => {

View File

@ -86,7 +86,7 @@ describe('CommentList.vue', () => {
})
it('displays comments when there are comments to display', () => {
expect(wrapper.find('div#comments').text()).toEqual('this is a comment')
expect(wrapper.find('div.comments').text()).toEqual('this is a comment')
})
it("refetches a post's comments from the backend", () => {

View File

@ -1,5 +1,5 @@
<template>
<div>
<div id="comments">
<h3 style="margin-top: -10px;">
<span>
<ds-icon name="comments" />
@ -16,7 +16,7 @@
</span>
</h3>
<ds-space margin-bottom="large" />
<div v-if="comments && comments.length" id="comments" class="comments">
<div v-if="comments && comments.length" class="comments">
<comment
v-for="(comment, index) in comments"
:key="comment.id"

View File

@ -92,7 +92,7 @@
<script>
import { mapGetters, mapActions } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import SearchInput from '~/components/SearchInput.vue'
import Modal from '~/components/Modal'
import NotificationMenu from '~/components/notifications/NotificationMenu'

View File

@ -8,20 +8,47 @@
"logout": "Ausloggen",
"email": "Deine E-Mail",
"password": "Dein Passwort",
"forgotPassword": "Passwort vergessen?",
"moreInfo": "Was ist Human Connection?",
"moreInfoURL": "https://human-connection.org",
"moreInfoHint": "zur Präsentationsseite",
"hello": "Hallo"
},
"password-reset": {
"title": "Passwort zurücksetzen",
"form": {
"description": "Eine Mail zum Zurücksetzen des Passworts wird an die angegebene E-Mail Adresse geschickt.",
"submit": "Email anfordern",
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
}
},
"verify-code": {
"form": {
"code": "Code eingeben",
"description": "Öffne dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
"next": "Weiter",
"change-password":{
"success": "Änderung des Passworts war erfolgreich!",
"error": "Passwort Änderung fehlgeschlagen. Möglicherweise falscher Sicherheitscode?",
"help": "Falls Probleme auftreten, schreib uns gerne eine Mail an:"
}
}
},
"editor": {
"placeholder": "Schreib etwas Inspirierendes..."
"placeholder": "Schreib etwas Inspirierendes...",
"mention": {
"noUsersFound": "Keine Benutzer gefunden"
},
"hashtag": {
"noHashtagsFound": "Keine Hashtags gefunden"
}
},
"profile": {
"name": "Mein Profil",
"memberSince": "Mitglied seit",
"follow": "Folgen",
"followers": "Folgen",
"following": "Folgt",
"follow": "abonnieren",
"followers": "Abonnenten",
"following": "abonniert",
"shouted": "Empfohlen",
"commented": "Kommentiert",
"userAnonym": "Anonymus",
@ -106,9 +133,9 @@
}
},
"admin": {
"name": "Systemverwaltung",
"name": "Admin",
"dashboard": {
"name": "Startzentrale",
"name": "Dashboard",
"users": "Benutzer",
"posts": "Beiträge",
"comments": "Kommentare",
@ -117,7 +144,7 @@
"projects": "Projekte",
"invites": "Einladungen",
"follows": "Folgen",
"shouts": "Shouts"
"shouts": "Empfehlungen"
},
"organizations": {
"name": "Organisationen"
@ -193,7 +220,11 @@
"name": "Name",
"loadMore": "mehr laden",
"loading": "wird geladen",
"reportContent": "Melden"
"reportContent": "Melden",
"validations": {
"email": "muss eine gültige E-Mail Adresse sein",
"verification-code": "muss genau 6 Buchstaben lang sein"
}
},
"actions": {
"loading": "lade",
@ -271,7 +302,7 @@
},
"followButton": {
"follow": "Folgen",
"following": "Folge Ich"
"following": "Folge ich"
},
"shoutButton": {
"shouted": "empfohlen"
@ -303,9 +334,13 @@
}
},
"contribution": {
"newPost": "Erstelle einen neuen Beitrag",
"filterFollow": "Beiträge filtern von Usern denen ich folge",
"filterALL": "Alle Beiträge anzeigen",
"success": "Gespeichert!",
"languageSelectLabel": "Sprache",
"tagsInputLabel": "Tags",
"tagsInputPlaceholder": "Hinzufügen ein Tag"
}
}

View File

@ -8,13 +8,40 @@
"logout": "Logout",
"email": "Your Email",
"password": "Your Password",
"forgotPassword": "Forgot Password?",
"moreInfo": "What is Human Connection?",
"moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "to the presentation page",
"hello": "Hello"
},
"password-reset": {
"title": "Reset your password",
"form": {
"description": "A password reset email will be sent to the given email address.",
"submit": "Request email",
"submitted": "A mail with further instruction has been sent to <b>{email}</b>"
}
},
"verify-code": {
"form": {
"code": "Enter your code",
"description": "Open your inbox and enter the code that we've sent to you.",
"next": "Continue",
"change-password": {
"success": "Changing your password was successful!",
"error": "Changing your password failed. Maybe the security code was not correct?",
"help": "In case of problems, feel free to ask for help by sending us a mail to:"
}
}
},
"editor": {
"placeholder": "Leave your inspirational thoughts..."
"placeholder": "Leave your inspirational thoughts...",
"mention": {
"noUsersFound": "No users found"
},
"hashtag": {
"noHashtagsFound": "No hashtags found"
}
},
"profile": {
"name": "My Profile",
@ -22,7 +49,7 @@
"follow": "Follow",
"followers": "Followers",
"following": "Following",
"shouted": "Shouted",
"shouted": "Recommended",
"commented": "Commented",
"userAnonym": "Anonymous",
"socialMedia": "Where else can I find",
@ -38,7 +65,7 @@
},
"notifications": {
"menu": {
"mentioned": "has mentioned you in a post"
"mentioned": "mentioned you in a post"
}
},
"search": {
@ -52,7 +79,7 @@
"name": "Your data",
"labelName": "Your Name",
"namePlaceholder": "Femanon Funny",
"labelCity": "Your City or Region",
"labelCity": "Su ciudad o región",
"labelBio": "About You",
"success": "Your data was successfully updated!"
},
@ -117,7 +144,7 @@
"projects": "Projects",
"invites": "Invites",
"follows": "Follows",
"shouts": "Shouts"
"shouts": "Recommended"
},
"organizations": {
"name": "Organizations"
@ -193,7 +220,11 @@
"name": "Name",
"loadMore": "load more",
"loading": "loading",
"reportContent": "Report"
"reportContent": "Report",
"validations": {
"email": "must be a valid email address",
"verification-code": "must be 6 characters long"
}
},
"actions": {
"loading": "loading",
@ -302,6 +333,9 @@
}
},
"contribution": {
"newPost": "Create a new Post",
"filterFollow": "Filter contributions from users I follow",
"filterALL": "View all contributions",
"success": "Saved!",
"languageSelectLabel": "Language",
"tagsInputLabel": "Tags",

View File

@ -1,4 +1,7 @@
{
"filter-menu": {
"title": "Su burbuja de filtro"
},
"login": {
"copy": "Si ya tiene una cuenta de Human Connection, inicie sesión aquí.",
"login": "Iniciar sesión",
@ -6,39 +9,85 @@
"email": "Tu correo electrónico",
"password": "Tu contraseña",
"moreInfo": "¿Qué es Human Connection?",
"moreInfoURL": "https://human-connection.org/es/",
"moreInfoHint": "a la página de presentación",
"hello": "Hola"
},
"editor": {
"placeholder": "Write something inspiring..."
},
"profile": {
"name": "Mi perfil",
"name": "Mi Perfil",
"memberSince": "Miembro desde",
"follow": "Seguir",
"followers": "Seguidores",
"following": "Siguiendo",
"shouted": "Gritar",
"commented": "Comentado"
"shouted": "Recomendado",
"commented": "Comentado",
"userAnonym": "Anónimo",
"socialMedia": "¿Dónde más puedo encontrar"
},
"notifications": {
"menu": {
"mentioned": "te ha mencionado en un post"
}
},
"search": {
"placeholder": "Buscar",
"hint": "¿Qué estás buscando?",
"failed": "no encontró nada"
},
"settings": {
"name": "Configuración",
"data": {
"name": "Sus datos"
"name": "Sus datos",
"labelName": "Su nombre",
"namePlaceholder": "Femanon Funny",
"labelCity": "Your City or Region",
"labelBio": "Acerca de usted",
"success": "Sus datos han sido actualizados con éxito!"
},
"security": {
"name": "Seguridad"
"name": "Seguridad",
"change-password": {
"button": "Cambiar contraseña",
"success": "Contraseña cambiada con éxito!",
"label-old-password": "Su contraseña antigua",
"label-new-password": "Su nueva contraseña",
"label-new-password-confirm": "Confirm new password",
"message-old-password-required": "Ingrese su contraseña anterior",
"message-new-password-required": "Introduzca una nueva contraseña",
"message-new-password-confirm-required": "Confirme su nueva contraseña",
"message-new-password-missmatch": "Vuelva a escribir la misma contraseña",
"passwordSecurity": "Seguridad de la contraseña",
"passwordStrength0": "Contraseña muy insegura",
"passwordStrength1": "Contraseña insegura",
"passwordStrength2": "Contraseña mediocre",
"passwordStrength3": "Contraseña segura",
"passwordStrength4": "Contraseña muy sólida"
}
},
"invites": {
"name": "Invita"
"name": "invitaciones"
},
"download": {
"name": "Descargar datos"
},
"delete": {
"name": "Borrar cuenta"
"name": "Eliminar cuenta"
},
"organizations": {
"name": "Mis organizaciones"
},
"languages": {
"name": "Idiomas"
"name": "idiomas"
},
"social-media": {
"name": "Medios de comunicación social",
"placeholder": "Agregar una URL de Social-Media",
"submit": "Añadir enlace",
"successAdd": "Social-Media agregó. Perfil actualizado!",
"successDelete": "Social-Media borrado. Perfil actualizado!"
}
},
"admin": {
@ -53,7 +102,7 @@
"projects": "Proyectos",
"invites": "Invita",
"follows": "Sigue",
"shouts": "Gritos"
"shouts": "Recomendado"
},
"organizations": {
"name": "Organizaciones"
@ -105,6 +154,11 @@
}
},
"common": {
"your": {
"post": "Your Post ::: Your Posts",
"comment": "Your Comment ::: Your Comments",
"shout": "Your Shout ::: Your Shouts"
},
"post": "Mensaje ::: Mensajes",
"comment": "Comentario ::: Comentarios",
"letsTalk": "Hablemos",
@ -119,6 +173,113 @@
"tag": "Etiqueta ::: Etiquetas",
"name": "Nombre",
"loadMore": "cargar más",
"loading": "cargando"
"loading": "cargando",
"reportContent": "Report"
},
"actions": {
"loading": "cargamento",
"loadMore": "cargar más",
"create": "Crear",
"save": "Guardar",
"edit": "Edite",
"delete": "Delete",
"cancel": "Cancelar"
},
"moderation": {
"name": "Moderación",
"reports": {
"empty": "Felicitaciones, nada que moderar.",
"name": "Informes",
"submitter": "comunicado por",
"disabledBy": "desactivado por"
}
},
"disable": {
"submit": "Desactivar",
"cancel": "Cancelar",
"success": "Discapacitado con éxito",
"user": {
"title": "Desactivar usuario",
"type": "Usuario",
"message": "¿Realmente quieres deshabilitar el usuario \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Deshabilitar contribución",
"type": "Contribución",
"message": "¿Realmente quieres deshabilitar la contribución \"<b>{name}</b>\"?"
},
"comment": {
"title": "Desactivar comentario",
"type": "Comentario",
"message": "¿Realmente quieres deshabilitar el comentario de \"<b>{name}</b>\"?"
}
},
"delete": {
"submit": "Borrar",
"cancel": "Cancelar",
"contribution": {
"title": "Borrar contribución",
"type": "Contribución",
"message": "¿Realmente desea eliminar la Contribución \"<b>{name}</b>\" ?",
"success": "Contribución eliminada con éxito!"
},
"comment": {
"title": "Eliminar comentario",
"type": "Comentario",
"message": "¿Realmente quieres borrar el comentario de \"<b>{name}</b>\" ?",
"success": "Comentario eliminado con éxito!"
}
},
"report": {
"submit": "Informe",
"cancel": "Cancelar",
"success": "Gracias por informarnos!",
"user": {
"title": "Usuario de informe",
"type": "Usuario",
"message": "¿Realmente quieres reportar al usuario \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Informe Contribución",
"type": "Contribución",
"message": "¿Realmente quieres informar al usuario de la contribución \"<b>{name}</b>\"?"
},
"comment": {
"title": "Informe Comentario",
"type": "Comentario",
"message": "¿Realmente quieres reportar el comentario de \"<b>{name}</b>\"?"
}
},
"followButton": {
"follow": "Folgen",
"following": "Folge Ich"
},
"shoutButton": {
"shouted": "empfohlen"
},
"release": {
"submit": "Liberación",
"cancel": "Cancelar",
"success": "Liberar con éxito!",
"user": {
"title": "Usuario de la versión ",
"type": "Usuario",
"message": "¿Realmente quieres liberar al usuario \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Contribución de la versión ",
"type": "Contribución",
"message": "¿Realmente quieres liberar la contribución \"<b>{name}</b>\"?"
},
"comment": {
"title": "Comentario de la versión",
"type": "Comentario",
"message": "¿Realmente quieres liberar el comentario de \"<b>{name}</b>\"?"
}
},
"user": {
"avatar": {
"submitted": "Carga con éxito"
}
}
}
}

View File

@ -1,4 +1,7 @@
{
"filter-menu": {
"title": "Votre bulle de filtre"
},
"login": {
"copy": "Si vous avez déjà un compte human-connection, connectez-vous ici.",
"login": "Connexion",
@ -6,25 +9,66 @@
"email": "Votre courriel",
"password": "Votre mot de passe",
"moreInfo": "Qu'est-ce que Human Connection?",
"moreInfoURL": "https://human-connection.org/fr/",
"moreInfoHint": "à la page de présentation",
"hello": "Bonjour"
},
"editor": {
"placeholder": "Écrivez quelque chose d'inspirant..."
},
"profile": {
"name": "Mon profil",
"memberSince": "Membre depuis",
"follow": "Suivre",
"followers": "Suiveurs",
"following": "Suivant"
"following": "Suivant",
"shouted": "Recommandé",
"commented": "Comentado",
"userAnonym": "Anónimo",
"socialMedia": "Où d'autre puis-je trouver"
},
"notifications": {
"menu": {
"mentioned": "a parlé de vous dans un article"
}
},
"search": {
"placeholder": "Rechercher",
"hint": "Qu'est-ce que vous cherchez ?",
"failed": "Rien trouvé"
},
"settings": {
"name": "Paramètres",
"name": "Configurations",
"data": {
"name": "Vos données"
"name": "Vos données",
"labelName": "Votre nom",
"namePlaceholder": "Fémanon Funny",
"labelCity": "Votre ville ou région",
"labelBio": "À propos de vous",
"success": "Vos données ont été mises à jour avec succès !"
},
"security": {
"name": "Sécurité"
"name": "Sécurité",
"change-password": {
"button": "Modifier le mot de passe",
"success": "Mot de passe modifié avec succès !",
"label-old-password": "Votre ancien mot de passe",
"label-new-password": "Votre nouveau mot de passe",
"label-new-password-confirm": "Confirmez votre nouveau mot de passe",
"message-old-password-required": "Entrez votre ancien mot de passe",
"message-new-password-required": "Entrez un nouveau mot de passe",
"message-new-password-confirm-required": "Confirmez votre nouveau mot de passe",
"message-new-password-missmatch": "Tapez à nouveau le même mot de passe",
"passwordSecurity": "Sécurité par mot de passe",
"passwordStrength0": "Mot de passe très peu sûr",
"passwordStrength1": "Mot de passe non sécurisé",
"passwordStrength2": "Mot de passe médiocre",
"passwordStrength3": "Mot de passe fort",
"passwordStrength4": "Mot de passe très fort"
}
},
"invites": {
"name": "Invite"
"name": "invitations"
},
"download": {
"name": "Télécharger les données"
@ -36,7 +80,14 @@
"name": "Mes organisations"
},
"languages": {
"name": "Langues"
"name": "langues"
},
"social-media": {
"name": "Médias sociaux",
"placeholder": "Ajouter une URL pour les médias sociaux",
"submit": "Ajouter un lien",
"successAdd": "Les médias sociaux ont été ajoutés. Profil mis à jour !",
"successDelete": "Médias sociaux supprimé. Profil mis à jour !"
}
},
"admin": {
@ -51,7 +102,7 @@
"projects": "Projets",
"invites": "Invite",
"follows": "Suit",
"shouts": "Cris"
"shouts": "Recommandé"
},
"organizations": {
"name": "Organisations"
@ -95,13 +146,18 @@
}
},
"common": {
"your": {
"post": "Votre message ::: Votre messages",
"comment": "Votre Commentaire ::: Votre Commentaires ",
"shout": "Votre Recommandation ::: Votre Recommandations"
},
"post": "Message ::: Messages",
"comment": "Commentaire ::: Commentaires",
"letsTalk": "Parlons-en",
"versus": "Versus",
"moreInfo": "Plus d'infos",
"takeAction": "Passer à l'action",
"shout": "Shout ::: Shouts",
"shout": "Recommandation ::: Recommandations",
"user": "Utilisateur ::: Utilisateurs",
"category": "Catégorie ::: Catégories",
"organization": "Organisation ::: Organisations",
@ -112,33 +168,64 @@
"loading": "chargement",
"reportContent": "Signaler"
},
"actions": {
"loading": "chargement",
"loadMore": "charger plus",
"create": "Créer",
"save": "sauvegarde",
"edit": "Modifier",
"delete": "Supprimer",
"cancel": "Annuler"
},
"moderation": {
"name": "Modération",
"reports": {
"empty": "Félicitations, rien à modérer.",
"name": "Signalisations",
"reporter": "signalé par"
"name": "Rapports",
"submitter": "signalé par",
"disabledBy": "ddésactivé par"
}
},
"disable": {
"submit": "Désactiver",
"cancel": "Annuler",
"success": "Désactivé avec succès",
"user": {
"title": "Désactiver l'utilisateur",
"type": "Utilisateur",
"message": "Souhaitez-vous vraiment désactiver l'utilisateur \" <b> {name} </b> \"?"
"message": "Voulez-vous vraiment désactiver l'utilisateur \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Désactiver l'apport",
"type": "apport",
"message": "Souhaitez-vous vraiment signaler l'entrée\" <b> {name} </b> \"?"
"title": "Cotisation d'invalidité",
"type": "Contribution",
"message": "Voulez-vous vraiment désactiver la contribution \"<b> {name} </b> \"?"
},
"comment": {
"title": "Désactiver le commentaire",
"title": "Désactiver commentaire",
"type": "Commentaire",
"message": "Souhaitez-vous vraiment désactiver le commentaire de \"<b>{name}</b>\" ?"
"message": "Voulez-vous vraiment désactiver le commentaire de \"<b>{name}</b>\" ?"
}
},
"delete": {
"submit": "Supprimer",
"cancel": "Annuler",
"contribution": {
"title": "Supprimer la contribution",
"type": "Contribution",
"message": "Voulez-vous vraiment supprimer la contribution \"<b>{name}</b>\" löschen möchtest?",
"success": "Contribution supprimée avec succès !"
},
"comment": {
"title": "Supprimer un commentaire",
"type": "Commentaire",
"message": "Voulez-vous vraiment supprimer le commentaire de \"<b>{name}</b>\" löschen möchtest?",
"success": "Commentaire supprimé avec succès !"
}
},
"report": {
"submit": "Envoyer le rapport",
"submit": "Rapport",
"cancel": "Annuler",
"success": "Merci de nous avoir fait part de vos commentaires!",
"user": {
"title": "Signaler l'utilisateur",
"type": "Utilisateur",
@ -155,15 +242,36 @@
"message": "Souhaitez-vous vraiment signaler l'utilisateur \" <b> {name} </b> \"?"
}
},
"actions": {
"cancel": "Annuler"
"followButton": {
"follow": "découler",
"following": "Je suis les"
},
"contribution": {
"edit": "Rédiger l'apport",
"delete": "Supprimer l'entrée"
"shoutButton": {
"shouted": "recommandé"
},
"comment": {
"edit": "Rédiger un commentaire",
"delete": "Supprimer le commentaire"
"release": {
"submit": "Relâchez",
"cancel": "Annuler",
"success": "Relâchez avec succès!",
"user": {
"title": "Validation par l'utilisateur",
"type": "Utilisateur",
"message": "Voulez-vous vraiment libérer l'utilisateur \"<b>{name}</b>\"?"
},
"contribution": {
"title": "Versement de la contribution",
"type": "Contribution",
"message": "Voulez-vous vraiment débloquer la contribution \"<b>{name}</b>\"?"
},
"comment": {
"title": "Publication des commentaires",
"type": "Commentaire",
"message": "Voulez-vous vraiment publier le commentaire de \"<b>{name}</b>\"?"
}
},
"user": {
"avatar": {
"submitted": "Téléchargement réussi"
}
}
}

View File

@ -25,7 +25,18 @@ module.exports = {
env: {
// pages which do NOT require a login
publicPages: ['login', 'logout', 'register', 'signup', 'reset', 'reset-token', 'pages-slug'],
publicPages: [
'login',
'logout',
'password-reset-request',
'password-reset-verify-code',
'password-reset-change-password',
'register',
'signup',
'reset',
'reset-token',
'pages-slug',
],
// pages to keep alive
keepAlivePages: ['index'],
// active locales

View File

@ -29,6 +29,7 @@
"!**/?(*.)+(spec|test).js?(x)"
],
"coverageReporters": [
"text",
"lcov"
],
"transform": {
@ -56,10 +57,10 @@
"@nuxtjs/style-resources": "~0.1.2",
"accounting": "~0.4.1",
"apollo-cache-inmemory": "~1.5.1",
"apollo-client": "~2.6.2",
"apollo-client": "~2.6.3",
"cookie-universal-nuxt": "~2.0.16",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-alpha.33",
"date-fns": "2.0.0-alpha.35",
"express": "~4.17.1",
"graphql": "~14.3.1",
"jsonwebtoken": "~8.5.1",
@ -87,14 +88,14 @@
"@vue/server-test-utils": "~1.0.0-beta.29",
"@vue/test-utils": "~1.0.0-beta.29",
"babel-core": "~7.0.0-bridge.0",
"babel-eslint": "~10.0.1",
"babel-eslint": "~10.0.2",
"babel-jest": "~24.8.0",
"eslint": "~5.16.0",
"eslint-config-prettier": "~4.3.0",
"eslint-config-prettier": "~5.0.0",
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~2.1.2",
"eslint-plugin-import": "~2.17.3",
"eslint-plugin-jest": "~22.6.4",
"eslint-plugin-jest": "~22.7.0",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.1.1",
@ -110,4 +111,4 @@
"vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0"
}
}
}

View File

@ -45,6 +45,11 @@
name="password"
type="password"
/>
<ds-space class="password-reset-link" margin-bottom="large">
<nuxt-link to="/password-reset/request">
{{ $t('login.forgotPassword') }}
</nuxt-link>
</ds-space>
<ds-button
:loading="pending"
primary
@ -73,7 +78,7 @@
</template>
<script>
import LocaleSwitch from '~/components/LocaleSwitch'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
export default {
components: {

View File

@ -0,0 +1,22 @@
<template>
<ds-container width="small">
<ds-flex>
<ds-flex-item :width="{ base: '100%' }" centered>
<ds-space style="text-align: center;" margin-top="small" margin-bottom="xxx-small" centered>
<nuxt-child />
</ds-space>
</ds-flex-item>
</ds-flex>
</ds-container>
</template>
<script>
export default {
layout: 'default',
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
}
</script>

View File

@ -0,0 +1,28 @@
<template>
<change-password
:email="email"
:code="code"
@passwordResetResponse="handlePasswordResetResponse"
/>
</template>
<script>
import ChangePassword from '~/components/PasswordReset/ChangePassword'
export default {
data() {
const { email = '', code = '' } = this.$route.query
return { email, code }
},
components: {
ChangePassword,
},
methods: {
handlePasswordResetResponse(response) {
if (response === 'success') {
this.$router.push('/login')
}
},
},
}
</script>

View File

@ -0,0 +1,18 @@
<template>
<request @handleSubmitted="handlePasswordResetRequested" />
</template>
<script>
import Request from '~/components/PasswordReset/Request'
export default {
components: {
Request,
},
methods: {
handlePasswordResetRequested({ email }) {
this.$router.push({ path: 'verify-code', query: { email } })
},
},
}
</script>

View File

@ -0,0 +1,22 @@
<template>
<verify-code :email="email" @verification="handleVerification" />
</template>
<script>
import VerifyCode from '~/components/PasswordReset/VerifyCode'
export default {
components: {
VerifyCode,
},
data() {
const { email = '' } = this.$route.query
return { email }
},
methods: {
handleVerification({ email, code }) {
this.$router.push({ path: 'change-password', query: { email, code } })
},
},
}
</script>

View File

@ -77,6 +77,17 @@ export default {
]
},
},
watch: {
$route(to, from) {
if (to.hash === '#comments') {
window.scroll({
top: document.getElementById('comments').offsetTop,
left: 0,
behavior: 'smooth',
})
}
},
},
}
</script>

17
webapp/store/editor.js Normal file
View File

@ -0,0 +1,17 @@
export const state = () => {
return {
placeholder: null,
}
}
export const getters = {
placeholder(state) {
return state.placeholder
},
}
export const mutations = {
SET_PLACEHOLDER_TEXT(state, text) {
state.placeholder = text
},
}

View File

@ -0,0 +1,20 @@
import { getters, mutations } from './editor.js'
let state
describe('getters', () => {
describe('placeholder', () => {
it('return the value in state', () => {
state = { placeholder: null }
expect(getters.placeholder(state)).toBe(null)
})
})
})
describe('mutations', () => {
it('SET_PLACEHOLDER_TEXT', () => {
state = { placeholder: null }
mutations.SET_PLACEHOLDER_TEXT(state, 'new placeholder')
expect(getters.placeholder(state)).toBe('new placeholder')
})
})

View File

@ -1893,10 +1893,10 @@ apollo-cache@1.3.2, apollo-cache@^1.2.1:
apollo-utilities "^1.3.2"
tslib "^1.9.3"
apollo-client@^2.5.1, apollo-client@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.2.tgz#03b6af651e09b6e413e486ddc87464c85bd6e514"
integrity sha512-oks1MaT5x7gHcPeC8vPC1UzzsKaEIC0tye+jg72eMDt5OKc7BobStTeS/o2Ib3e0ii40nKxGBnMdl/Xa/p56Yg==
apollo-client@^2.5.1, apollo-client@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
dependencies:
"@types/zen-observable" "^0.8.0"
apollo-cache "1.3.2"
@ -2310,10 +2310,10 @@ babel-core@~7.0.0-bridge.0:
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
babel-eslint@~10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
integrity sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==
babel-eslint@~10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/parser" "^7.0.0"
@ -3754,10 +3754,10 @@ data-urls@^1.0.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@2.0.0-alpha.33:
version "2.0.0-alpha.33"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.33.tgz#c2f73c3cc50ac301c9217eb93603c9bc40e891bf"
integrity sha512-tqUVEk3oxnJuNIvwAMKHAMo4uFRG0zXvjxZQll+BonoPt+m4NMcUgO14NDxbHuy7uYcrVErd2GdSsw02EDZQ7w==
date-fns@2.0.0-alpha.35:
version "2.0.0-alpha.35"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.35.tgz#185813cdc51b05cc1468a95116494bb3f3440934"
integrity sha512-dAY1ujqRtyUsa9mVeupyMzUluWo1d7kAMwyXTQHFImKYSHKvxDw/dipiY6fdswQOs8CwpGoiKysGfaaRP5r3bA==
date-now@^0.1.4:
version "0.1.4"
@ -4236,10 +4236,10 @@ eslint-config-prettier@^3.3.0:
dependencies:
get-stdin "^6.0.0"
eslint-config-prettier@~4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz#c55c1fcac8ce4518aeb77906984e134d9eb5a4f0"
integrity sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA==
eslint-config-prettier@~5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-5.0.0.tgz#f7a94b2b8ae7cbf25842c36fa96c6d32cd0a697c"
integrity sha512-c17Aqiz5e8LEqoc/QPmYnaxQFAHTx2KlCZBPxXXjEMmNchOLnV/7j0HoPZuC+rL/tDC9bazUYOKJW9bOhftI/w==
dependencies:
get-stdin "^6.0.0"
@ -4300,10 +4300,10 @@ eslint-plugin-import@~2.17.3:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.6.4:
version "22.6.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz#2895b047dd82f90f43a58a25cf136220a21c9104"
integrity sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==
eslint-plugin-jest@~22.7.0:
version "22.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.7.0.tgz#a1d325bccb024b04f5354c56fe790baba54a454c"
integrity sha512-0U9nBd9V6+GKpM/KvRDcmMuPsewSsdM7NxCozgJkVAh8IrwHmQ0aw44/eYuVkhT8Fcdhsz0zYiyPtKg147eXMQ==
eslint-plugin-node@~9.1.0:
version "9.1.0"