Merge remote-tracking branch 'origin/2119_Create_Post_consistent_form_input_validation' into 2119_Create_Post_consistent_form_input_validation-improvements

This commit is contained in:
roschaefer 2019-11-08 16:28:54 +01:00
commit 4108bb7d71
5 changed files with 133 additions and 109 deletions

View File

@ -22,37 +22,36 @@ config.stubs['v-popover'] = '<span><slot /></span>'
const categories = [ const categories = [
{ {
"id": "cat3", id: 'cat3',
"slug": "health-wellbeing", slug: 'health-wellbeing',
"icon": "medkit" icon: 'medkit',
}, },
{ {
"id": "cat12", id: 'cat12',
"slug": "it-internet-data-privacy", slug: 'it-internet-data-privacy',
"icon": "mouse-pointer" icon: 'mouse-pointer',
}, },
{ {
"id": "cat9", id: 'cat9',
"slug": "democracy-politics", slug: 'democracy-politics',
"icon": "university" icon: 'university',
}, },
{ {
"id": "cat15", id: 'cat15',
"slug": "consumption-sustainability", slug: 'consumption-sustainability',
"icon": "shopping-cart" icon: 'shopping-cart',
}, },
{ {
"id": "cat4", id: 'cat4',
"slug": "environment-nature", slug: 'environment-nature',
"icon": "tree" icon: 'tree',
} },
] ]
describe('ContributionForm.vue', () => { describe('ContributionForm.vue', () => {
let wrapper let wrapper
let postTitleInput let postTitleInput
let expectedParams let expectedParams
let deutschOption
let cancelBtn let cancelBtn
let mocks let mocks
let propsData let propsData
@ -137,21 +136,13 @@ describe('ContributionForm.vue', () => {
beforeEach(() => { beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper()
wrapper.setData({
form: {
languageOptions: [
{
label: 'Deutsch',
value: 'de',
},
],
},
})
}) })
describe('CreatePost', () => { describe('CreatePost', () => {
describe('language placeholder', () => { describe('language placeholder', () => {
it("displays the name that corresponds with the user's location code", () => { it.skip("displays the name that corresponds with the user's location code", () => {
// Well not anymore right? We want the user to save the language
// excplicitly. I'll keep this test if we change our minds
expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English') expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English')
}) })
}) })
@ -242,8 +233,16 @@ 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)
wrapper.find(CategoriesSelect).setData({categories}) wrapper.find(CategoriesSelect).setData({ categories })
await wrapper.find(CategoriesSelect).findAll('button').at(1).trigger('click') wrapper
.findAll('li')
.at(1)
.trigger('click') // language
await wrapper
.find(CategoriesSelect)
.findAll('button')
.at(1)
.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 () => {
@ -251,15 +250,12 @@ describe('ContributionForm.vue', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it("sends a fallback language based on a user's locale", () => {
wrapper.find('form').trigger('submit')
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) wrapper
deutschOption.trigger('click') .findAll('li')
.at(0)
.trigger('click') // choose German as language
wrapper.find('form').trigger('submit') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
@ -303,8 +299,16 @@ 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).setData({categories}) wrapper.find(CategoriesSelect).setData({ categories })
await wrapper.find(CategoriesSelect).findAll('button').at(1).trigger('click') wrapper
.findAll('li')
.at(1)
.trigger('click') // language
await wrapper
.find(CategoriesSelect)
.findAll('button')
.at(1)
.trigger('click')
}) })
it('shows an error toaster when apollo mutation rejects', async () => { it('shows an error toaster when apollo mutation rejects', async () => {
@ -385,6 +389,10 @@ describe('ContributionForm.vue', () => {
postTitleInput = wrapper.find('.ds-input') postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle) postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent) wrapper.vm.updateEditorContent(postContent)
wrapper
.findAll('li')
.at(0)
.trigger('click') // language
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
@ -394,10 +402,26 @@ describe('ContributionForm.vue', () => {
postTitleInput = wrapper.find('.ds-input') postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle) postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent) wrapper.vm.updateEditorContent(postContent)
wrapper.find(CategoriesSelect).setData({categories}) wrapper
await wrapper.find(CategoriesSelect).findAll('button').at(0).trigger('click') .findAll('li')
await wrapper.find(CategoriesSelect).findAll('button').at(3).trigger('click') .at(0)
await wrapper.find(CategoriesSelect).findAll('button').at(4).trigger('click') .trigger('click') // language
wrapper.find(CategoriesSelect).setData({ categories })
await wrapper
.find(CategoriesSelect)
.findAll('button')
.at(0)
.trigger('click')
await wrapper
.find(CategoriesSelect)
.findAll('button')
.at(3)
.trigger('click')
await wrapper
.find(CategoriesSelect)
.findAll('button')
.at(4)
.trigger('click')
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })

View File

@ -50,23 +50,27 @@
<ds-text align="right"> <ds-text align="right">
<ds-chip v-if="errors && errors.categoryIds" color="danger" size="base"> <ds-chip v-if="errors && errors.categoryIds" color="danger" size="base">
{{ form.categoryIds.length }} / 3 {{ form.categoryIds.length }} / 3
<ds-icon name="warning"></ds-icon> <ds-icon name="warning" class="colorRed"></ds-icon>
</ds-chip> </ds-chip>
<ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip> <ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip>
</ds-text> </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>
<ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }">
<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
@ -77,13 +81,7 @@
> >
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
</ds-button> </ds-button>
<ds-button <ds-button type="submit" icon="check" :loading="loading" :disabled="errors" primary>
type="submit"
icon="check"
:loading="loading"
:disabled="errors"
primary
>
{{ $t('actions.save') }} {{ $t('actions.save') }}
</ds-button> </ds-button>
</div> </div>
@ -115,16 +113,35 @@ 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: '',
categoryIds: [],
teaserImage: null,
image: null,
language: null,
languageOptions: [],
},
formSchema: { formSchema: {
title: { required: true, min: 3, max: 100 }, title: { required: true, min: 3, max: 100 },
content: { content: {
@ -139,66 +156,41 @@ export default {
required: true, required: true,
validator: (rule, value) => { validator: (rule, value) => {
const errors = [] const errors = []
if (!(value && 1 <= value.length && value.length <= 3)) { if (!(value && value.length >= 1 && value.length <= 3)) {
errors.push(new Error(this.$t('common.validations.categories'))) errors.push(new Error(this.$t('common.validations.categories')))
} }
return errors return errors
}, },
}, },
language: { required: true },
}, },
id: null, languageOptions,
id,
slug,
loading: false, loading: false,
slug: null,
users: [], users: [],
contentMin: 3, contentMin: 3,
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)
},
},
},
computed: { computed: {
contentLength() { contentLength() {
return this.$filters.removeHtml(this.form.content).length return this.$filters.removeHtml(this.form.content).length
}, },
locale() {
const locale =
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({
@ -231,11 +223,6 @@ export default {
updateEditorContent(value) { updateEditorContent(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 })
})
},
addTeaserImage(file) { addTeaserImage(file) {
this.form.teaserImage = file this.form.teaserImage = file
}, },
@ -295,4 +282,14 @@ export default {
padding-right: 0; padding-right: 0;
} }
} }
.checkicon {
cursor: default;
top: -18px;
}
.checkicon_cat {
top: -58px;
}
.colorRed {
color: red;
}
</style> </style>

View File

@ -45,6 +45,7 @@ export const postFragment = lang => gql`
deleted deleted
slug slug
image image
language
author { author {
...user ...user
} }

View File

@ -580,7 +580,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

@ -581,7 +581,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"
}, },