Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 734-authorization-problem-disabling-post

# Conflicts:
#	webapp/components/ContributionForm/index.vue
#	webapp/graphql/PostMutations.js
This commit is contained in:
Wolfgang Huß 2019-06-14 19:02:33 +02:00
commit 68dcbacaff
10 changed files with 248 additions and 27 deletions

View File

@ -74,6 +74,22 @@ describe('CreatePost', () => {
await expect(client.request(mutation)).resolves.toMatchObject(expected)
})
})
describe('language', () => {
it('allows a user to set the language of the post', async () => {
const createPostWithLanguageMutation = `
mutation {
CreatePost(title: "I am a title", content: "Some content", language: "en") {
language
}
}
`
const expected = { CreatePost: { language: 'en' } }
await expect(client.request(createPostWithLanguageMutation)).resolves.toEqual(
expect.objectContaining(expected),
)
})
})
})
})

View File

@ -15,8 +15,9 @@ type Post {
disabledBy: User @relation(name: "DISABLED", direction: "IN")
createdAt: String
updatedAt: String
relatedContributions: [Post]! @cypher(
language: String
relatedContributions: [Post]!
@cypher(
statement: """
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
RETURN DISTINCT post
@ -28,13 +29,20 @@ type Post {
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
commentsCount: Int!
@cypher(
statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)"
)
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
shoutedCount: Int!
@cypher(
statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)"
)
# Has the currently logged in user shouted that post?
shoutedByCurrentUser: Boolean! @cypher(
shoutedByCurrentUser: Boolean!
@cypher(
statement: """
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1

View File

@ -0,0 +1,142 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import ContributionForm from './index.vue'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
config.stubs['no-ssr'] = '<span><slot /></span>'
describe('ContributionForm.vue', () => {
let wrapper
let postTitleInput
let expectedParams
let deutschOption
let cancelBtn
let mocks
const postTitle = 'this is a title for a post'
const postContent = 'this is a post'
const computed = { locale: () => 'English' }
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
CreatePost: {
title: postTitle,
slug: 'this-is-a-title-for-a-post',
content: postContent,
contentExcerpt: postContent,
language: 'en',
},
},
})
.mockRejectedValue({ message: 'Not Authorised!' }),
},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
$i18n: {
locale: () => 'en',
},
$router: {
back: jest.fn(),
push: jest.fn(),
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(ContributionForm, { mocks, localVue, computed })
}
beforeEach(() => {
wrapper = Wrapper()
wrapper.setData({ form: { languageOptions: [{ label: 'Deutsch', value: 'de' }] } })
})
describe('CreatePost', () => {
describe('invalid form submission', () => {
it('title required for form submission', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue('this is a title for a post')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content required for form submission', async () => {
wrapper.vm.updateEditorContent('this is a post')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
})
describe('valid form submission', () => {
expectedParams = {
variables: { title: postTitle, content: postContent, language: 'en', id: null },
}
beforeEach(async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue('this is a title for a post')
wrapper.vm.updateEditorContent('this is a post')
await wrapper.find('form').trigger('submit')
})
it('with title and content', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it("sends a fallback language based on a user's locale", () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('supports changing the language', async () => {
expectedParams.variables.language = 'de'
deutschOption = wrapper.findAll('li').at(0)
deutschOption.trigger('click')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it("pushes the user to the post's page", async () => {
expect(mocks.$router.push).toHaveBeenCalledTimes(1)
})
it('shows a success toaster', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
})
describe('cancel', () => {
it('calls $router.back() when cancel button clicked', () => {
cancelBtn = wrapper.find('.cancel-button')
cancelBtn.trigger('click')
expect(mocks.$router.back).toHaveBeenCalledTimes(1)
})
})
describe('handles errors', () => {
beforeEach(async () => {
wrapper = Wrapper()
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue('this is a title for a post')
wrapper.vm.updateEditorContent('this is a post')
// second submission causes mutation to reject
await wrapper.find('form').trigger('submit')
})
it('shows an error toaster when apollo mutation rejects', async () => {
await wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
})
})
})
})
})

View File

@ -6,8 +6,27 @@
<no-ssr>
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
</no-ssr>
<ds-space margin-bottom="xxx-large" />
<ds-flex class="contribution-form-footer">
<ds-flex-item :width="{ base: '10%', sm: '10%', md: '10%', lg: '15%' }" />
<ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }">
<ds-space margin-bottom="small" />
<ds-select
model="language"
:options="form.languageOptions"
icon="globe"
:placeholder="form.placeholder"
:label="$t('contribution.languageSelectLabel')"
/>
</ds-flex-item>
</ds-flex>
<div slot="footer" style="text-align: right">
<ds-button :disabled="loading || disabled" ghost @click.prevent="$router.back()">
<ds-button
:disabled="loading || disabled"
ghost
class="cancel-button"
@click="$router.back()"
>
{{ $t('actions.cancel') }}
</ds-button>
<ds-button
@ -28,6 +47,8 @@
<script>
import gql from 'graphql-tag'
import HcEditor from '~/components/Editor'
import orderBy from 'lodash/orderBy'
import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js'
export default {
@ -42,6 +63,9 @@ export default {
form: {
title: '',
content: '',
language: null,
languageOptions: [],
placeholder: '',
},
formSchema: {
title: { required: true, min: 3, max: 64 },
@ -65,13 +89,25 @@ export default {
this.slug = contribution.slug
this.form.content = contribution.content
this.form.title = contribution.title
this.form.language = this.locale
this.form.placeholder = this.locale
},
},
},
computed: {
locale() {
const locale = this.contribution.language
? locales.find(loc => this.contribution.language === loc.code)
: locales.find(loc => this.$i18n.locale() === loc.code)
return locale.name
},
},
mounted() {
this.availableLocales()
},
methods: {
submit() {
this.loading = true
this.$apollo
.mutate({
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
@ -79,11 +115,12 @@ export default {
id: this.id,
title: this.form.title,
content: this.form.content,
language: this.form.language ? this.form.language.value : this.$i18n.locale(),
},
})
.then(res => {
this.loading = false
this.$toast.success('Saved!')
this.$toast.success(this.$t('contribution.success'))
this.disabled = true
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
@ -103,6 +140,11 @@ export default {
// this.form.content = value
this.$refs.contributionForm.update('content', value)
},
availableLocales() {
orderBy(locales, 'name').map(locale => {
this.form.languageOptions.push({ label: locale.name, value: locale.code })
})
},
},
apollo: {
User: {
@ -135,4 +177,8 @@ export default {
padding-right: 0;
}
}
.contribution-form-footer {
border-top: $border-size-base solid $border-color-softest;
}
</style>

View File

@ -2,25 +2,27 @@ import gql from 'graphql-tag'
export default () => {
return {
CreatePost: gql`
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
CreatePost: gql(`
mutation($title: String!, $content: String!, $language: String) {
CreatePost(title: $title, content: $content, language: $language) {
id
title
slug
content
contentExcerpt
language
}
}
`,
UpdatePost: gql`
mutation($id: ID!, $title: String!, $content: String!) {
UpdatePost(id: $id, title: $title, content: $content) {
`),
UpdatePost: gql(`
mutation($id: ID!, $title: String!, $content: String!, $language: String) {
UpdatePost(id: $id, title: $title, content: $content, language: $language) {
id
title
slug
content
contentExcerpt
language
}
}
`,

View File

@ -301,5 +301,9 @@
"avatar": {
"submitted": "Upload erfolgreich"
}
},
"contribution": {
"success": "Gespeichert!",
"languageSelectLabel": "Sprache"
}
}

View File

@ -300,5 +300,9 @@
"avatar": {
"submitted": "Upload successful"
}
},
"contribution": {
"success": "Saved!",
"languageSelectLabel": "Language"
}
}

View File

@ -3,9 +3,7 @@
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form :contribution="contribution" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
&nbsp;
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">&nbsp;</ds-flex-item>
</ds-flex>
</template>
@ -49,6 +47,7 @@ export default {
deleted
slug
image
language
author {
id
disabled

View File

@ -26,8 +26,8 @@ describe('ProfileSlug', () => {
id: 'p23',
name: 'It is a post',
},
$t: jest.fn(t => t),
// If you mocking router, than don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$t: jest.fn(),
// If you're mocking router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$route: {
params: {
id: '4711',