mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
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:
commit
68dcbacaff
@ -74,6 +74,22 @@ describe('CreatePost', () => {
|
|||||||
await expect(client.request(mutation)).resolves.toMatchObject(expected)
|
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),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -15,29 +15,37 @@ type Post {
|
|||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
||||||
createdAt: String
|
createdAt: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
|
language: String
|
||||||
relatedContributions: [Post]! @cypher(
|
relatedContributions: [Post]!
|
||||||
statement: """
|
@cypher(
|
||||||
|
statement: """
|
||||||
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||||
RETURN DISTINCT post
|
RETURN DISTINCT post
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
|
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")
|
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?
|
# Has the currently logged in user shouted that post?
|
||||||
shoutedByCurrentUser: Boolean! @cypher(
|
shoutedByCurrentUser: Boolean!
|
||||||
statement: """
|
@cypher(
|
||||||
|
statement: """
|
||||||
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
|
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
|
||||||
RETURN COUNT(u) >= 1
|
RETURN COUNT(u) >= 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
142
webapp/components/ContributionForm/ContributionForm.spec.js
Normal file
142
webapp/components/ContributionForm/ContributionForm.spec.js
Normal 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!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -6,8 +6,27 @@
|
|||||||
<no-ssr>
|
<no-ssr>
|
||||||
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
|
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
|
||||||
</no-ssr>
|
</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">
|
<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') }}
|
{{ $t('actions.cancel') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
<ds-button
|
<ds-button
|
||||||
@ -28,6 +47,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import HcEditor from '~/components/Editor'
|
import HcEditor from '~/components/Editor'
|
||||||
|
import orderBy from 'lodash/orderBy'
|
||||||
|
import locales from '~/locales'
|
||||||
import PostMutations from '~/graphql/PostMutations.js'
|
import PostMutations from '~/graphql/PostMutations.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -42,6 +63,9 @@ export default {
|
|||||||
form: {
|
form: {
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
|
language: null,
|
||||||
|
languageOptions: [],
|
||||||
|
placeholder: '',
|
||||||
},
|
},
|
||||||
formSchema: {
|
formSchema: {
|
||||||
title: { required: true, min: 3, max: 64 },
|
title: { required: true, min: 3, max: 64 },
|
||||||
@ -65,13 +89,25 @@ export default {
|
|||||||
this.slug = contribution.slug
|
this.slug = contribution.slug
|
||||||
this.form.content = contribution.content
|
this.form.content = contribution.content
|
||||||
this.form.title = contribution.title
|
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: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||||
@ -79,11 +115,12 @@ export default {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.form.title,
|
title: this.form.title,
|
||||||
content: this.form.content,
|
content: this.form.content,
|
||||||
|
language: this.form.language ? this.form.language.value : this.$i18n.locale(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$toast.success('Saved!')
|
this.$toast.success(this.$t('contribution.success'))
|
||||||
this.disabled = true
|
this.disabled = true
|
||||||
|
|
||||||
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
|
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
|
||||||
@ -103,6 +140,11 @@ export default {
|
|||||||
// this.form.content = value
|
// this.form.content = value
|
||||||
this.$refs.contributionForm.update('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: {
|
apollo: {
|
||||||
User: {
|
User: {
|
||||||
@ -135,4 +177,8 @@ export default {
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contribution-form-footer {
|
||||||
|
border-top: $border-size-base solid $border-color-softest;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,25 +2,27 @@ import gql from 'graphql-tag'
|
|||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return {
|
return {
|
||||||
CreatePost: gql`
|
CreatePost: gql(`
|
||||||
mutation($title: String!, $content: String!) {
|
mutation($title: String!, $content: String!, $language: String) {
|
||||||
CreatePost(title: $title, content: $content) {
|
CreatePost(title: $title, content: $content, language: $language) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
slug
|
slug
|
||||||
content
|
content
|
||||||
contentExcerpt
|
contentExcerpt
|
||||||
|
language
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`),
|
||||||
UpdatePost: gql`
|
UpdatePost: gql(`
|
||||||
mutation($id: ID!, $title: String!, $content: String!) {
|
mutation($id: ID!, $title: String!, $content: String!, $language: String) {
|
||||||
UpdatePost(id: $id, title: $title, content: $content) {
|
UpdatePost(id: $id, title: $title, content: $content, language: $language) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
slug
|
slug
|
||||||
content
|
content
|
||||||
contentExcerpt
|
contentExcerpt
|
||||||
|
language
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@ -301,5 +301,9 @@
|
|||||||
"avatar": {
|
"avatar": {
|
||||||
"submitted": "Upload erfolgreich"
|
"submitted": "Upload erfolgreich"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"contribution": {
|
||||||
|
"success": "Gespeichert!",
|
||||||
|
"languageSelectLabel": "Sprache"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -300,5 +300,9 @@
|
|||||||
"avatar": {
|
"avatar": {
|
||||||
"submitted": "Upload successful"
|
"submitted": "Upload successful"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"contribution": {
|
||||||
|
"success": "Saved!",
|
||||||
|
"languageSelectLabel": "Language"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,7 @@
|
|||||||
<ds-flex-item :width="{ base: '100%', md: 3 }">
|
<ds-flex-item :width="{ base: '100%', md: 3 }">
|
||||||
<hc-contribution-form :contribution="contribution" />
|
<hc-contribution-form :contribution="contribution" />
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item :width="{ base: '100%', md: 1 }">
|
<ds-flex-item :width="{ base: '100%', md: 1 }"> </ds-flex-item>
|
||||||
|
|
||||||
</ds-flex-item>
|
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -49,6 +47,7 @@ export default {
|
|||||||
deleted
|
deleted
|
||||||
slug
|
slug
|
||||||
image
|
image
|
||||||
|
language
|
||||||
author {
|
author {
|
||||||
id
|
id
|
||||||
disabled
|
disabled
|
||||||
|
|||||||
@ -26,8 +26,8 @@ describe('ProfileSlug', () => {
|
|||||||
id: 'p23',
|
id: 'p23',
|
||||||
name: 'It is a post',
|
name: 'It is a post',
|
||||||
},
|
},
|
||||||
$t: jest.fn(t => t),
|
$t: jest.fn(),
|
||||||
// If you mocking router, than don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
// 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: {
|
$route: {
|
||||||
params: {
|
params: {
|
||||||
id: '4711',
|
id: '4711',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user