diff --git a/backend/package.json b/backend/package.json index 565973a67..4036430ea 100644 --- a/backend/package.json +++ b/backend/package.json @@ -61,7 +61,7 @@ "graphql-custom-directives": "~0.2.14", "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.2", - "graphql-shield": "~5.7.1", + "graphql-shield": "~5.6.1", "graphql-tag": "~2.10.1", "graphql-yoga": "~1.18.0", "helmet": "~3.18.0", diff --git a/backend/src/schema/resolvers/fileUpload/index.js b/backend/src/schema/resolvers/fileUpload/index.js index c37d87e39..fa78238c3 100644 --- a/backend/src/schema/resolvers/fileUpload/index.js +++ b/backend/src/schema/resolvers/fileUpload/index.js @@ -12,7 +12,6 @@ const storeUpload = ({ createReadStream, fileLocation }) => export default async function fileUpload(params, { file, url }, uploadCallback = storeUpload) { const upload = params[file] - if (upload) { const { createReadStream, filename } = await upload const { name } = path.parse(filename) diff --git a/backend/src/schema/types/scalar/Upload.gql b/backend/src/schema/types/scalar/Upload.gql index fca9ea1fc..cf3965846 100644 --- a/backend/src/schema/types/scalar/Upload.gql +++ b/backend/src/schema/types/scalar/Upload.gql @@ -1 +1 @@ -scalar Upload \ No newline at end of file +scalar Upload diff --git a/backend/yarn.lock b/backend/yarn.lock index 14cec1a81..b66143b5e 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1110,10 +1110,10 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== -"@types/yup@0.26.17": - version "0.26.17" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.17.tgz#5cb7cfc211d8e985b21d88289542591c92cad9dc" - integrity sha512-MN7VHlPsZQ2MTBxLE2Gl+Qfg2WyKsoz+vIr8xN0OSZ4AvJDrrKBlxc8b59UXCCIG9tPn9XhxTXh3j/htHbzC2Q== +"@types/yup@0.26.16": + version "0.26.16" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.16.tgz#75c428236207c48d9f8062dd1495cda8c5485a15" + integrity sha512-E2RNc7DSeQ+2EIJ1H3+yFjYu6YiyQBUJ7yNpIxomrYJ3oFizLZ5yDS3T1JTUNBC2OCRkgnhLS0smob5UuCHfNA== "@types/zen-observable@^0.5.3": version "0.5.4" @@ -3788,12 +3788,12 @@ graphql-request@~1.8.2: dependencies: cross-fetch "2.2.2" -graphql-shield@~5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.7.1.tgz#04095fb8148a463997f7c509d4aeb2a6abf79f98" - integrity sha512-UZ0K1uAqRAoGA1U2DsUu4vIZX2Vents4Xim99GFEUBTgvSDkejiE+k/Dywqfu76lJFEE8qu3vG5fhJN3SmnKbA== +graphql-shield@~5.6.1: + version "5.6.2" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.6.2.tgz#27eaad2ce2591ed81b1203e8915df99b28fb5ad5" + integrity sha512-DlS6r39s7AaP07yMM6i7GI87UkfL65O1tUPW4kNqp67fD1BU71Ekl7Kt/1L3rxS/gcQdGufuKka5oKUa5GKo2A== dependencies: - "@types/yup" "0.26.17" + "@types/yup" "0.26.16" lightercollective "^0.3.0" object-hash "^1.3.1" yup "^0.27.0" diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index 6856a64b2..caeeafdf6 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -3,11 +3,14 @@ import ContributionForm from './index.vue' import Styleguide from '@human-connection/styleguide' import Vuex from 'vuex' import PostMutations from '~/graphql/PostMutations.js' +import Filters from '~/plugins/vue-filters' +import TeaserImage from '~/components/TeaserImage/TeaserImage' const localVue = createLocalVue() localVue.use(Vuex) localVue.use(Styleguide) +localVue.use(Filters) config.stubs['no-ssr'] = '' @@ -21,6 +24,10 @@ describe('ContributionForm.vue', () => { let propsData const postTitle = 'this is a title for a post' const postContent = 'this is a post' + const imageUpload = { + file: { filename: 'avataar.svg', previewElement: '' }, + url: 'someUrlToImage', + } beforeEach(() => { mocks = { @@ -100,7 +107,13 @@ describe('ContributionForm.vue', () => { beforeEach(async () => { expectedParams = { mutation: PostMutations().CreatePost, - variables: { title: postTitle, content: postContent, language: 'en', id: null }, + variables: { + title: postTitle, + content: postContent, + language: 'en', + id: null, + imageUpload: null, + }, } postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) @@ -124,6 +137,13 @@ describe('ContributionForm.vue', () => { expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) + it('supports adding a teaser image', async () => { + expectedParams.variables.imageUpload = imageUpload + wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload) + 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) }) @@ -143,6 +163,7 @@ describe('ContributionForm.vue', () => { describe('handles errors', () => { beforeEach(async () => { + jest.useFakeTimers() wrapper = Wrapper() postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) @@ -150,6 +171,7 @@ describe('ContributionForm.vue', () => { // 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 @@ -167,6 +189,7 @@ describe('ContributionForm.vue', () => { title: 'dies ist ein Post', content: 'auf Deutsch geschrieben', language: 'de', + imageUpload, }, } wrapper = Wrapper() @@ -188,10 +211,6 @@ describe('ContributionForm.vue', () => { expect(wrapper.vm.form.content).toEqual(propsData.contribution.content) }) - it('sets language equal to contribution language', () => { - expect(wrapper.vm.form.language).toEqual({ value: propsData.contribution.language }) - }) - it('calls the UpdatePost apollo mutation', async () => { expectedParams = { mutation: PostMutations().UpdatePost, @@ -200,6 +219,7 @@ describe('ContributionForm.vue', () => { content: postContent, language: propsData.contribution.language, id: propsData.contribution.id, + imageUpload, }, } postTitleInput = wrapper.find('.ds-input') diff --git a/webapp/components/ContributionForm/index.vue b/webapp/components/ContributionForm/index.vue index c925a6dca..57eb105be 100644 --- a/webapp/components/ContributionForm/index.vue +++ b/webapp/components/ContributionForm/index.vue @@ -2,6 +2,13 @@ @@ -50,10 +58,12 @@ import HcEditor from '~/components/Editor' import orderBy from 'lodash/orderBy' import locales from '~/locales' import PostMutations from '~/graphql/PostMutations.js' +import HcTeaserImage from '~/components/TeaserImage/TeaserImage' export default { components: { HcEditor, + HcTeaserImage, }, props: { contribution: { type: Object, default: () => {} }, @@ -63,6 +73,7 @@ export default { form: { title: '', content: '', + teaserImage: null, language: null, languageOptions: [], }, @@ -88,7 +99,7 @@ export default { this.slug = contribution.slug this.form.content = contribution.content this.form.title = contribution.title - this.form.language = { value: contribution.language } + this.form.teaserImage = contribution.imageUpload }, }, }, @@ -106,15 +117,25 @@ export default { }, methods: { submit() { + const { title, content, teaserImage } = this.form + let language + if (this.form.language) { + language = this.form.language.value + } else if (this.contribution && this.contribution.language) { + language = this.contribution.language + } else { + language = this.$i18n.locale() + } this.loading = true this.$apollo .mutate({ mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost, variables: { id: this.id, - title: this.form.title, - content: this.form.content, - language: this.form.language ? this.form.language.value : this.$i18n.locale(), + title, + content, + language, + imageUpload: teaserImage, }, }) .then(res => { @@ -144,6 +165,9 @@ export default { this.form.languageOptions.push({ label: locale.name, value: locale.code }) }) }, + addTeaserImage(file) { + this.form.teaserImage = file + }, }, apollo: { User: { @@ -176,8 +200,4 @@ export default { padding-right: 0; } } - -.contribution-form-footer { - border-top: $border-size-base solid $border-color-softest; -} diff --git a/webapp/components/TeaserImage/TeaserImage.spec.js b/webapp/components/TeaserImage/TeaserImage.spec.js new file mode 100644 index 000000000..07b17e16b --- /dev/null +++ b/webapp/components/TeaserImage/TeaserImage.spec.js @@ -0,0 +1,61 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import TeaserImage from './TeaserImage.vue' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('TeaserImage.vue', () => { + let wrapper + let mocks + + beforeEach(() => { + mocks = { + $toast: { + error: jest.fn(), + }, + } + }) + describe('mount', () => { + const Wrapper = () => { + return mount(TeaserImage, { mocks, localVue }) + } + beforeEach(() => { + wrapper = Wrapper() + }) + + describe('File upload', () => { + const imageUpload = [ + { file: { filename: 'avataar.svg', previewElement: '' }, url: 'someUrlToImage' }, + ] + + it('supports adding a teaser image', () => { + wrapper.vm.addTeaserImage(imageUpload) + expect(wrapper.emitted().addTeaserImage[0]).toEqual(imageUpload) + }) + }) + + describe('handles errors', () => { + beforeEach(() => jest.useFakeTimers()) + const message = 'File upload failed' + const fileError = { status: 'error' } + + it('defaults to error false', () => { + expect(wrapper.vm.error).toEqual(false) + }) + + it('shows an error toaster when verror is called', () => { + wrapper.vm.verror(fileError, message) + expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message) + }) + + it('changes error status from false to true to false', () => { + wrapper.vm.verror(fileError, message) + expect(wrapper.vm.error).toEqual(true) + jest.runAllTimers() + expect(wrapper.vm.error).toEqual(false) + }) + }) + }) +}) diff --git a/webapp/components/TeaserImage/TeaserImage.vue b/webapp/components/TeaserImage/TeaserImage.vue new file mode 100644 index 000000000..cb657fe9a --- /dev/null +++ b/webapp/components/TeaserImage/TeaserImage.vue @@ -0,0 +1,197 @@ + + + + diff --git a/webapp/components/Upload/index.vue b/webapp/components/Upload/index.vue index f7f730632..3f84f8a7c 100644 --- a/webapp/components/Upload/index.vue +++ b/webapp/components/Upload/index.vue @@ -9,7 +9,7 @@ @vdropzone-error="verror" >
- +
@@ -22,12 +22,10 @@