Merge branch 'master' of github.com:Human-Connection/Human-Connection into 906-maintenace-mode

This commit is contained in:
Matt Rider 2019-07-11 11:54:13 -03:00
commit 7a3c432a0b
31 changed files with 965 additions and 235 deletions

View File

@ -68,7 +68,7 @@
"helmet": "~3.18.0",
"jsonwebtoken": "~8.5.1",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.13",
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.5.8",
"neo4j-driver": "~1.7.4",
"neo4j-graphql-js": "^2.6.3",
@ -88,7 +88,7 @@
"@babel/core": "~7.5.4",
"@babel/node": "~7.5.0",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.5.2",
"@babel/preset-env": "~7.5.4",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.6.8",
"babel-core": "~7.0.0-0",

View File

@ -0,0 +1,7 @@
enum Emotion {
surprised
cry
happy
angry
funny
}

View File

@ -0,0 +1,10 @@
type EMOTED @relation(name: "EMOTED") {
from: User
to: Post
emotion: Emotion
#createdAt: DateTime
#updatedAt: DateTime
createdAt: String
updatedAt: String
}

View File

@ -48,6 +48,8 @@ type Post {
RETURN COUNT(u) >= 1
"""
)
emotions: [EMOTED]
}
type Mutation {

View File

@ -73,6 +73,8 @@ type User {
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
emotions: [EMOTED]
}

View File

@ -320,10 +320,10 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread@^7.5.2":
version "7.5.2"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.2.tgz#ec92b0c6419074ea7af77c78b7c5d42041f2f5a9"
integrity sha512-C/JU3YOx5J4d9s0GGlJlYXVwsbd5JmqQ0AvB7cIDAx7nN57aDTnlJEsZJPuSskeBtMGFWSWU5Q+piTiDe0s7FQ==
"@babel/plugin-proposal-object-rest-spread@^7.5.4":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz#250de35d867ce8260a31b1fdac6c4fc1baa99331"
integrity sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
@ -649,17 +649,17 @@
core-js "^2.5.7"
regenerator-runtime "^0.12.0"
"@babel/preset-env@~7.5.2":
version "7.5.2"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.2.tgz#34a46f01aed617b174b8dbaf8fed9239300343d0"
integrity sha512-7rRJLaUqJhQ+8xGrWtMROAgOi/+udIzyK2ES9NHhDIUvR2zfx/ON5lRR8ACUGehIYst8KVbl4vpkgOqn08gBxA==
"@babel/preset-env@~7.5.4":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d"
integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
"@babel/plugin-proposal-dynamic-import" "^7.5.0"
"@babel/plugin-proposal-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread" "^7.5.2"
"@babel/plugin-proposal-object-rest-spread" "^7.5.4"
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
"@babel/plugin-syntax-async-generators" "^7.2.0"
@ -5382,9 +5382,9 @@ lodash.isstring@^4.0.1:
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.mergewith@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.once@^4.0.0:
version "4.1.1"
@ -5401,10 +5401,10 @@ lodash@=3.10.1:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.13:
version "4.17.13"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.14:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
long@^4.0.0:
version "4.0.0"

View File

@ -276,9 +276,9 @@ When("I fill the password form with:", table => {
table = table.rowsHash();
cy.get("input[id=oldPassword]")
.type(table["Your old password"])
.get("input[id=newPassword]")
.get("input[id=password]")
.type(table["Your new passsword"])
.get("input[id=confirmPassword]")
.get("input[id=passwordConfirmation]")
.type(table["Confirm new password"]);
});

View File

@ -0,0 +1 @@
MATCH (u:User)-[e:EMOTED]->(c:Post) DETACH DELETE e;

View File

@ -5,31 +5,54 @@
// [-] Omitted in Nitro
// [?] Unclear / has work to be done for Nitro
{
[ ] userId: {
[ ] type: String,
[ ] required: true,
[X] userId: {
[X] type: String,
[X] required: true,
[-] index: true
},
[ ] contributionId: {
[ ] type: String,
[ ] required: true,
[X] contributionId: {
[X] type: String,
[X] required: true,
[-] index: true
},
[ ] rated: {
[ ] type: String,
[?] rated: {
[X] type: String,
[ ] required: true,
[ ] enum: ['funny', 'happy', 'surprised', 'cry', 'angry']
[?] enum: ['funny', 'happy', 'surprised', 'cry', 'angry']
},
[ ] createdAt: {
[ ] type: Date,
[ ] default: Date.now
[X] createdAt: {
[X] type: Date,
[X] default: Date.now
},
[ ] updatedAt: {
[ ] type: Date,
[ ] default: Date.now
[X] updatedAt: {
[X] type: Date,
[X] default: Date.now
},
[ ] wasSeeded: { type: Boolean }
[-] wasSeeded: { type: Boolean }
}
*/
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion;
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as emotion
MATCH (u:User {id: emotion.userId}),
(c:Post {id: emotion.contributionId})
MERGE (u)-[e:EMOTED {
id: emotion._id["$oid"],
emotion: emotion.rated,
createdAt: datetime(emotion.createdAt.`$date`),
updatedAt: datetime(emotion.updatedAt.`$date`)
}]->(c)
RETURN e;
/*
// Queries
// user sets an emotion emotion:
// MERGE (u)-[e:EMOTED {id: ..., emotion: "funny", createdAt: ..., updatedAt: ...}]->(c)
// user removes emotion
// MATCH (u)-[e:EMOTED]->(c) DELETE e
// contribution distributions over every `emotion` property value for one post
// MATCH (u:User)-[e:EMOTED]->(c:Post {id: "5a70bbc8508f5b000b443b1a"}) RETURN e.emotion,COUNT(e.emotion)
// contribution distributions over every `emotion` property value for one user (advanced - "whats the primary emotion used by the user?")
// MATCH (u:User{id:"5a663b1ac64291000bf302a1"})-[e:EMOTED]->(c:Post) RETURN e.emotion,COUNT(e.emotion)
// contribution distributions over every `emotion` property value for all posts created by one user (advanced - "how do others react to my contributions?")
// MATCH (u:User)-[e:EMOTED]->(c:Post)<-[w:WROTE]-(a:User{id:"5a663b1ac64291000bf302a1"}) RETURN e.emotion,COUNT(e.emotion)
// if we can filter the above an a variable timescale that would be great (should be possible on createdAt and updatedAt fields)
*/

View File

@ -1 +1 @@
// this is just a relation between users(?) - no need to delete
MATCH (u1:User)-[f:FOLLOWS]->(u2:User) DETACH DELETE f;

View File

@ -60,8 +60,8 @@ delete_collection "contributions" "contributions_post"
delete_collection "contributions" "contributions_cando"
delete_collection "shouts" "shouts"
delete_collection "comments" "comments"
delete_collection "emotions" "emotions"
#delete_collection "emotions"
#delete_collection "invites"
#delete_collection "notifications"
#delete_collection "organizations"
@ -82,12 +82,12 @@ import_collection "users" "users/users.cql"
import_collection "follows_users" "follows/follows.cql"
#import_collection "follows_organizations" "follows/follows.cql"
import_collection "contributions_post" "contributions/contributions.cql"
import_collection "contributions_cando" "contributions/contributions.cql"
#import_collection "contributions_cando" "contributions/contributions.cql"
#import_collection "contributions_DELETED" "contributions/contributions.cql"
import_collection "shouts" "shouts/shouts.cql"
import_collection "comments" "comments/comments.cql"
import_collection "emotions" "emotions/emotions.cql"
# import_collection "emotions"
# import_collection "invites"
# import_collection "notifications"
# import_collection "organizations"

View File

@ -54,7 +54,7 @@ describe('ChangePassword.vue', () => {
describe('match', () => {
beforeEach(() => {
wrapper.find('input#oldPassword').setValue('some secret')
wrapper.find('input#newPassword').setValue('some secret')
wrapper.find('input#password').setValue('some secret')
})
it('invalid', () => {
@ -90,8 +90,8 @@ describe('ChangePassword.vue', () => {
describe('given valid input', () => {
beforeEach(() => {
wrapper.find('input#oldPassword').setValue('supersecret')
wrapper.find('input#newPassword').setValue('superdupersecret')
wrapper.find('input#confirmPassword').setValue('superdupersecret')
wrapper.find('input#password').setValue('superdupersecret')
wrapper.find('input#passwordConfirmation').setValue('superdupersecret')
})
describe('submit form', () => {
@ -109,8 +109,8 @@ describe('ChangePassword.vue', () => {
expect.objectContaining({
variables: {
oldPassword: 'supersecret',
newPassword: 'superdupersecret',
confirmPassword: 'superdupersecret',
password: 'superdupersecret',
passwordConfirmation: 'superdupersecret',
},
}),
)
@ -135,8 +135,8 @@ describe('ChangePassword.vue', () => {
/* describe('mutation rejects', () => {
beforeEach(async () => {
await wrapper.find('input#oldPassword').setValue('supersecret')
await wrapper.find('input#newPassword').setValue('supersecret')
await wrapper.find('input#confirmPassword').setValue('supersecret')
await wrapper.find('input#password').setValue('supersecret')
await wrapper.find('input#passwordConfirmation').setValue('supersecret')
})
it('displays error message', async () => {

View File

@ -1,12 +1,6 @@
<template>
<ds-form
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
@input="handleInput"
@input-valid="handleInputValid"
>
<template>
<ds-form v-model="formData" :schema="formSchema" @submit="handleSubmit">
<template slot-scope="{ errors }">
<ds-input
id="oldPassword"
model="oldPassword"
@ -15,22 +9,22 @@
:label="$t('settings.security.change-password.label-old-password')"
/>
<ds-input
id="newPassword"
model="newPassword"
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.newPassword" />
<password-strength :password="formData.password" />
<ds-space margin-top="base">
<ds-button :loading="loading" :disabled="disabled" primary>
<ds-button :loading="loading" :disabled="errors" primary>
{{ $t('settings.security.change-password.button') }}
</ds-button>
</ds-space>
@ -41,6 +35,7 @@
<script>
import gql from 'graphql-tag'
import PasswordStrength from './Strength'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export default {
name: 'ChangePassword',
@ -48,11 +43,11 @@ export default {
PasswordStrength,
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
formData: {
oldPassword: '',
newPassword: '',
confirmPassword: '',
...passwordForm.formData,
},
formSchema: {
oldPassword: {
@ -60,38 +55,18 @@ export default {
required: true,
message: this.$t('settings.security.change-password.message-old-password-required'),
},
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',
),
},
],
...passwordForm.formSchema,
},
loading: false,
disabled: true,
}
},
methods: {
async handleInput(data) {
this.disabled = true
},
async handleInputValid(data) {
this.disabled = false
},
async handleSubmit(data) {
this.loading = true
const mutation = gql`
mutation($oldPassword: String!, $newPassword: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
mutation($oldPassword: String!, $password: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $password)
}
`
const variables = this.formData
@ -102,8 +77,8 @@ export default {
this.$toast.success(this.$t('settings.security.change-password.success'))
this.formData = {
oldPassword: '',
newPassword: '',
confirmPassword: '',
password: '',
passwordConfirmation: '',
}
} catch (err) {
this.$toast.error(err.message)
@ -111,15 +86,6 @@ export default {
this.loading = false
}
},
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

@ -47,8 +47,8 @@ describe('ChangePassword ', () => {
describe('submitting new password', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('input#newPassword').setValue('supersecret')
wrapper.find('input#confirmPassword').setValue('supersecret')
wrapper.find('input#password').setValue('supersecret')
wrapper.find('input#passwordConfirmation').setValue('supersecret')
wrapper.find('form').trigger('submit')
})
@ -58,7 +58,7 @@ describe('ChangePassword ', () => {
it('delivers new password to backend', () => {
const expected = expect.objectContaining({
variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' },
variables: { code: '123456', email: 'mail@example.org', password: 'supersecret' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})

View File

@ -1,54 +1,52 @@
<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-form
v-if="!changePasswordResult"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitPassword"
class="change-password"
>
<template slot-scope="{ errors }">
<ds-input
id="newPassword"
model="newPassword"
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="confirmPassword"
model="confirmPassword"
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.newPassword" />
<password-strength :password="formData.password" />
<ds-space margin-top="base">
<ds-button :loading="$apollo.loading" :disabled="disabled" primary>
<ds-button :loading="$apollo.loading" :disabled="errors" 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>
</template>
</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>
</ds-space>
</ds-card>
</template>
@ -57,6 +55,7 @@
import PasswordStrength from '../Password/Strength'
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export default {
components: {
@ -68,48 +67,28 @@ export default {
code: { type: String, required: true },
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
formData: {
newPassword: '',
confirmPassword: '',
...passwordForm.formData,
},
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',
),
},
],
...passwordForm.formSchema,
},
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)
mutation($code: String!, $email: String!, $password: String!) {
resetPassword(code: $code, email: $email, newPassword: $password)
}
`
const { newPassword } = this.formData
const { password } = this.formData
const { email, code } = this
const variables = { newPassword, email, code }
const variables = { password, email, code }
try {
const {
data: { resetPassword },
@ -119,22 +98,13 @@ export default {
this.$emit('passwordResetResponse', this.changePasswordResult)
}, 3000)
this.formData = {
newPassword: '',
confirmPassword: '',
password: '',
passwordConfirmation: '',
}
} 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,132 @@
import { mount, createLocalVue } from '@vue/test-utils'
import CreateUserAccount, { SignupVerificationMutation } from './CreateUserAccount'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('CreateUserAccount', () => {
let wrapper
let Wrapper
let mocks
let propsData
beforeEach(() => {
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn(),
},
}
propsData = {}
})
describe('mount', () => {
Wrapper = () => {
return mount(CreateUserAccount, {
mocks,
propsData,
localVue,
})
}
describe('given email and nonce', () => {
beforeEach(() => {
propsData.nonce = '666777'
propsData.email = 'sixseven@example.org'
})
it('renders a form to create a new user', () => {
wrapper = Wrapper()
expect(wrapper.find('.create-user-account').exists()).toBe(true)
})
describe('submit', () => {
let action
beforeEach(() => {
action = async () => {
wrapper = Wrapper()
wrapper.find('input#name').setValue('John Doe')
wrapper.find('input#password').setValue('hellopassword')
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
it('calls CreateUserAccount graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers data to backend', async () => {
await action()
const expected = expect.objectContaining({
variables: {
about: '',
name: 'John Doe',
email: 'sixseven@example.org',
nonce: '666777',
password: 'hellopassword',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case mutation resolves', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockResolvedValue({
data: {
SignupVerification: {
id: 'u1',
name: 'John Doe',
slug: 'john-doe',
},
},
})
})
it('displays success', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith('registration.create-user-account.success')
})
describe('after timeout', () => {
beforeEach(jest.useFakeTimers)
it('emits `userCreated` with { password, email }', async () => {
await action()
jest.runAllTimers()
expect(wrapper.emitted('userCreated')).toEqual([
[
{
email: 'sixseven@example.org',
password: 'hellopassword',
},
],
])
})
})
})
describe('in case mutation rejects', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockRejectedValue(new Error('Invalid nonce'))
})
it('displays form errors', async () => {
await action()
expect(wrapper.find('.backendErrors').text()).toContain('Invalid nonce')
})
})
})
})
})
})

View File

@ -0,0 +1,142 @@
<template>
<ds-card v-if="success" class="success">
<ds-space>
<sweetalert-icon icon="success" />
<ds-text align="center" bold color="success">
{{ $t('registration.create-user-account.success') }}
</ds-text>
</ds-space>
</ds-card>
<ds-form
v-else
class="create-user-account"
v-model="formData"
:schema="formSchema"
@submit="submit"
>
<template slot-scope="{ errors }">
<ds-card :header="$t('registration.create-user-account.title')">
<ds-input
id="name"
model="name"
icon="user"
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.namePlaceholder')"
/>
<ds-input
id="bio"
model="about"
type="textarea"
rows="3"
:label="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')"
/>
<ds-input
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.password" />
<template slot="footer">
<ds-space class="backendErrors" v-if="backendErrors">
<ds-text align="center" bold color="danger">
{{ backendErrors.message }}
</ds-text>
</ds-space>
<ds-button
style="float: right;"
icon="check"
type="submit"
:loading="$apollo.loading"
:disabled="errors"
primary
>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export const SignupVerificationMutation = gql`
mutation($nonce: String!, $name: String!, $email: String!, $password: String!) {
SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) {
id
name
slug
}
}
`
export default {
components: {
PasswordStrength,
SweetalertIcon,
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
formData: {
name: '',
about: '',
...passwordForm.formData,
},
formSchema: {
name: {
type: 'string',
required: true,
min: 3,
},
about: {
type: 'string',
required: false,
},
...passwordForm.formSchema,
},
disabled: true,
success: null,
backendErrors: null,
}
},
props: {
nonce: { type: String, required: true },
email: { type: String, required: true },
},
methods: {
async submit() {
const { name, password, about } = this.formData
const { email, nonce } = this
try {
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
variables: { name, password, about, email, nonce },
})
this.success = true
setTimeout(() => {
this.$emit('userCreated', {
email,
password,
})
}, 3000)
} catch (err) {
this.backendErrors = err
}
},
},
}
</script>

View File

@ -0,0 +1,146 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Signup, { SignupMutation, SignupByInvitationMutation } from './Signup'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('Signup', () => {
let wrapper
let Wrapper
let mocks
let propsData
beforeEach(() => {
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn().mockResolvedValue({ data: { Signup: { email: 'mail@example.org' } } }),
},
}
propsData = {}
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
return mount(Signup, {
mocks,
propsData,
localVue,
})
}
describe('without invitation code', () => {
it('renders signup form', () => {
wrapper = Wrapper()
expect(wrapper.find('.signup').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 Signup graphql mutation', () => {
const expected = expect.objectContaining({ mutation: SignupMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers email to backend', () => {
const expected = expect.objectContaining({
variables: { email: 'mail@example.org', token: null },
})
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 mail for email verification was sent', () => {
const expected = ['registration.signup.form.success', { 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' }]])
})
})
})
})
describe('with invitation code', () => {
let action
beforeEach(() => {
propsData.token = '666777'
action = async () => {
wrapper = Wrapper()
wrapper.find('input#email').setValue('mail@example.org')
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
describe('submit', () => {
it('calls SignupByInvitation graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupByInvitationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers invitation token to backend', async () => {
await action()
const expected = expect.objectContaining({
variables: { email: 'mail@example.org', token: '666777' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case a user account with the email already exists', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: User account with this email already exists.'),
)
})
it('explains the error', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith('registration.signup.form.errors.email-exists')
})
})
describe('in case the invitation code was incorrect', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: Invitation code already used or does not exist.'),
)
})
it('explains the error', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith(
'registration.signup.form.errors.invalid-invitation-token',
)
})
})
})
})
})
})

View File

@ -0,0 +1,141 @@
<template>
<ds-card class="signup">
<ds-space margin="large">
<ds-form
v-if="!success && !error"
@input="handleInput"
@input-valid="handleInputValid"
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
>
<h1>{{ $t('registration.signup.title') }}</h1>
<ds-space v-if="token" margin-botton="large">
<ds-text v-html="$t('registration.signup.form.invitation-code', { code: token })" />
</ds-space>
<ds-space margin-botton="large">
<ds-text>
{{ $t('registration.signup.form.description') }}
</ds-text>
</ds-space>
<ds-input
:placeholder="$t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<ds-button
:disabled="disabled"
:loading="$apollo.loading"
primary
fullwidth
name="submit"
type="submit"
icon="envelope"
>
{{ $t('registration.signup.form.submit') }}
</ds-button>
</ds-form>
<div v-else>
<template v-if="!error">
<sweetalert-icon icon="info" />
<ds-text align="center" v-html="submitMessage" />
</template>
<template v-else>
<sweetalert-icon icon="error" />
<ds-text align="center">
{{ error.message }}
</ds-text>
</template>
</div>
</ds-space>
</ds-card>
</template>
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export const SignupMutation = gql`
mutation($email: String!) {
Signup(email: $email) {
email
}
}
`
export const SignupByInvitationMutation = gql`
mutation($email: String!, $token: String!) {
SignupByInvitation(email: $email, token: $token) {
email
}
}
`
export default {
components: {
SweetalertIcon,
},
props: {
token: { type: String, default: null },
},
data() {
return {
formData: {
email: '',
},
formSchema: {
email: {
type: 'email',
required: true,
message: this.$t('common.validations.email'),
},
},
disabled: true,
success: false,
error: null,
}
},
computed: {
submitMessage() {
const { email } = this.formData
return this.$t('registration.signup.form.success', { email })
},
},
methods: {
handleInput() {
this.disabled = true
},
handleInputValid() {
this.disabled = false
},
async handleSubmit() {
const mutation = this.token ? SignupByInvitationMutation : SignupMutation
const { email } = this.formData
const { token } = this
try {
await this.$apollo.mutate({ mutation, variables: { email, token } })
this.success = true
setTimeout(() => {
this.$emit('handleSubmitted', { email })
}, 3000)
} catch (err) {
const { message } = err
const mapping = {
'User account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token',
}
for (const [pattern, key] of Object.entries(mapping)) {
if (message.includes(pattern))
this.error = { key, message: this.$t(`registration.signup.form.errors.${key}`) }
}
if (!this.error) {
this.$toast.error(message)
}
}
},
},
}
</script>

View File

@ -0,0 +1,36 @@
export default function PasswordForm({ translate }) {
const passwordMismatchMessage = translate(
'settings.security.change-password.message-new-password-missmatch',
)
return {
formData: {
password: '',
passwordConfirmation: '',
},
formSchema: {
password: {
type: 'string',
required: true,
message: translate('settings.security.change-password.message-new-password-required'),
},
passwordConfirmation: [
{
validator(rule, value, callback, source, options) {
var errors = []
if (source.password !== value) {
errors.push(new Error(passwordMismatchMessage))
}
callback(errors)
},
},
{
type: 'string',
required: true,
message: translate(
'settings.security.change-password.message-new-password-confirm-required',
),
},
],
},
}
}

View File

@ -87,7 +87,7 @@
</ds-container>
</div>
<ds-container style="word-break: break-all">
<div style="padding: 6rem 2rem 5rem;" :width="{ base: '100%', md: '96%' }">
<div class="main-container" :width="{ base: '100%', md: '96%' }">
<nuxt />
</div>
</ds-container>
@ -226,6 +226,11 @@ export default {
display: inline-flex;
}
.main-container {
padding-top: 6rem;
padding-botton: 5rem;
}
.main-navigation {
a {
color: $text-color-soft;

View File

@ -29,7 +29,8 @@
"moreInfo": "Was ist Human Connection?",
"moreInfoURL": "https://human-connection.org",
"moreInfoHint": "zur Präsentationsseite",
"hello": "Hallo"
"hello": "Hallo",
"success": "Du bist eingeloggt!"
},
"password-reset": {
"title": "Passwort zurücksetzen",
@ -39,6 +40,24 @@
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
}
},
"registration": {
"signup": {
"title": "Mach mit bei Human Connection!",
"form": {
"description": "Um loszulegen, gib deine E-Mail Adresse ein:",
"errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
},
"submit": "Konto erstellen",
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
}
},
"create-user-account": {
"title": "Benutzerkonto anlegen",
"success": "Dein Benutzerkonto wurde erstellt!"
}
},
"verify-code": {
"form": {
"code": "Code eingeben",
@ -189,6 +208,11 @@
},
"settings": {
"name": "Einstellungen"
},
"invites": {
"name": "Benutzer einladen",
"title": "Benutzer als Admin anmelden",
"description": "Dieses Anmeldeformular ist zu sehen sobald die Anmeldung öffentlich zugänglich ist."
}
},
"post": {

View File

@ -29,7 +29,8 @@
"moreInfo": "What is Human Connection?",
"moreInfoURL": "https://human-connection.org/en/",
"moreInfoHint": "to the presentation page",
"hello": "Hello"
"hello": "Hello",
"success": "You are logged in!"
},
"password-reset": {
"title": "Reset your password",
@ -39,6 +40,25 @@
"submitted": "A mail with further instruction has been sent to <b>{email}</b>"
}
},
"registration": {
"signup": {
"title": "Join Human Connection!",
"form": {
"description": "To get started, enter your email address:",
"invitation-code": "Your invitation code is: <b>{code}</b>",
"errors": {
"email-exists": "There is already a user account with this email address!",
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
},
"submit": "Create an account",
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
}
},
"create-user-account": {
"title": "Create user account",
"success": "Your account has been created!"
}
},
"verify-code": {
"form": {
"code": "Enter your code",
@ -189,6 +209,11 @@
},
"settings": {
"name": "Settings"
},
"invites": {
"name": "Invite users",
"title": "Signup users as admin",
"description": "This registration form will be visible as soon as the registration is open to the public."
}
},
"post": {

View File

@ -31,10 +31,10 @@ module.exports = {
'password-reset-request',
'password-reset-verify-code',
'password-reset-change-password',
'register',
'signup',
'reset',
'reset-token',
// 'registration-signup', TODO: uncomment to open public registration
'registration-signup-by-invitation-code',
'registration-verify-code',
'registration-create-user-account',
'pages-slug',
],
// pages to keep alive

View File

@ -82,7 +82,7 @@
"devDependencies": {
"@babel/core": "~7.5.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "~7.4.5",
"@babel/preset-env": "~7.5.4",
"@vue/cli-shared-utils": "~3.9.0",
"@vue/eslint-config-prettier": "~4.0.1",
"@vue/server-test-utils": "~1.0.0-beta.29",

View File

@ -52,6 +52,10 @@ export default {
name: this.$t('admin.tags.name'),
path: `/admin/tags`,
},
{
name: this.$t('admin.invites.name'),
path: `/admin/invite`,
},
// TODO implement
/* {
name: this.$t('admin.settings.name'),

View File

@ -0,0 +1,23 @@
<template>
<ds-section>
<ds-space>
<ds-heading size="h3">
{{ $t('admin.invites.title') }}
</ds-heading>
<ds-text>
{{ $t('admin.invites.description') }}
</ds-text>
</ds-space>
<signup />
</ds-section>
</template>
<script>
import Signup from '~/components/Registration/Signup'
export default {
components: {
Signup,
},
}
</script>

View File

@ -115,7 +115,7 @@ export default {
async onSubmit() {
try {
await this.$store.dispatch('auth/login', { ...this.form })
this.$toast.success('You are logged in!')
this.$toast.success(this.$t('login.success'))
this.$router.replace(this.$route.query.path || '/')
} catch (err) {
this.$toast.error(err.message)

View File

@ -0,0 +1,14 @@
<template>
<nuxt-child />
</template>
<script>
export default {
layout: 'default',
asyncData({ store, redirect }) {
if (store.getters['auth/isLoggedIn']) {
redirect('/')
}
},
}
</script>

View File

@ -0,0 +1,27 @@
<template>
<create-user-account @userCreated="handleUserCreated" :email="email" :nonce="nonce" />
</template>
<script>
import CreateUserAccount from '~/components/Registration/CreateUserAccount'
export default {
data() {
const { nonce = '', email = '' } = this.$route.query
return { nonce, email }
},
components: {
CreateUserAccount,
},
methods: {
async handleUserCreated({ email, password }) {
try {
await this.$store.dispatch('auth/login', { email, password })
this.$toast.success('You are logged in!')
this.$router.push('/')
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -268,6 +268,14 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-decorators" "^7.2.0"
"@babel/plugin-proposal-dynamic-import@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506"
integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
"@babel/plugin-proposal-json-strings@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
@ -276,10 +284,10 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.4.4.tgz#1ef173fcf24b3e2df92a678f027673b55e7e3005"
integrity sha512-dMBG6cSPBbHeEBdFXeQ2QLc5gUpg4Vkaz8octD4aoW/ISO+jBOcsuxYL7bsb5WSu8RLP6boxrBIALEHgoHtO9g==
"@babel/plugin-proposal-object-rest-spread@^7.5.4":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz#250de35d867ce8260a31b1fdac6c4fc1baa99331"
integrity sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
@ -357,10 +365,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-async-to-generator@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz#a3f1d01f2f21cadab20b33a82133116f14fb5894"
integrity sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==
"@babel/plugin-transform-async-to-generator@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e"
integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
@ -402,10 +410,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-destructuring@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.4.4.tgz#9d964717829cc9e4b601fc82a26a71a4d8faf20f"
integrity sha512-/aOx+nW0w8eHiEHm+BTERB2oJn5D127iye/SUQl7NjHy0lf+j7h4MKMMSOwdazGq9OxgiNADncE+SRJkCxjZpQ==
"@babel/plugin-transform-destructuring@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a"
integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
@ -418,10 +426,10 @@
"@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4"
"@babel/plugin-transform-duplicate-keys@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3"
integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==
"@babel/plugin-transform-duplicate-keys@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853"
integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
@ -462,30 +470,33 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-transform-modules-amd@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6"
integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==
"@babel/plugin-transform-modules-amd@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91"
integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==
dependencies:
"@babel/helper-module-transforms" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0"
babel-plugin-dynamic-import-node "^2.3.0"
"@babel/plugin-transform-modules-commonjs@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.4.4.tgz#0bef4713d30f1d78c2e59b3d6db40e60192cac1e"
integrity sha512-4sfBOJt58sEo9a2BQXnZq+Q3ZTSAUXyK3E30o36BOGnJ+tvJ6YSxF0PG6kERvbeISgProodWuI9UVG3/FMY6iw==
"@babel/plugin-transform-modules-commonjs@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74"
integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==
dependencies:
"@babel/helper-module-transforms" "^7.4.4"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-simple-access" "^7.1.0"
babel-plugin-dynamic-import-node "^2.3.0"
"@babel/plugin-transform-modules-systemjs@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.4.4.tgz#dc83c5665b07d6c2a7b224c00ac63659ea36a405"
integrity sha512-MSiModfILQc3/oqnG7NrP1jHaSPryO6tA2kOMmAQApz5dayPxWiHqmq4sWH2xF5LcQK56LlbKByCd8Aah/OIkQ==
"@babel/plugin-transform-modules-systemjs@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249"
integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==
dependencies:
"@babel/helper-hoist-variables" "^7.4.4"
"@babel/helper-plugin-utils" "^7.0.0"
babel-plugin-dynamic-import-node "^2.3.0"
"@babel/plugin-transform-modules-umd@^7.2.0":
version "7.2.0"
@ -603,39 +614,41 @@
"@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4"
"@babel/preset-env@^7.4.5", "@babel/preset-env@~7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
"@babel/preset-env@^7.4.5", "@babel/preset-env@~7.5.4":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d"
integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-async-generator-functions" "^7.2.0"
"@babel/plugin-proposal-dynamic-import" "^7.5.0"
"@babel/plugin-proposal-json-strings" "^7.2.0"
"@babel/plugin-proposal-object-rest-spread" "^7.4.4"
"@babel/plugin-proposal-object-rest-spread" "^7.5.4"
"@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
"@babel/plugin-syntax-async-generators" "^7.2.0"
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
"@babel/plugin-syntax-object-rest-spread" "^7.2.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
"@babel/plugin-transform-arrow-functions" "^7.2.0"
"@babel/plugin-transform-async-to-generator" "^7.4.4"
"@babel/plugin-transform-async-to-generator" "^7.5.0"
"@babel/plugin-transform-block-scoped-functions" "^7.2.0"
"@babel/plugin-transform-block-scoping" "^7.4.4"
"@babel/plugin-transform-classes" "^7.4.4"
"@babel/plugin-transform-computed-properties" "^7.2.0"
"@babel/plugin-transform-destructuring" "^7.4.4"
"@babel/plugin-transform-destructuring" "^7.5.0"
"@babel/plugin-transform-dotall-regex" "^7.4.4"
"@babel/plugin-transform-duplicate-keys" "^7.2.0"
"@babel/plugin-transform-duplicate-keys" "^7.5.0"
"@babel/plugin-transform-exponentiation-operator" "^7.2.0"
"@babel/plugin-transform-for-of" "^7.4.4"
"@babel/plugin-transform-function-name" "^7.4.4"
"@babel/plugin-transform-literals" "^7.2.0"
"@babel/plugin-transform-member-expression-literals" "^7.2.0"
"@babel/plugin-transform-modules-amd" "^7.2.0"
"@babel/plugin-transform-modules-commonjs" "^7.4.4"
"@babel/plugin-transform-modules-systemjs" "^7.4.4"
"@babel/plugin-transform-modules-amd" "^7.5.0"
"@babel/plugin-transform-modules-commonjs" "^7.5.0"
"@babel/plugin-transform-modules-systemjs" "^7.5.0"
"@babel/plugin-transform-modules-umd" "^7.2.0"
"@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5"
"@babel/plugin-transform-new-target" "^7.4.4"
@ -650,7 +663,7 @@
"@babel/plugin-transform-template-literals" "^7.4.4"
"@babel/plugin-transform-typeof-symbol" "^7.2.0"
"@babel/plugin-transform-unicode-regex" "^7.4.4"
"@babel/types" "^7.4.4"
"@babel/types" "^7.5.0"
browserslist "^4.6.0"
core-js-compat "^3.1.1"
invariant "^2.2.2"
@ -2352,6 +2365,13 @@ babel-messages@^6.23.0:
dependencies:
babel-runtime "^6.22.0"
babel-plugin-dynamic-import-node@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
dependencies:
object.assign "^4.1.0"
babel-plugin-istanbul@^5.1.0:
version "5.1.4"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.4.tgz#841d16b9a58eeb407a0ddce622ba02fe87a752ba"
@ -6843,7 +6863,7 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash._reinterpolate@~3.0.0:
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
@ -6904,19 +6924,19 @@ lodash.tail@^4.1.1:
integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=
lodash.template@^4.2.4, lodash.template@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
dependencies:
lodash._reinterpolate "~3.0.0"
lodash._reinterpolate "^3.0.0"
lodash.templatesettings "^4.0.0"
lodash.templatesettings@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
dependencies:
lodash._reinterpolate "~3.0.0"
lodash._reinterpolate "^3.0.0"
lodash.uniq@^4.5.0:
version "4.5.0"
@ -6929,9 +6949,9 @@ lodash.uniqueid@^4.0.1:
integrity sha1-MmjyanyI5PSxdY1nknGBTjH6WyY=
lodash@4.x, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
log-symbols@^2.2.0:
version "2.2.0"
@ -7710,7 +7730,7 @@ object-hash@^1.1.4:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
object-keys@^1.0.12:
object-keys@^1.0.11, object-keys@^1.0.12:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@ -7727,6 +7747,16 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
object.assign@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
dependencies:
define-properties "^1.1.2"
function-bind "^1.1.1"
has-symbols "^1.0.0"
object-keys "^1.0.11"
object.getownpropertydescriptors@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"