Merge pull request #2160 from Human-Connection/2119_Create_Post_consistent_form_input_validation

🍰 2119-Fix Contribution  consistent form input validation
This commit is contained in:
mattwr18 2019-11-19 00:27:48 +01:00 committed by GitHub
commit c775884c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 275 additions and 259 deletions

View File

@ -5,9 +5,12 @@ import {
} from "cypress-cucumber-preprocessor/steps"; } from "cypress-cucumber-preprocessor/steps";
import helpers from "../../support/helpers"; import helpers from "../../support/helpers";
import { VERSION } from '../../constants/terms-and-conditions-version.js' import { VERSION } from '../../constants/terms-and-conditions-version.js'
import locales from '../../../webapp/locales'
import orderBy from 'lodash/orderBy'
/* global cy */ /* global cy */
const languages = orderBy(locales, 'name')
let lastPost = {}; let lastPost = {};
let loginCredentials = { let loginCredentials = {
@ -245,6 +248,12 @@ Then("I select a category", () => {
.click(); .click();
}); });
When("I choose {string} as the language for the post", (languageCode) => {
cy.get('.ds-flex-item > .ds-form-item .ds-select ')
.click().get('.ds-select-option')
.eq(languages.findIndex(l => l.code === languageCode)).click()
})
Then("the post shows up on the landing page at position {int}", index => { Then("the post shows up on the landing page at position {int}", index => {
cy.openPage("landing"); cy.openPage("landing");
const selector = `.post-card:nth-child(${index}) > .ds-card-content`; const selector = `.post-card:nth-child(${index}) > .ds-card-content`;

View File

@ -20,6 +20,7 @@ Feature: Notification for a mention
""" """
And mention "@matt-rider" in the text And mention "@matt-rider" in the text
And I select a category And I select a category
And I choose "en" as the language for the post
And I click on "Save" And I click on "Save"
When I log out When I log out
And I log in with the following credentials: And I log in with the following credentials:

View File

@ -17,7 +17,8 @@ Feature: Create a post
Human Connection is a free and open-source social network Human Connection is a free and open-source social network
for active citizenship. for active citizenship.
""" """
Then I select a category And I select a category
And I choose "en" as the language for the post
And I click on "Save" And I click on "Save"
Then I get redirected to ".../my-first-post" Then I get redirected to ".../my-first-post"
And the post was saved successfully And the post was saved successfully

View File

@ -8,10 +8,12 @@ localVue.use(Styleguide)
describe('CategoriesSelect.vue', () => { describe('CategoriesSelect.vue', () => {
let wrapper let wrapper
let mocks let mocks
let provide
let democracyAndPolitics let democracyAndPolitics
let environmentAndNature let environmentAndNature
let consumptionAndSustainablity let consumptionAndSustainablity
const propsData = { model: 'categoryIds' }
const categories = [ const categories = [
{ {
id: 'cat9', id: 'cat9',
@ -35,6 +37,11 @@ describe('CategoriesSelect.vue', () => {
}, },
] ]
beforeEach(() => { beforeEach(() => {
provide = {
$parentForm: {
update: jest.fn(),
},
}
mocks = { mocks = {
$t: jest.fn(), $t: jest.fn(),
} }
@ -42,7 +49,7 @@ describe('CategoriesSelect.vue', () => {
describe('shallowMount', () => { describe('shallowMount', () => {
const Wrapper = () => { const Wrapper = () => {
return mount(CategoriesSelect, { mocks, localVue }) return mount(CategoriesSelect, { propsData, mocks, localVue, provide })
} }
beforeEach(() => { beforeEach(() => {
@ -60,8 +67,8 @@ describe('CategoriesSelect.vue', () => {
expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id]) expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id])
}) })
it('emits an updateCategories event when the selectedCategoryIds changes', () => { it('calls $parent.update with selected category ids', () => {
expect(wrapper.emitted().updateCategories[0][0]).toEqual([categories[0].id]) expect(provide.$parentForm.update).toHaveBeenCalledWith('categoryIds', ['cat9'])
}) })
it('removes categories when clicked a second time', () => { it('removes categories when clicked a second time', () => {

View File

@ -5,6 +5,7 @@
<ds-flex-item> <ds-flex-item>
<ds-button <ds-button
size="small" size="small"
:data-test="categoryButtonsId(category.id)"
@click.prevent="toggleCategory(category.id)" @click.prevent="toggleCategory(category.id)"
:primary="isActive(category.id)" :primary="isActive(category.id)"
:disabled="isDisabled(category.id)" :disabled="isDisabled(category.id)"
@ -28,16 +29,23 @@
<script> <script>
import CategoryQuery from '~/graphql/CategoryQuery' import CategoryQuery from '~/graphql/CategoryQuery'
import xor from 'lodash/xor'
export default { export default {
inject: {
$parentForm: {
default: null,
},
},
props: { props: {
existingCategoryIds: { type: Array, default: () => [] }, existingCategoryIds: { type: Array, default: () => [] },
model: { type: String, required: true },
}, },
data() { data() {
return { return {
categories: null, categories: null,
selectedMax: 3, selectedMax: 3,
selectedCategoryIds: [], selectedCategoryIds: this.existingCategoryIds,
} }
}, },
computed: { computed: {
@ -48,39 +56,22 @@ export default {
return this.selectedCount >= this.selectedMax return this.selectedCount >= this.selectedMax
}, },
}, },
watch: {
selectedCategoryIds(categoryIds) {
this.$emit('updateCategories', categoryIds)
},
existingCategoryIds: {
immediate: true,
handler: function(existingCategoryIds) {
if (!existingCategoryIds || !existingCategoryIds.length) {
return
}
this.selectedCategoryIds = existingCategoryIds
},
},
},
methods: { methods: {
toggleCategory(id) { toggleCategory(id) {
const index = this.selectedCategoryIds.indexOf(id) this.selectedCategoryIds = xor(this.selectedCategoryIds, [id])
if (index > -1) { if (this.$parentForm) {
this.selectedCategoryIds.splice(index, 1) this.$parentForm.update(this.model, this.selectedCategoryIds)
} else {
this.selectedCategoryIds.push(id)
} }
}, },
isActive(id) { isActive(id) {
const index = this.selectedCategoryIds.indexOf(id) return this.selectedCategoryIds.includes(id)
if (index > -1) {
return true
}
return false
}, },
isDisabled(id) { isDisabled(id) {
return !!(this.reachedMaximum && !this.isActive(id)) return !!(this.reachedMaximum && !this.isActive(id))
}, },
categoryButtonsId(categoryId) {
return `category-buttons-${categoryId}`
},
}, },
apollo: { apollo: {
Category: { Category: {

View File

@ -20,15 +20,45 @@ config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>' config.stubs['nuxt-link'] = '<span><slot /></span>'
config.stubs['v-popover'] = '<span><slot /></span>' config.stubs['v-popover'] = '<span><slot /></span>'
const categories = [
{
id: 'cat3',
slug: 'health-wellbeing',
icon: 'medkit',
},
{
id: 'cat12',
slug: 'it-internet-data-privacy',
icon: 'mouse-pointer',
},
{
id: 'cat9',
slug: 'democracy-politics',
icon: 'university',
},
{
id: 'cat15',
slug: 'consumption-sustainability',
icon: 'shopping-cart',
},
{
id: 'cat4',
slug: 'environment-nature',
icon: 'tree',
},
]
describe('ContributionForm.vue', () => { describe('ContributionForm.vue', () => {
let wrapper let wrapper,
let postTitleInput postTitleInput,
let expectedParams expectedParams,
let deutschOption cancelBtn,
let cancelBtn mocks,
let mocks propsData,
let propsData categoryIds,
let categoryIds englishLanguage,
deutschLanguage,
dataPrivacyButton
const postTitle = 'this is a title for a post' const postTitle = 'this is a title for a post'
const postTitleTooShort = 'xx' const postTitleTooShort = 'xx'
let postTitleTooLong = '' let postTitleTooLong = ''
@ -36,11 +66,6 @@ describe('ContributionForm.vue', () => {
postTitleTooLong += 'x' postTitleTooLong += 'x'
} }
const postContent = 'this is a post' const postContent = 'this is a post'
const postContentTooShort = 'xx'
let postContentTooLong = ''
for (let i = 0; i < 2001; i++) {
postContentTooLong += 'x'
}
const imageUpload = { const imageUpload = {
file: { file: {
filename: 'avataar.svg', filename: 'avataar.svg',
@ -109,90 +134,59 @@ describe('ContributionForm.vue', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper()
wrapper.setData({
form: {
languageOptions: [
{
label: 'Deutsch',
value: 'de',
},
],
},
})
}) })
describe('CreatePost', () => { describe('CreatePost', () => {
describe('language placeholder', () => {
it("displays the name that corresponds with the user's location code", () => {
expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English')
})
})
describe('invalid form submission', () => { describe('invalid form submission', () => {
it('title and content should not be empty ', async () => { beforeEach(async () => {
wrapper.find('.submit-button-for-test').trigger('click') wrapper.find(CategoriesSelect).setData({ categories })
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
}) })
it('title should not be empty', async () => { it('title should not be empty', async () => {
await wrapper.vm.updateEditorContent(postContent) postTitleInput.setValue('')
wrapper.find('.submit-button-for-test').trigger('click') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
it('title should not be too long', async () => { it('title should not be too long', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitleTooLong) postTitleInput.setValue(postTitleTooLong)
await wrapper.vm.updateEditorContent(postContent) wrapper.find('form').trigger('submit')
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
it('title should not be too short', async () => { it('title should not be too short', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitleTooShort) postTitleInput.setValue(postTitleTooShort)
await wrapper.vm.updateEditorContent(postContent) wrapper.find('form').trigger('submit')
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
it('content should not be empty', async () => { it('content should not be empty', async () => {
postTitleInput = wrapper.find('.ds-input') await wrapper.vm.updateEditorContent('')
postTitleInput.setValue(postTitle) await wrapper.find('form').trigger('submit')
await wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content should not be too short', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContentTooShort)
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('content should not be too long', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContentTooLong)
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
it('should have at least one category', async () => { it('should have at least one category', async () => {
postTitleInput = wrapper.find('.ds-input') dataPrivacyButton = await wrapper
postTitleInput.setValue(postTitle) .find(CategoriesSelect)
await wrapper.vm.updateEditorContent(postContent) .find('[data-test="category-buttons-cat12"]')
wrapper.find('.submit-button-for-test').trigger('click') dataPrivacyButton.trigger('click')
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
it('should have not have more than three categories', async () => { it('should have not have more than three categories', async () => {
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent)
wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27'] wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
wrapper.find('.submit-button-for-test').trigger('click') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
}) })
@ -214,43 +208,51 @@ describe('ContributionForm.vue', () => {
postTitleInput = wrapper.find('.ds-input') postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle) postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent) await wrapper.vm.updateEditorContent(postContent)
categoryIds = ['cat12'] wrapper.find(CategoriesSelect).setData({ categories })
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
}) })
it('creates a post with valid title, content, and at least one category', async () => { it('creates a post with valid title, content, and at least one category', async () => {
await wrapper.find('.submit-button-for-test').trigger('click') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it("sends a fallback language based on a user's locale", () => {
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it('supports changing the language', async () => { it('supports changing the language', async () => {
expectedParams.variables.language = 'de' expectedParams.variables.language = 'de'
deutschOption = wrapper.findAll('li').at(0) deutschLanguage = wrapper.findAll('li').filter(language => language.text() === 'Deutsch')
deutschOption.trigger('click') deutschLanguage.trigger('click')
wrapper.find('.submit-button-for-test').trigger('click') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it('supports adding a teaser image', async () => { it('supports adding a teaser image', async () => {
expectedParams.variables.imageUpload = imageUpload expectedParams.variables.imageUpload = imageUpload
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload) wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
await wrapper.find('.submit-button-for-test').trigger('click') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it('content is valid with just a link', async () => {
await wrapper.vm.updateEditorContent(
'<a href="https://www.youtube.com/watch?v=smoEelV6FUk" target="_blank"></a>',
)
wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it("pushes the user to the post's page", async () => { it("pushes the user to the post's page", async () => {
wrapper.find('.submit-button-for-test').trigger('click') wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate await mocks.$apollo.mutate
expect(mocks.$router.push).toHaveBeenCalledTimes(1) expect(mocks.$router.push).toHaveBeenCalledTimes(1)
}) })
it('shows a success toaster', async () => { it('shows a success toaster', async () => {
wrapper.find('.submit-button-for-test').trigger('click') wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate await mocks.$apollo.mutate
expect(mocks.$toast.success).toHaveBeenCalledTimes(1) expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
}) })
@ -275,11 +277,17 @@ describe('ContributionForm.vue', () => {
postTitleInput.setValue(postTitle) postTitleInput.setValue(postTitle)
await wrapper.vm.updateEditorContent(postContent) await wrapper.vm.updateEditorContent(postContent)
categoryIds = ['cat12'] categoryIds = ['cat12']
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) wrapper.find(CategoriesSelect).setData({ categories })
englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English')
englishLanguage.trigger('click')
dataPrivacyButton = await wrapper
.find(CategoriesSelect)
.find('[data-test="category-buttons-cat12"]')
dataPrivacyButton.trigger('click')
}) })
it('shows an error toaster when apollo mutation rejects', async () => { it('shows an error toaster when apollo mutation rejects', async () => {
await wrapper.find('.submit-button-for-test').trigger('click') await wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate await mocks.$apollo.mutate
await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!') await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
}) })
@ -341,11 +349,11 @@ describe('ContributionForm.vue', () => {
expectedParams = { expectedParams = {
mutation: PostMutations().UpdatePost, mutation: PostMutations().UpdatePost,
variables: { variables: {
title: postTitle, title: propsData.contribution.title,
content: postContent, content: propsData.contribution.content,
language: propsData.contribution.language, language: propsData.contribution.language,
id: propsData.contribution.id, id: propsData.contribution.id,
categoryIds, categoryIds: ['cat12'],
image, image,
imageUpload: null, imageUpload: null,
}, },
@ -353,22 +361,20 @@ describe('ContributionForm.vue', () => {
}) })
it('calls the UpdatePost apollo mutation', async () => { it('calls the UpdatePost apollo mutation', async () => {
postTitleInput = wrapper.find('.ds-input') expectedParams.variables.content = postContent
postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent) wrapper.vm.updateEditorContent(postContent)
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) await wrapper.find('form').trigger('submit')
wrapper.find('.submit-button-for-test').trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it('supports updating categories', async () => { it('supports updating categories', async () => {
const categoryIds = ['cat3', 'cat51', 'cat37'] expectedParams.variables.categoryIds.push('cat3')
postTitleInput = wrapper.find('.ds-input') wrapper.find(CategoriesSelect).setData({ categories })
postTitleInput.setValue(postTitle) const healthWellbeingButton = await wrapper
wrapper.vm.updateEditorContent(postContent) .find(CategoriesSelect)
expectedParams.variables.categoryIds = categoryIds .find('[data-test="category-buttons-cat3"]')
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) healthWellbeingButton.trigger('click')
await wrapper.find('.submit-button-for-test').trigger('click') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
}) })

View File

@ -1,5 +1,11 @@
<template> <template>
<ds-form ref="contributionForm" v-model="form" :schema="formSchema"> <ds-form
class="contribution-form"
ref="contributionForm"
v-model="form"
:schema="formSchema"
@submit="submit"
>
<template slot-scope="{ errors }"> <template slot-scope="{ errors }">
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage"> <hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
<img <img
@ -21,7 +27,13 @@
name="title" name="title"
autofocus autofocus
/> />
<small class="smallTag">{{ form.title.length }}/{{ formSchema.title.max }}</small> <ds-text align="right">
<ds-chip v-if="errors && errors.title" color="danger" size="base">
{{ form.title.length }}/{{ formSchema.title.max }}
<ds-icon name="warning"></ds-icon>
</ds-chip>
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
</ds-text>
<client-only> <client-only>
<hc-editor <hc-editor
:users="users" :users="users"
@ -29,27 +41,43 @@
:hashtags="hashtags" :hashtags="hashtags"
@input="updateEditorContent" @input="updateEditorContent"
/> />
<small class="smallTag">{{ form.contentLength }}</small> <ds-text align="right">
<ds-chip v-if="errors && errors.content" color="danger" size="base">
{{ contentLength }}
<ds-icon name="warning"></ds-icon>
</ds-chip>
<ds-chip v-else size="base">
{{ contentLength }}
</ds-chip>
</ds-text>
</client-only> </client-only>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<hc-categories-select <hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
model="categoryIds" <ds-text align="right">
@updateCategories="updateCategories" <ds-chip v-if="errors && errors.categoryIds" color="danger" size="base">
:existingCategoryIds="form.categoryIds" {{ form.categoryIds.length }} / 3
/> <ds-icon name="warning"></ds-icon>
</ds-chip>
<ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip>
</ds-text>
<ds-flex class="contribution-form-footer"> <ds-flex class="contribution-form-footer">
<ds-flex-item :width="{ base: '10%', sm: '10%', md: '10%', lg: '15%' }" /> <ds-flex-item :width="{ lg: '50%', md: '50%', sm: '100%' }" />
<ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }"> <ds-flex-item>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<ds-select <ds-select
model="language" model="language"
:options="form.languageOptions" :options="languageOptions"
icon="globe" icon="globe"
:placeholder="locale" :placeholder="$t('contribution.languageSelectText')"
:label="$t('contribution.languageSelectLabel')" :label="$t('contribution.languageSelectLabel')"
/> />
</ds-flex-item> </ds-flex-item>
</ds-flex> </ds-flex>
<ds-text align="right">
<ds-chip v-if="errors && errors.language" size="base" color="danger">
<ds-icon name="warning"></ds-icon>
</ds-chip>
</ds-text>
<ds-space /> <ds-space />
<div slot="footer" style="text-align: right"> <div slot="footer" style="text-align: right">
<ds-button <ds-button
@ -60,15 +88,7 @@
> >
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
</ds-button> </ds-button>
<ds-button <ds-button type="submit" icon="check" :loading="loading" :disabled="errors" primary>
class="submit-button-for-test"
type="submit"
icon="check"
:loading="loading"
:disabled="failsValidations || errors"
primary
@click.prevent="submit"
>
{{ $t('actions.save') }} {{ $t('actions.save') }}
</ds-button> </ds-button>
</div> </div>
@ -100,73 +120,78 @@ export default {
contribution: { type: Object, default: () => {} }, contribution: { type: Object, default: () => {} },
}, },
data() { data() {
const languageOptions = orderBy(locales, 'name').map(locale => {
return { label: locale.name, value: locale.code }
})
const formDefaults = {
title: '',
content: '',
teaserImage: null,
image: null,
language: null,
categoryIds: [],
}
let id = null
let slug = null
const form = { ...formDefaults }
if (this.contribution && this.contribution.id) {
id = this.contribution.id
slug = this.contribution.slug
form.title = this.contribution.title
form.content = this.contribution.content
form.image = this.contribution.image
form.language =
this.contribution && this.contribution.language
? languageOptions.find(o => this.contribution.language === o.value)
: null
form.categoryIds = this.categoryIds(this.contribution.categories)
}
return { return {
form: { form,
title: '',
content: '',
contentLength: 0,
teaserImage: null,
image: null,
language: null,
languageOptions: [],
categoryIds: [],
},
formSchema: { formSchema: {
title: { required: true, min: 3, max: 100 }, title: { required: true, min: 3, max: 100 },
content: [{ required: true }], content: { required: true },
categoryIds: {
type: 'array',
required: true,
validator: (rule, value) => {
const errors = []
if (!(value && value.length >= 1 && value.length <= 3)) {
errors.push(new Error(this.$t('common.validations.categories')))
}
return errors
},
},
language: { required: true },
}, },
id: null, languageOptions,
id,
slug,
loading: false, loading: false,
slug: null,
users: [], users: [],
contentMin: 3, contentMin: 3,
failsValidations: true,
hashtags: [], hashtags: [],
} }
}, },
watch: {
contribution: {
immediate: true,
handler: function(contribution) {
if (!contribution || !contribution.id) {
return
}
this.id = contribution.id
this.slug = contribution.slug
this.form.title = contribution.title
this.form.content = contribution.content
this.form.image = contribution.image
this.form.categoryIds = this.categoryIds(contribution.categories)
this.manageContent(this.form.content)
},
},
},
computed: { computed: {
locale() { contentLength() {
const locale = return this.$filters.removeHtml(this.form.content).length
this.contribution && this.contribution.language
? locales.find(loc => this.contribution.language === loc.code)
: locales.find(loc => this.$i18n.locale() === loc.code)
return locale.name
}, },
...mapGetters({ ...mapGetters({
currentUser: 'auth/user', currentUser: 'auth/user',
}), }),
}, },
mounted() {
this.availableLocales()
},
methods: { methods: {
submit() { submit() {
const { title, content, image, teaserImage, categoryIds } = this.form const {
let language language: { value: language },
if (this.form.language) { title,
language = this.form.language.value content,
} else if (this.contribution && this.contribution.language) { image,
language = this.contribution.language teaserImage,
} else { categoryIds,
language = this.$i18n.locale() } = this.form
}
this.loading = true this.loading = true
this.$apollo this.$apollo
.mutate({ .mutate({
@ -185,7 +210,6 @@ export default {
this.loading = false this.loading = false
this.$toast.success(this.$t('contribution.success')) this.$toast.success(this.$t('contribution.success'))
const result = data[this.id ? 'UpdatePost' : 'CreatePost'] const result = data[this.id ? 'UpdatePost' : 'CreatePost']
this.failedValidations = false
this.$router.push({ this.$router.push({
name: 'post-id-slug', name: 'post-id-slug',
@ -195,45 +219,16 @@ export default {
.catch(err => { .catch(err => {
this.$toast.error(err.message) this.$toast.error(err.message)
this.loading = false this.loading = false
this.failedValidations = true
}) })
}, },
updateEditorContent(value) { updateEditorContent(value) {
// TODO: Do smth????? what is happening
this.$refs.contributionForm.update('content', value) this.$refs.contributionForm.update('content', value)
this.manageContent(value)
},
manageContent(content) {
// filter HTML out of content value
const str = content.replace(/<\/?[^>]+(>|$)/gm, '')
// Set counter length of text
this.form.contentLength = str.length
this.validatePost()
},
availableLocales() {
orderBy(locales, 'name').map(locale => {
this.form.languageOptions.push({ label: locale.name, value: locale.code })
})
},
updateCategories(ids) {
this.form.categoryIds = ids
this.validatePost()
}, },
addTeaserImage(file) { addTeaserImage(file) {
this.form.teaserImage = file this.form.teaserImage = file
}, },
categoryIds(categories) { categoryIds(categories) {
const categoryIds = [] return categories.map(c => c.id)
categories.map(categoryId => {
categoryIds.push(categoryId.id)
})
return categoryIds
},
validatePost() {
const passesContentValidations = this.form.contentLength >= this.contentMin
const passesCategoryValidations =
this.form.categoryIds.length > 0 && this.form.categoryIds.length <= 3
this.failsValidations = !(passesContentValidations && passesCategoryValidations)
}, },
}, },
apollo: { apollo: {
@ -288,4 +283,10 @@ export default {
padding-right: 0; padding-right: 0;
} }
} }
.contribution-form {
.ds-chip {
cursor: default;
}
}
</style> </style>

View File

@ -463,7 +463,8 @@
"reportContent": "Melden", "reportContent": "Melden",
"validations": { "validations": {
"email": "muss eine gültige E-Mail Adresse sein", "email": "muss eine gültige E-Mail Adresse sein",
"url": "muss eine gültige URL sein" "url": "muss eine gültige URL sein",
"categories": "es müssen eine bis drei Kategorien ausgewählt werden"
} }
}, },
"actions": { "actions": {
@ -608,7 +609,8 @@
"filterFollow": "Beiträge filtern von Usern denen ich folge", "filterFollow": "Beiträge filtern von Usern denen ich folge",
"filterALL": "Alle Beiträge anzeigen", "filterALL": "Alle Beiträge anzeigen",
"success": "Gespeichert!", "success": "Gespeichert!",
"languageSelectLabel": "Sprache", "languageSelectLabel": "Sprache deines Beitrags",
"languageSelectText": "Sprache wählen",
"categories": { "categories": {
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt" "infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
}, },

View File

@ -464,7 +464,8 @@
"reportContent": "Report", "reportContent": "Report",
"validations": { "validations": {
"email": "must be a valid e-mail address", "email": "must be a valid e-mail address",
"url": "must be a valid URL" "url": "must be a valid URL",
"categories": "at least one and at most three categories must be selected"
} }
}, },
"actions": { "actions": {
@ -609,7 +610,8 @@
"filterFollow": "Filter contributions from users I follow", "filterFollow": "Filter contributions from users I follow",
"filterALL": "View all contributions", "filterALL": "View all contributions",
"success": "Saved!", "success": "Saved!",
"languageSelectLabel": "Language", "languageSelectLabel": "Language of your contribution",
"languageSelectText": "Select Language",
"categories": { "categories": {
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected" "infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
}, },

View File

@ -10,41 +10,37 @@
<script> <script>
import HcContributionForm from '~/components/ContributionForm/ContributionForm' import HcContributionForm from '~/components/ContributionForm/ContributionForm'
import PostQuery from '~/graphql/PostQuery' import PostQuery from '~/graphql/PostQuery'
import { mapGetters } from 'vuex'
export default { export default {
components: { components: {
HcContributionForm, HcContributionForm,
}, },
computed: { computed: {
user() { ...mapGetters({
return this.$store.getters['auth/user'] user: 'auth/user',
}, }),
author() {
return this.contribution ? this.contribution.author : {}
},
contribution() {
return this.Post ? this.Post[0] : {}
},
}, },
watch: { async asyncData(context) {
contribution() { const {
if (this.author.id !== this.user.id) { app,
throw new Error(`You can't edit that!`) store,
} error,
}, params: { id },
}, } = context
apollo: { const client = app.apolloProvider.defaultClient
Post: { const {
query() { data: {
return PostQuery(this.$i18n) Post: [contribution],
}, },
variables() { } = await client.query({
return { query: PostQuery(app.$i18n),
id: this.$route.params.id, variables: { id },
} })
}, if (contribution.author.id !== store.getters['auth/user'].id) {
fetchPolicy: 'cache-and-network', error({ statusCode: 403, message: "You can't edit that!" })
}, }
return { contribution }
}, },
} }
</script> </script>