diff --git a/backend/src/schema/resolvers/helpers/events.js b/backend/src/schema/resolvers/helpers/events.js index 84e64299d..835088d8c 100644 --- a/backend/src/schema/resolvers/helpers/events.js +++ b/backend/src/schema/resolvers/helpers/events.js @@ -1,29 +1,32 @@ import { UserInputError } from 'apollo-server' export const validateEventParams = (params) => { + let locationName = null if (params.postType && params.postType === 'Event') { const { eventInput } = params validateEventDate(eventInput.eventStart) params.eventStart = eventInput.eventStart + if (eventInput.eventEnd) { validateEventEnd(eventInput.eventStart, eventInput.eventEnd) params.eventEnd = eventInput.eventEnd + } else { + params.eventEnd = null } + if (eventInput.eventLocationName && !eventInput.eventVenue) { throw new UserInputError('Event venue must be present if event location is given!') } params.eventVenue = eventInput.eventVenue - params.eventLocationName = eventInput.eventLocationName + params.eventLocationName = eventInput.eventLocationName && eventInput.eventLocationName.trim() + if (params.eventLocationName) { + locationName = params.eventLocationName + } else { + params.eventLocationName = null + } params.eventIsOnline = !!eventInput.eventIsOnline } delete params.eventInput - let locationName - if (params.eventLocationName) { - locationName = params.eventLocationName - } else { - params.eventLocationName = null - locationName = null - } return locationName } diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index 6b9db448b..9095665fc 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils' import ContributionForm from './ContributionForm.vue' +import PostMutations from '~/graphql/PostMutations.js' import Vuex from 'vuex' -import PostMutations from '~/graphql/PostMutations.js' import ImageUploader from '~/components/Uploader/ImageUploader' import MutationObserver from 'mutation-observer' @@ -108,6 +108,10 @@ describe('ContributionForm.vue', () => { await wrapper.vm.updateEditorContent(postContent) }) + it('has no event data block', () => { + expect(wrapper.find('div.eventDatas').exists()).toBe(false) + }) + it('title cannot be empty', async () => { postTitleInput.setValue('') wrapper.find('form').trigger('submit') @@ -293,5 +297,88 @@ describe('ContributionForm.vue', () => { }) }) }) + + describe('Events', () => { + beforeEach(() => { + propsData.createEvent = true + wrapper = Wrapper() + }) + + it('has event data block', () => { + expect(wrapper.find('div.eventDatas').exists()).toBe(true) + }) + + describe('is online event', () => { + it('has false as default', () => { + expect(wrapper.vm.formData.eventIsOnline).toBe(false) + }) + + it('has input for event location', () => { + expect(wrapper.find('input[name="eventLocationName"]').exists()).toBe(true) + }) + + describe('click is online event', () => { + beforeEach(() => { + wrapper.find('input[name="eventIsOnline"]').setChecked(true) + }) + + it('has no input for event location', () => { + expect(wrapper.find('input[name="eventLocationName"]').exists()).toBe(false) + }) + }) + + describe('invalid form', () => { + beforeEach(() => { + wrapper.find('input[name="title"]').setValue('Illegaler Kindergeburtstag') + wrapper.vm.updateEditorContent('Elli hat Geburtstag!') + }) + + it('has submit button disabled', () => { + expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled') + }) + }) + + describe('valid form', () => { + const now = new Date() + + beforeEach(() => { + wrapper.find('input[name="title"]').setValue('Illegaler Kindergeburtstag') + wrapper.vm.updateEditorContent('Elli hat Geburtstag!') + wrapper + .findComponent({ name: 'DatePicker' }) + .vm.$emit('change', new Date(now.getFullYear(), now.getMonth() + 1).toISOString()) + wrapper.find('input[name="eventVenue"]').setValue('Ellis Kinderzimmer') + wrapper.find('input[name="eventLocationName"]').setValue('Deutschland') + }) + + it('has submit button not disabled', () => { + expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe(undefined) + }) + + describe('submit', () => { + beforeEach(() => { + wrapper.find('form').trigger('submit') + }) + + it('calls create post', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledWith({ + mutation: PostMutations().CreatePost, + variables: expect.objectContaining({ + title: 'Illegaler Kindergeburtstag', + content: 'Elli hat Geburtstag!', + eventInput: { + eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(), + eventVenue: 'Ellis Kinderzimmer', + eventLocationName: 'Deutschland', + eventIsOnline: false, + eventEnd: null, + }, + }), + }) + }) + }) + }) + }) + }) }) }) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index 997a25341..0067dab72 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -54,13 +54,13 @@ -
+

