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;