From ed2b964236994ccaec3c5fe9eb71d0bae1022a3b Mon Sep 17 00:00:00 2001 From: karabij Date: Fri, 2 Dec 2022 17:22:16 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20meeting=20register?= =?UTF-8?q?=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add a form which enables the user to register the meeting. On success, the meeting is saved to local storage. Since changes to local storage do not cause react to rerender anything, you need to refresh the page for the meeting to appear in the meeting list view. --- .../demo/src/views/meetings/list/index.tsx | 10 +- .../FormikDateTimePicker.tsx | 16 +- .../FormikDateTimePicker/SuggestionButton.tsx | 1 - .../Formik/FormikDateTimePicker/utils.tsx | 10 +- .../meetings/MeetingRow/MeetingRow.tsx | 33 ++- .../MyMeetings/MyMeetings.stories.tsx | 10 +- .../RegisterMeetingForm.tsx | 215 +++++++++--------- .../magnify/src/factories/meetings/index.ts | 14 +- .../src/i18n/Messages/formLabelMessages.ts | 4 +- .../src/types/entities/meeting/index.ts | 5 +- 10 files changed, 167 insertions(+), 151 deletions(-) diff --git a/src/frontend/demo/src/views/meetings/list/index.tsx b/src/frontend/demo/src/views/meetings/list/index.tsx index ac5ce0565..d25abe487 100644 --- a/src/frontend/demo/src/views/meetings/list/index.tsx +++ b/src/frontend/demo/src/views/meetings/list/index.tsx @@ -3,9 +3,15 @@ import * as React from 'react'; import { DefaultPage } from '../../../components/DefaultPage'; export function MeetingsListView() { + const myMeetings: string | null = localStorage.getItem('meetings'); + const myMeetingsList: any = myMeetings ? JSON.parse(myMeetings) : []; + const mySortedMeetingsList = myMeetingsList.sort( + (a: any, b: any) => new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime(), + ); + return ( - - + + ); } diff --git a/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/FormikDateTimePicker.tsx b/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/FormikDateTimePicker.tsx index acdae858c..5d86090a4 100644 --- a/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/FormikDateTimePicker.tsx +++ b/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/FormikDateTimePicker.tsx @@ -6,6 +6,7 @@ import React, { FunctionComponent, useState } from 'react'; import { useIntl } from 'react-intl'; import TimePicker, { TimePickerValue } from 'react-time-picker'; import SuggestionButton from './SuggestionButton'; +import { mergeDateTime } from './utils'; export interface formikDateTimePickerProps { dateName: string; @@ -59,7 +60,7 @@ const FormikDateTimePicker: FunctionComponent = ({ .. return; } formikContext.setFieldTouched(props.timeName, true); - }, [timeField.value]); + }, [timeField.value, dateField.value]); const suggestionButtons = props.localTimeSuggestions.map((value: string, index: number) => ( = ({ .. )}
- + = ({ .. size: 'small', }} > - = ({ .. + { + return ( + + {msg} + + ); + }} + />
diff --git a/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/SuggestionButton.tsx b/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/SuggestionButton.tsx index deb5c46dc..553570368 100644 --- a/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/SuggestionButton.tsx +++ b/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/SuggestionButton.tsx @@ -31,7 +31,6 @@ const SuggestionButton: FunctionComponent = ({ ...props } primary={isChosenButton} onClick={() => { props.onClick(props.frenchButtonValue); - console.log(`chosenDateTime : ${chosenDateTime}`); }} > diff --git a/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/utils.tsx b/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/utils.tsx index 80ce64964..748bf10c8 100644 --- a/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/utils.tsx +++ b/src/frontend/magnify/src/components/design-system/Formik/FormikDateTimePicker/utils.tsx @@ -12,11 +12,11 @@ export const splitDateTime = (dateTimeISO: string | null): { date: string; time: }; export const mergeDateTime = ( - dateString: string | null, - timeString: string | null, -): string | null => { + dateString: string | undefined, + timeString: string | undefined, +): string | undefined => { if (!dateString || !timeString) { - return null; + return undefined; } try { const time = Duration.fromISOTime(timeString); @@ -26,6 +26,6 @@ export const mergeDateTime = ( }); return dateTime.toISO(); } catch (e) { - return null; + return undefined; } }; diff --git a/src/frontend/magnify/src/components/meetings/MeetingRow/MeetingRow.tsx b/src/frontend/magnify/src/components/meetings/MeetingRow/MeetingRow.tsx index 7ba63cacb..8d2775b31 100644 --- a/src/frontend/magnify/src/components/meetings/MeetingRow/MeetingRow.tsx +++ b/src/frontend/magnify/src/components/meetings/MeetingRow/MeetingRow.tsx @@ -1,12 +1,12 @@ import { defineMessage } from '@formatjs/intl'; import { Box, Button, ButtonExtendedProps, Card, Menu, Notification, Spinner, Text } from 'grommet'; import { MoreVertical } from 'grommet-icons'; +import { Interval } from 'luxon'; import React from 'react'; import { useIntl } from 'react-intl'; import { useIsSmallSize } from '../../../hooks/useIsMobile'; import { Meeting } from '../../../types/entities/meeting'; -import { Room } from '../../../types/entities/room'; export interface MeetingRowProps { meeting: Meeting; @@ -24,24 +24,23 @@ export default function MeetingRow({ meeting }: MeetingRowProps) { const intl = useIntl(); const isSmallSize = useIsSmallSize(); const menuItems: ButtonExtendedProps[] = []; + const startDateTime: Date = new Date(meeting.startDateTime); + const endDateTime: Date = new Date(meeting.endDateTime); - const convertToHourMinutesFormat = (numberMinutes: number): string => { - const nbHours = Math.floor(numberMinutes / 60); - const nbMinutes = numberMinutes - 60 * nbHours; - return nbHours > 0 ? `${nbHours} h ${nbMinutes} min` : `${nbMinutes} min`; - }; + const meetingDay = startDateTime.toLocaleDateString(intl.locale, { + year: '2-digit', + month: '2-digit', + day: '2-digit', + }); - const zeroFormatNumber = (number: number): string => { - return number < 10 ? '0' + number.toString() : number.toString(); - }; + const meetingHour = startDateTime.toLocaleTimeString(intl.locale, { + timeStyle: 'short', + }); - const meetingDay = `${zeroFormatNumber(meeting.startDateTime.getDay())}/${zeroFormatNumber( - meeting.startDateTime.getMonth() + 1, - )}/${meeting.startDateTime.getFullYear()}`; - - const meetingHour = `${zeroFormatNumber(meeting.startDateTime.getHours())}:${zeroFormatNumber( - meeting.startDateTime.getMinutes(), - )}`; + const expectedDuration = Interval.fromDateTimes(startDateTime, endDateTime).toDuration([ + 'hours', + 'minutes', + ]); return ( @@ -73,7 +72,7 @@ export default function MeetingRow({ meeting }: MeetingRowProps) { {meetingHour} - {convertToHourMinutesFormat(meeting.expectedDuration)} + {`${expectedDuration.hours}h ${Math.floor(expectedDuration.minutes)}min`} diff --git a/src/frontend/magnify/src/components/meetings/MyMeetings/MyMeetings.stories.tsx b/src/frontend/magnify/src/components/meetings/MyMeetings/MyMeetings.stories.tsx index 683c242d2..df8b7fab8 100644 --- a/src/frontend/magnify/src/components/meetings/MyMeetings/MyMeetings.stories.tsx +++ b/src/frontend/magnify/src/components/meetings/MyMeetings/MyMeetings.stories.tsx @@ -1,6 +1,6 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import { createRandomMeeting } from '../../../factories/meetings'; +import { Meeting } from '../../../types'; import { MyMeetings } from './MyMeetings'; export default { @@ -8,8 +8,14 @@ export default { component: MyMeetings, } as ComponentMeta; +const myMeetings: string | null = localStorage.getItem('meetings'); +const myMeetingsList: Meeting[] = myMeetings ? JSON.parse(myMeetings) : []; +const mySortedMeetingsList = myMeetingsList.sort( + (a, b) => new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime(), +); + const Template: ComponentStory = (args) => ( - + ); // create the template and stories diff --git a/src/frontend/magnify/src/components/meetings/RegisterMeetingForm/RegisterMeetingForm.tsx b/src/frontend/magnify/src/components/meetings/RegisterMeetingForm/RegisterMeetingForm.tsx index 1bc966667..c16a05ee4 100644 --- a/src/frontend/magnify/src/components/meetings/RegisterMeetingForm/RegisterMeetingForm.tsx +++ b/src/frontend/magnify/src/components/meetings/RegisterMeetingForm/RegisterMeetingForm.tsx @@ -1,16 +1,14 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; -import { Form, Formik, FormikHelpers } from 'formik'; +import { faker } from '@faker-js/faker'; +import { Form, Formik } from 'formik'; import { Box } from 'grommet'; import { DateTime, Settings } from 'luxon'; import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import * as Yup from 'yup'; +import { AnyObject, Assign, ObjectShape, TypeOfShape } from 'yup/lib/object'; +import { RequiredStringSchema } from 'yup/lib/string'; import { formLabelMessages } from '../../../i18n/Messages/formLabelMessages'; -import { MeetingsRepository } from '../../../services'; import { Meeting } from '../../../types'; -import { Maybe } from '../../../types/misc'; -import { MagnifyQueryKeys } from '../../../utils/constants/react-query'; import FormikDateTimePicker from '../../design-system/Formik/FormikDateTimePicker'; import { mergeDateTime } from '../../design-system/Formik/FormikDateTimePicker/utils'; import FormikInput from '../../design-system/Formik/Input'; @@ -34,9 +32,8 @@ const messages = defineMessages({ description: 'Label for the submit button to register a new meeting', }, invalidTime: { - defaultMessage: - 'Starting time should be in the future and ending time should be after starting time.', - description: 'Error message when event scheduling date time update is in the past.', + defaultMessage: 'Invalid time inputs', + description: 'Error message when time inputs are invalid.', id: 'components.design-system.Formik.FormikDateTimePicker.invalidTime', }, }); @@ -57,74 +54,55 @@ export interface RegisterMeetingFormValues { endTime: string | undefined; } -interface FormErrors { - slug?: string[]; +interface meetingFormValidationSchema { + name: RequiredStringSchema; + startDate: RequiredStringSchema; + endDate: RequiredStringSchema; + startTime: RequiredStringSchema; + endTime: RequiredStringSchema; } +// interface FormErrors { +// slug?: string[]; +// } + const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { const intl = useIntl(); Settings.defaultLocale = intl.locale; - const startTimeTestOptions: Yup.TestConfig = { - name: 'startDateTimeIsAfterOrNow', - test: function (startTimeValue: string | undefined) { - const nullableStartTimeValue = startTimeValue == undefined ? null : startTimeValue; - const chosenStartDateTime = this.parent - ? mergeDateTime(this.parent.startDate, nullableStartTimeValue) - : null; - const chosenEndDateTime = this.parent - ? mergeDateTime(this.parent.endDate, this.parent.endTime) - : null; - return ( - chosenStartDateTime == null || + const globalDateInputValidation: Yup.TestConfig< + TypeOfShape>, + AnyObject + > = { + name: 'dateInputsShouldBeValid', + test: function (values, context) { + const chosenStartDateTime = mergeDateTime(values['startDate'], values['startTime']); + const chosenEndDateTime = mergeDateTime(values['endDate'], values['endTime']); + if ( + chosenStartDateTime == undefined || (chosenStartDateTime >= DateTime.local().toISO() && - (chosenEndDateTime == null || chosenStartDateTime <= chosenEndDateTime)) - ); - }, - message: intl.formatMessage(messages.invalidTime), - exclusive: true, - }; - - const endTimeTestOptions: Yup.TestConfig = { - name: 'endDateTimeIsAfterOrStartDate', - test: function (endTimeValue: string | undefined) { - const nullableEndTimeValue = endTimeValue == undefined ? null : endTimeValue; - const chosenStartDateTime = this.parent - ? mergeDateTime(this.parent.startDate, this.parent.startTime) - : null; - const chosenEndDateTime = this.parent - ? mergeDateTime(this.parent.endDate, nullableEndTimeValue) - : null; - return ( - chosenEndDateTime == null || - (chosenEndDateTime >= DateTime.local().toISO() && - (chosenStartDateTime == null || chosenStartDateTime <= chosenEndDateTime)) - ); + (chosenEndDateTime == undefined || chosenStartDateTime <= chosenEndDateTime)) + ) + return true; + else { + return context.createError({ + path: 'startDate', + message: intl.formatMessage(messages.invalidTime), + }); + } }, - message: intl.formatMessage(messages.invalidTime), exclusive: true, }; - const validationSchema = Yup.object().shape({ - name: Yup.string().required(), - startDate: Yup.string().required(), - endDate: Yup.string().required(), - startTime: Yup.string().required().test(startTimeTestOptions), - endTime: Yup.string().required().test(endTimeTestOptions), - }); - const queryClient = useQueryClient(); - - const mutation = useMutation( - MeetingsRepository.create, - { - onSuccess: (newMeeting) => { - queryClient.setQueryData([MagnifyQueryKeys.MEETINGS], (meetings: Meeting[] = []) => { - return [...meetings, newMeeting]; - }); - onSuccess(newMeeting); - }, - }, - ); + const validationSchema = Yup.object() + .shape({ + name: Yup.string().required(), + startDate: Yup.string().required(), + endDate: Yup.string().required(), + startTime: Yup.string().required(), + endTime: Yup.string().required(), + }) + .test(globalDateInputValidation); const allSuggestions = getSuggestions(intl.locale); const allFrenchSuggestions = getSuggestions('fr'); @@ -140,18 +118,36 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { [], ); - const handleSubmit = ( - values: RegisterMeetingFormValues, - actions: FormikHelpers, - ) => { - mutation.mutate(values, { - onError: (error) => { - const formErrors = error?.response?.data as Maybe; - if (formErrors?.slug) { - actions.setFieldError('name', formErrors.slug.join(',')); - } - }, - }); + const handleSubmit = (values: RegisterMeetingFormValues) => { + try { + const oldMeetings: string | null = localStorage.getItem('meetings'); + const id = faker.datatype.uuid(); + const startDateTime = mergeDateTime(values.startDate, values.startTime); + const endDateTime = mergeDateTime(values.endDate, values.endTime); + + const newMeeting: Meeting = { + id: id, + name: values.name, + startDateTime: startDateTime + ? DateTime.fromISO(startDateTime).toUTC().toISO() + : DateTime.now().toUTC().toISO(), + endDateTime: endDateTime + ? DateTime.fromISO(endDateTime).toUTC().toISO() + : DateTime.now().toUTC().toISO(), + jitsi: { + room: `${id}`, + token: `${faker.datatype.number({ min: 0, max: 1000 })}`, + }, + }; + if (oldMeetings) { + localStorage.setItem('meetings', JSON.stringify([...JSON.parse(oldMeetings), newMeeting])); + } else { + localStorage.setItem('meetings', JSON.stringify([newMeeting])); + } + onSuccess(newMeeting); + } catch (error) { + console.log(error); + } }; return ( @@ -160,43 +156,36 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { onSubmit={handleSubmit} validationSchema={validationSchema} > - {({ errors, touched }) => ( -
- - - - - - + + + + + + + - {errors.startDate && touched.startDate ?
{errors.startDate}
: null} - {touched.endDate && errors.endDate ?
{errors.endDate}
: null} - {touched.startTime && errors.startTime ?
{errors.startTime}
: null} - {touched.endTime && errors.endTime ?
{errors.endTime}
: null} - - - - )} + + ); }; diff --git a/src/frontend/magnify/src/factories/meetings/index.ts b/src/frontend/magnify/src/factories/meetings/index.ts index 476e2eefa..a7148e233 100644 --- a/src/frontend/magnify/src/factories/meetings/index.ts +++ b/src/frontend/magnify/src/factories/meetings/index.ts @@ -5,15 +5,21 @@ import { Meeting, defaultRecurrenceConfiguration } from '../../types/entities/me export const createRandomMeeting = (isReccurent = false, room?: Room): Meeting => { const id = faker.datatype.uuid(); const name = faker.lorem.slug(); - const startDate = faker.date.between(new Date().toLocaleDateString(), '2030-01-01T00:00:00.000Z'); - const duration = faker.random.numeric(2); + const startDateTime = faker.date.between( + new Date().toLocaleDateString(), + '2030-01-01T00:00:00.000Z', + ); + const maxEndDateTime = new Date(startDateTime); + maxEndDateTime.setHours(maxEndDateTime.getHours() + 5); + + const endDateTime = faker.date.between(startDateTime, maxEndDateTime); return { id: id, name: name, room: room, - startDateTime: startDate, - expectedDuration: Number(duration), + startDateTime: startDateTime.toISOString(), + endDateTime: endDateTime.toISOString(), jitsi: { room: room ? `${room.slug}-${id}` : `${id}`, token: '456', diff --git a/src/frontend/magnify/src/i18n/Messages/formLabelMessages.ts b/src/frontend/magnify/src/i18n/Messages/formLabelMessages.ts index 58a716c35..9ddd2b8d7 100644 --- a/src/frontend/magnify/src/i18n/Messages/formLabelMessages.ts +++ b/src/frontend/magnify/src/i18n/Messages/formLabelMessages.ts @@ -9,11 +9,11 @@ export const formLabelMessages = defineMessages({ meetingStartDateTime: { id: 'i18n.Messages.formLabelMessages.meetingStartDateTime', description: 'Meeting Start Date Time for input', - defaultMessage: 'Meeting start date and hour', + defaultMessage: 'Meeting start', }, meetingEndDateTime: { id: 'i18n.Messages.formLabelMessages.meetingEndDateTime', description: 'Meeting End Date Time for input', - defaultMessage: 'Meeting end date and hour', + defaultMessage: 'Meeting end', }, }); diff --git a/src/frontend/magnify/src/types/entities/meeting/index.ts b/src/frontend/magnify/src/types/entities/meeting/index.ts index 9f23ef10a..89d50b24a 100644 --- a/src/frontend/magnify/src/types/entities/meeting/index.ts +++ b/src/frontend/magnify/src/types/entities/meeting/index.ts @@ -16,8 +16,9 @@ export interface Meeting { id: string; name: string; room?: Room; - startDateTime: Date; - expectedDuration: number; + // start and end DateTime will be in the ISO 8601 format with UTC time zone + startDateTime: string; + endDateTime: string; jitsi: { room: string; token: string;