diff --git a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx index 6c5a79064..e821a7c6c 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx @@ -24,13 +24,24 @@ function LegacyConfigForm({ const legacyFormValidationSchema = Yup.object().shape({ blackoutDates: Yup.array( Yup.object().shape({ - startDate: Yup.date().required(intl.formatMessage(messages.blackoutStartDateRequired)), - endDate: Yup.date().required(intl.formatMessage(messages.blackoutEndDateRequired)).when('startDate', { + startDate: Yup.string().checkFormat( + intl.formatMessage(messages.blackoutStartDateInValidFormat), 'date', + ).required(intl.formatMessage(messages.blackoutStartDateRequired)), + endDate: Yup.string().checkFormat( + intl.formatMessage(messages.blackoutEndDateInValidFormat), 'date', + ).required(intl.formatMessage(messages.blackoutEndDateRequired)).when('startDate', { is: (startDate) => startDate, - then: Yup.date().min(Yup.ref('startDate'), intl.formatMessage(messages.blackoutEndDateInPast)), + then: Yup.string().compare(intl.formatMessage(messages.blackoutEndDateInPast), 'date'), + }), + startTime: Yup.string().checkFormat( + intl.formatMessage(messages.blackoutStartTimeInValidFormat), 'time', + ), + endTime: Yup.string().checkFormat( + intl.formatMessage(messages.blackoutEndTimeInValidFormat), 'time', + ).when('startTime', { + is: (startTime) => startTime, + then: Yup.string().compare(intl.formatMessage(messages.blackoutEndTimeInPast), 'time'), }), - startTime: Yup.string(), - endTime: Yup.string().compare(intl.formatMessage(messages.blackoutEndTimeInPast)), }), ), discussionTopics: Yup.array( diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js b/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js index d2a9013e3..5bced7687 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js +++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js @@ -270,6 +270,26 @@ const messages = defineMessages({ defaultMessage: 'End time cannot be before start time', description: 'Tells the user that the blackout end time cannot be in past and cannot be before start time', }, + blackoutStartTimeInValidFormat: { + id: 'authoring.blackoutDates.startTime.inValidFormat', + defaultMessage: 'Enter a valid start time', + description: 'Tells the user that the blackout start time format is in valid', + }, + blackoutEndTimeInValidFormat: { + id: 'authoring.blackoutDates.endTime.inValidFormat', + defaultMessage: 'Enter a valid end time', + description: 'Tells the user that the blackout end time format is in valid', + }, + blackoutStartDateInValidFormat: { + id: 'authoring.blackoutDates.startDate.inValidFormat', + defaultMessage: 'Enter a valid start Date', + description: 'Tells the user that the blackout start date format is in valid', + }, + blackoutEndDateInValidFormat: { + id: 'authoring.blackoutDates.endDate.inValidFormat', + defaultMessage: 'Enter a valid end date', + description: 'Tells the user that the blackout end date format is in valid', + }, deleteAltText: { id: 'authoring.topics.delete', defaultMessage: 'Delete Topic', diff --git a/src/pages-and-resources/discussions/app-config-form/utils.js b/src/pages-and-resources/discussions/app-config-form/utils.js index b7f29d809..ad29df7ff 100644 --- a/src/pages-and-resources/discussions/app-config-form/utils.js +++ b/src/pages-and-resources/discussions/app-config-form/utils.js @@ -1,7 +1,6 @@ import moment from 'moment'; import _ from 'lodash'; import { getIn } from 'formik'; - import { blackoutDatesStatus as constants } from '../data/constants'; export const filterItemFromObject = (array, key, value) => ( @@ -27,14 +26,29 @@ export const checkStatus = ([startDate, endDate]) => { return status; }; -export const formatDate = (date, time) => (time ? `${date}T${time}` : date); +export const validTimeFormats = ['hh:mm A', 'HH:mm']; +export const mergeDateTime = (date, time) => ((date && time) ? `${date}T${time}` : date); export const isSameDay = (startDate, endDate) => moment(startDate).isSame(endDate, 'day'); export const isSameMonth = (startDate, endDate) => moment(startDate).isSame(endDate, 'month'); export const isSameYear = (startDate, endDate) => moment(startDate).isSame(endDate, 'year'); +export const getTime = (dateTime) => dateTime.split('T')[1] || ''; +export const hasValidDateFormat = (date) => moment(date, ['MM/DD/YYYY', 'YYYY-MM-DD'], true).isValid(); +export const hasValidTimeFormat = (time) => time && moment(time, validTimeFormats, true).isValid(); +export const normalizeTime = (time) => time && moment(time, validTimeFormats, true).format('HH:mm'); +export const normalizeDate = (date) => moment( + date, ['MM/DD/YYYY', 'YYYY-MM-DDTHH:mm', 'YYYY-MM-DD'], true, +).format('YYYY-MM-DD'); + +export const decodeDateTime = (date, time) => { + const nDate = normalizeDate(date); + const nTime = normalizeTime(time); + + return moment(mergeDateTime(nDate, nTime)); +}; export const sortBlackoutDatesByStatus = (data, status, order) => ( _.orderBy(data.filter(date => date.status === status), - [(obj) => moment(formatDate(obj.startDate, obj.startTime))], [order]) + [(obj) => decodeDateTime(obj.startDate, obj.startTime)], [order]) ); export const formatBlackoutDates = ({ @@ -47,8 +61,8 @@ export const formatBlackoutDates = ({ const isTimeAvailable = Boolean(startTime && endTime); const mStartDate = moment(startDate); const mEndDate = moment(endDate); - const mStartDateTime = moment(`${startDate}T${startTime}`); - const mEndDateTime = moment(`${endDate}T${endTime}`); + const mStartDateTime = decodeDateTime(startDate, startTime); + const mEndDateTime = decodeDateTime(endDate, endTime); if (hasSameDay && !isTimeAvailable) { formattedDate = mStartDate.format('MMMM D, YYYY'); diff --git a/src/pages-and-resources/discussions/data/api.js b/src/pages-and-resources/discussions/data/api.js index d411fc9a3..99195ef91 100644 --- a/src/pages-and-resources/discussions/data/api.js +++ b/src/pages-and-resources/discussions/data/api.js @@ -1,10 +1,16 @@ import { getConfig, camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import _ from 'lodash'; -import moment from 'moment'; import { v4 as uuid } from 'uuid'; -import { checkStatus, sortBlackoutDatesByStatus, formatDate } from '../app-config-form/utils'; +import { + checkStatus, + sortBlackoutDatesByStatus, + mergeDateTime, + normalizeDate, + normalizeTime, + getTime, +} from '../app-config-form/utils'; import { blackoutDatesStatus as constants } from './constants'; function normalizeLtiConfig(data) { @@ -43,10 +49,10 @@ export function normalizeBlackoutDates(data) { const normalizeData = data.map(([startDate, endDate]) => ({ id: uuid(), - startDate: moment(startDate).format('YYYY-MM-DD'), - startTime: startDate.split('T')[1] || '', - endDate: moment(endDate).format('YYYY-MM-DD'), - endTime: endDate.split('T')[1] || '', + startDate: normalizeDate(startDate), + startTime: getTime(startDate), + endDate: normalizeDate(endDate), + endTime: getTime(endDate), status: checkStatus([startDate, endDate]), })); @@ -119,10 +125,16 @@ function normalizeApps(data) { }; } -export function denormalizeBlackoutDate(date) { +export function denormalizeBlackoutDate(blackoutPeriod) { return [ - formatDate(date.startDate, date.startTime), - formatDate(date.endDate, date.endTime), + mergeDateTime( + normalizeDate(blackoutPeriod.startDate), + normalizeTime(blackoutPeriod.startTime), + ), + mergeDateTime( + normalizeDate(blackoutPeriod.endDate), + normalizeTime(blackoutPeriod.endTime), + ), ]; } diff --git a/src/utils.js b/src/utils.js index 95b3239f1..6e10ec873 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,12 +3,14 @@ import { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useMediaQuery } from 'react-responsive'; import * as Yup from 'yup'; -import moment from 'moment'; import { RequestStatus } from './data/constants'; import { getCourseAppSettingValue, getLoadingStatus } from './pages-and-resources/data/selectors'; import { fetchCourseAppSettings, updateCourseAppSetting } from './pages-and-resources/data/thunks'; import { PagesAndResourcesContext } from './pages-and-resources/PagesAndResourcesProvider'; +import { + hasValidDateFormat, hasValidTimeFormat, decodeDateTime, +} from './pages-and-resources/discussions/app-config-form/utils'; export const executeThunk = async (thunk, dispatch, getState) => { await thunk(dispatch, getState); @@ -91,13 +93,28 @@ export function setupYupExtensions() { }); }); - Yup.addMethod(Yup.string, 'compare', function compare(message) { + Yup.addMethod(Yup.string, 'compare', function compare(message, type) { return this.test('isGreater', message, function isGreater() { - if (!this.parent || !this.parent.startTime || !this.parent.endTime) { + // This function compare 2 dates or 2 times. It return no error if dateInstance/timeInstance is empty + // of if startTime or endTime is not present for time comparesion + // or startDate or endDate is not present for date comparesion + + if (!this.parent + || (!(this.parent.startTime && this.parent.endTime) && type === 'time') + || (!(this.parent.startDate && this.parent.endDate) && type === 'date') + ) { return true; } - const isInvalidStartDateTime = moment(`${moment(this.parent.startDate).format('YYYY-MM-DD')}T${this.parent.startTime}`) - .isSameOrAfter(moment(`${moment(this.parent.endDate).format('YYYY-MM-DD')}T${this.parent.endTime}`)); + + const startDateTime = decodeDateTime(this.parent.startDate, this.parent.startTime); + const endDateTime = decodeDateTime(this.parent.endDate, this.parent.endTime); + let isInvalidStartDateTime; + + if (type === 'date') { + isInvalidStartDateTime = startDateTime.isAfter(endDateTime); + } else if (type === 'time') { + isInvalidStartDateTime = startDateTime.isSameOrAfter(endDateTime); + } if (isInvalidStartDateTime) { throw this.createError({ @@ -108,4 +125,27 @@ export function setupYupExtensions() { return true; }); }); + + Yup.addMethod(Yup.string, 'checkFormat', function checkFormat(message, type) { + return this.test('isValidFormat', message, function isValidFormat() { + if (!this.originalValue) { + return true; + } + let isValid; + + if (type === 'date') { + isValid = hasValidDateFormat(this.originalValue); + } else if (type === 'time') { + isValid = hasValidTimeFormat(this.originalValue); + } + + if (!isValid) { + throw this.createError({ + path: `${this.path}`, + error: message, + }); + } + return true; + }); + }); }