From 23fc6c1b06e6b5dfeaf8bc2076b2b44a34ee5ac1 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 | 7 +- .../FormikDateTimePicker/SuggestionButton.tsx | 1 - .../Formik/FormikDateTimePicker/utils.tsx | 10 +- .../meetings/MeetingRow/MeetingRow.tsx | 33 +++--- .../MyMeetings/MyMeetings.stories.tsx | 10 +- .../RegisterMeetingForm.tsx | 109 +++++++++++++----- .../magnify/src/factories/meetings/index.ts | 14 ++- .../src/types/entities/meeting/index.ts | 5 +- 9 files changed, 133 insertions(+), 66 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..442452f43 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; @@ -58,8 +59,12 @@ const FormikDateTimePicker: FunctionComponent = ({ .. if (timeField.value === undefined || timeField.value.length === 0) { return; } + const chosen = mergeDateTime(dateField.value, timeField.value); + if (chosen) { + console.log(`chosenDateTime in UTC format: ${DateTime.fromISO(chosen).toUTC().toISO()}`); + } formikContext.setFieldTouched(props.timeName, true); - }, [timeField.value]); + }, [timeField.value, dateField.value]); const suggestionButtons = props.localTimeSuggestions.map((value: string, index: number) => ( = ({ ...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..857b2d104 100644 --- a/src/frontend/magnify/src/components/meetings/RegisterMeetingForm/RegisterMeetingForm.tsx +++ b/src/frontend/magnify/src/components/meetings/RegisterMeetingForm/RegisterMeetingForm.tsx @@ -1,6 +1,7 @@ +import { faker } from '@faker-js/faker'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AxiosError } from 'axios'; -import { Form, Formik, FormikHelpers } from 'formik'; +import { ErrorMessage, Form, Formik, FormikHelpers } from 'formik'; import { Box } from 'grommet'; import { DateTime, Settings } from 'luxon'; import React, { useMemo } from 'react'; @@ -33,6 +34,16 @@ const messages = defineMessages({ defaultMessage: 'Register meeting', description: 'Label for the submit button to register a new meeting', }, + startTimeError: { + defaultMessage: 'Start time error. ', + description: 'Error message when starting time and date are invalid', + id: 'components.design-system.Formik.FormikDateTimePicker.startTimeError', + }, + endTimeError: { + defaultMessage: 'End time error. ', + description: 'Error message when ending time and date are invalid', + id: 'components.design-system.Formik.FormikDateTimePicker.endTimeError', + }, invalidTime: { defaultMessage: 'Starting time should be in the future and ending time should be after starting time.', @@ -68,9 +79,8 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { 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) + ? mergeDateTime(this.parent.startDate, startTimeValue) : null; const chosenEndDateTime = this.parent ? mergeDateTime(this.parent.endDate, this.parent.endTime) @@ -88,12 +98,11 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { 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) + ? mergeDateTime(this.parent.endDate, endTimeValue) : null; return ( chosenEndDateTime == null || @@ -112,19 +121,19 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { 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 queryClient = useQueryClient(); + + // const mutation = useMutation( + // MeetingsRepository.create, + // { + // onSuccess: (newMeeting) => { + // queryClient.setQueryData([MagnifyQueryKeys.MEETINGS], (meetings: Meeting[] = []) => { + // return [...meetings, newMeeting]; + // }); + // onSuccess(newMeeting); + // }, + // }, + // ); const allSuggestions = getSuggestions(intl.locale); const allFrenchSuggestions = getSuggestions('fr'); @@ -144,14 +153,43 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { 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(',')); - } - }, - }); + // mutation.mutate(values, { + // onError: (error) => { + // const formErrors = error?.response?.data as Maybe; + // if (formErrors?.slug) { + // actions.setFieldError('name', formErrors.slug.join(',')); + // } + // }, + // }); + 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 ( @@ -186,15 +224,22 @@ const RegisterMeetingForm = ({ onSuccess }: RegisterMeetingFormProps) => { - {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} - + {errors.startTime && touched.startTime ? ( + + {intl.formatMessage(messages.startTimeError)} + + + ) : null} + {errors.endTime && touched.endTime ? ( + + {intl.formatMessage(messages.endTimeError)} + + + ) : 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/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;