- + -
+
-
+
- +
- + - - + +
@@ -113,10 +116,10 @@
- +
@@ -131,9 +134,11 @@
{{ $t('post.viewEvent.eventIsOnline') }}
@@ -153,7 +158,7 @@ - + null, }, - creatEvent: { + createEvent: { type: Boolean, default: false, }, @@ -252,24 +257,6 @@ export default { eventVenue: eventVenue || '', eventIsOnline: eventIsOnline || false, }, - formSchema: { - title: { required: true, min: 3, max: 100 }, - content: { required: true }, - imageBlurred: { required: false }, - categoryIds: { - type: 'array', - required: this.categoriesActive, - validator: (_, value = []) => { - if (this.categoriesActive && (value.length === 0 || value.length > 3)) { - return [new Error(this.$t('common.validations.categories'))] - } - return [] - }, - }, - eventStart: { required: !!this.creatEvent }, - eventVenue: { required: !!this.creatEvent, min: 3, max: 100 }, - eventLocationName: { required: !!this.creatEvent, min: 3, max: 100 }, - }, loading: false, users: [], hashtags: [], @@ -283,14 +270,69 @@ export default { ...mapGetters({ currentUser: 'auth/user', }), + formSchema() { + return { + title: { required: true, min: 3, max: 100 }, + content: { required: true }, + imageBlurred: { required: false }, + categoryIds: { + type: 'array', + required: this.categoriesActive, + validator: (_, value = []) => { + if (this.categoriesActive && (value.length === 0 || value.length > 3)) { + return [new Error(this.$t('common.validations.categories'))] + } + return [] + }, + }, + eventStart: { required: !!this.createEvent }, + eventVenue: { + required: !!this.createEvent, + min: 3, + max: 100, + validator: (_, value = '') => { + if (!this.createEvent) return [] + if (!value.trim()) { + return [new Error(this.$t('common.validations.eventVenueNotEmpty'))] + } + if (value.length < 3 || value.length > 100) { + return [ + new Error(this.$t('common.validations.eventVenueLength', { min: 3, max: 100 })), + ] + } + return [] + }, + }, + eventLocationName: { + required: !!this.createEvent && !this.formData.eventIsOnline, + min: 3, + max: 100, + validator: (_, value = '') => { + if (!this.createEvent) return [] + if (this.formData.eventIsOnline) return [] + if (!value.trim()) { + return [new Error(this.$t('common.validations.eventLocationNameNotEmpty'))] + } + if (value.length < 3 || value.length > 100) { + return [ + new Error( + this.$t('common.validations.eventLocationNameLength', { min: 3, max: 100 }), + ), + ] + } + return [] + }, + }, + } + }, eventInput() { - if (this.creatEvent) { + if (this.createEvent) { return { eventStart: this.formData.eventStart, eventVenue: this.formData.eventVenue, eventEnd: this.formData.eventEnd, eventIsOnline: this.formData.eventIsOnline, - eventLocationName: this.formData.eventLocationName, + eventLocationName: !this.formData.eventIsOnline ? this.formData.eventLocationName : null, } } return undefined @@ -310,6 +352,9 @@ export default { groupCategories() { return this.group && this.group.categories }, + showEventLocationName() { + return !this.formData.eventIsOnline + }, }, watch: { groupCategories() { @@ -356,7 +401,7 @@ export default { id: this.contribution.id || null, image, groupId: this.groupId, - postType: !this.creatEvent ? 'Article' : 'Event', + postType: !this.createEvent ? 'Article' : 'Event', eventInput: this.eventInput, }, }) @@ -378,6 +423,15 @@ export default { updateEditorContent(value) { this.$refs.contributionForm.update('content', value) }, + changeEventIsOnline(event) { + this.$refs.contributionForm.update('eventIsOnline', this.formData.eventIsOnline) + }, + changeEventEnd(event) { + this.$refs.contributionForm.update('eventEnd', event) + }, + changeEventStart(event) { + this.$refs.contributionForm.update('eventStart', event) + }, addHeroImage(file) { this.formData.image = null if (file) { @@ -443,6 +497,24 @@ export default { margin-top: -10px; } } + // style override to handle dynamic inputs + .event-location-grid { + grid-template-columns: repeat(2, 1fr) !important; + } + + .event-grid-item { + // important needed because of component inline style + grid-row-end: span 3 !important; + } + .event-grid-item-z-helper { + z-index: 20; + } + .event-grid-item-margin-helper { + margin-top: 10px; + } + .event-grid-item-font-helper { + font-size: larger; + } } .contribution-form > .base-card { @@ -491,6 +563,12 @@ export default { min-width: fit-content; } } + + > .buttons-footer-helper { + margin-right: 16px; + // important needed because of component inline style + margin-bottom: 6px !important; + } } .blur-toggle { diff --git a/webapp/graphql/PostMutations.js b/webapp/graphql/PostMutations.js index 6b71b86b9..c3a9369c3 100644 --- a/webapp/graphql/PostMutations.js +++ b/webapp/graphql/PostMutations.js @@ -61,6 +61,8 @@ export default () => { $content: String! $image: ImageInput $categoryIds: [ID] + $postType: PostType + $eventInput: _EventInput ) { UpdatePost( id: $id @@ -68,6 +70,8 @@ export default () => { content: $content image: $image categoryIds: $categoryIds + postType: $postType + eventInput: $eventInput ) { id title @@ -85,6 +89,14 @@ export default () => { name role } + postType + eventStart + eventLocationName + eventVenue + eventLocation { + lng + lat + } } } `, diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 5f36d293b..1ce7d2f61 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -115,6 +115,10 @@ "validations": { "categories": "es müssen eine bis drei Themen ausgewählt werden", "email": "muss eine gültige E-Mail-Adresse sein", + "eventLocationNameLength": "Minimum {min}, Maximum {max} Zeichen", + "eventLocationNameNotEmpty": "es dürfen nicht nur Freizeichen eingetragen werden", + "eventVenueLength": "Minimum {min}, Maximum {max} Zeichen", + "eventVenueNotEmpty": "es dürfen nicht nur Freizeichen eingetragen werden", "url": "muss eine gültige URL sein" }, "versus": "Versus" @@ -726,6 +730,7 @@ }, "edited": "bearbeitet", "editPost": { + "event": "Bearbeite deine Veranstaltung", "forGroup": { "title": "Für die Gruppe „{name}“" }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 0719d8cbf..f25cc8575 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -115,6 +115,10 @@ "validations": { "categories": "at least one and at most three topics must be selected", "email": "must be a valid e-mail address", + "eventLocationNameLength": "minimum {min} or maximum {max} characters", + "eventLocationNameNotEmpty": "only empty characters are not allowed", + "eventVenueLength": "minimum {min} or maximum {max} characters", + "eventVenueNotEmpty": "only empty characters are not allowed", "url": "must be a valid URL" }, "versus": "Versus" @@ -722,10 +726,11 @@ "forGroup": { "title": "For The Group “{name}”" }, - "title": "Create A New Post" + "title": "Create A New Article" }, "edited": "edited", "editPost": { + "event": "Edit Your Event", "forGroup": { "title": "For The Group “{name}”" }, diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index 743d58b54..4e6476220 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -64,22 +64,41 @@ {{ post.eventVenue }} - - + {{ post.eventLocationName }} - - + {{ $t('post.viewEvent.eventIsOnline') }} - - {{ getEventDateString }} +
+
+ + {{ getEventStartDateString }} +
+
+ + {{ getEventStartTimeString }} +
+
+
 — 
+
+
+ + {{ getEventEndDateString }} +
+
+ + {{ getEventEndTimeString }} +
+
- + @@ -225,7 +244,10 @@ export default { const { slug, id } = this.$route.params return [ { - name: this.$t('common.post', null, 1), + name: + this.post?.postType[0] === 'Event' + ? this.$t('post.viewEvent.title') + : this.$t('post.viewPost.title'), path: `/post/${id}/${slug}`, children: [ { @@ -288,26 +310,17 @@ export default { !this.post.group || (this.group && ['usual', 'admin', 'owner'].includes(this.group.myRole)) ) }, - getEventDateString() { - if (this.post.eventEnd) { - const eventStart = format(new Date(this.post.eventStart), 'dd.MM.') - const eventEnd = format(new Date(this.post.eventEnd), 'dd.MM.yyyy') - return `${eventStart} - ${eventEnd}` - } else { - return format(new Date(this.post.eventStart), 'dd.MM.yyyy') - } + getEventStartDateString() { + return format(new Date(this.post.eventStart), 'dd.MM.yyyy') }, - getEventTimeString() { - if (this.post.eventEnd) { - const eventStartTime = format(new Date(this.post.eventStart), 'HH:mm') - const eventEndTime = format(new Date(this.post.eventEnd), 'HH:mm') - /* assumption that if e.g. 00:00 == 00:00 is saved, - it's not realistic because they are the default values, so don't show the time info. - */ - return eventStartTime !== eventEndTime ? `${eventStartTime} - ${eventEndTime}` : '' - } else { - return format(new Date(this.post.eventStart), 'HH:mm') - } + getEventStartTimeString() { + return format(new Date(this.post.eventStart), 'HH:mm') + }, + getEventEndDateString() { + return this.post.eventEnd ? format(new Date(this.post.eventEnd), 'dd.MM.yyyy') : '' + }, + getEventEndTimeString() { + return this.post.eventEnd ? format(new Date(this.post.eventEnd), 'HH:mm') : '' }, }, methods: { diff --git a/webapp/pages/post/create.vue b/webapp/pages/post/create.vue index 199cc14a4..cc4fb828a 100644 --- a/webapp/pages/post/create.vue +++ b/webapp/pages/post/create.vue @@ -1,13 +1,13 @@ @@ -69,7 +67,7 @@ export default { const { groupId = null } = this.$route.query return { groupId, - creatEvent: false, + createEvent: false, } }, computed: { @@ -98,6 +96,11 @@ export default { fetchPolicy: 'cache-and-network', }, }, + methods: { + switchPostType() { + this.createEvent = !this.createEvent + }, + }, } diff --git a/webapp/pages/post/edit/_id.spec.js b/webapp/pages/post/edit/_id.spec.js index aa25eb8e8..2773483a4 100644 --- a/webapp/pages/post/edit/_id.spec.js +++ b/webapp/pages/post/edit/_id.spec.js @@ -29,7 +29,12 @@ describe('post/_id.vue', () => { defaultClient: { query: jest.fn().mockResolvedValue({ data: { - Post: [{ author: { id: authorId } }], + Post: [ + { + author: { id: authorId }, + postType: ['Article'], + }, + ], }, }), }, diff --git a/webapp/pages/post/edit/_id.vue b/webapp/pages/post/edit/_id.vue index 664c55219..04d120c50 100644 --- a/webapp/pages/post/edit/_id.vue +++ b/webapp/pages/post/edit/_id.vue @@ -1,7 +1,13 @@