diff --git a/packages/volto/news/6732.feature b/packages/volto/news/6732.feature new file mode 100644 index 00000000000..cdd39302eee --- /dev/null +++ b/packages/volto/news/6732.feature @@ -0,0 +1 @@ +Replace `moment.js` with native `Intl` formatting in `Comments` and `EventDatesInfo` components. @avoinea diff --git a/packages/volto/src/components/theme/Comments/Comments.jsx b/packages/volto/src/components/theme/Comments/Comments.jsx index 27df92a45dd..4525a9ebf71 100644 --- a/packages/volto/src/components/theme/Comments/Comments.jsx +++ b/packages/volto/src/components/theme/Comments/Comments.jsx @@ -3,10 +3,13 @@ import PropTypes from 'prop-types'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { createPortal } from 'react-dom'; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; -import { compose } from 'redux'; + import { Button, Comment, Container, Icon } from 'semantic-ui-react'; -import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; +import { + formatDate, + formatRelativeDate, +} from '@plone/volto/helpers/Utils/Date'; import { addComment, deleteComment, @@ -200,8 +203,6 @@ const Comments = (props) => { return allCommentsWithCildren; }; - const moment = props.moment.default; - const allCommentsWithCildren = useMemo( () => addRepliesAsChildrenToComments(items), [items], @@ -223,8 +224,17 @@ const Comments = (props) => { {' '} - - {moment(comment.creation_date).fromNow()} + + {formatRelativeDate({ + date: comment.creation_date, + locale: intl.locale, + })} @@ -375,4 +385,4 @@ Comments.propTypes = { pathname: PropTypes.string.isRequired, }; -export default compose(injectLazyLibs(['moment']))(Comments); +export default Comments; diff --git a/packages/volto/src/components/theme/Comments/Comments.test.jsx b/packages/volto/src/components/theme/Comments/Comments.test.jsx index e9443571e12..205ebce0210 100644 --- a/packages/volto/src/components/theme/Comments/Comments.test.jsx +++ b/packages/volto/src/components/theme/Comments/Comments.test.jsx @@ -12,23 +12,18 @@ vi.mock('@plone/volto/components/theme/Comments/CommentEditModal', () => ({ const mockStore = configureStore(); -vi.mock('moment', () => ({ - default: vi.fn(() => ({ - format: vi.fn(() => 'Sunday, April 23, 2017 3:38 AM'), - fromNow: vi.fn(() => 'a few seconds ago'), - })), -})); - -vi.mock('@plone/volto/helpers/Loadable/Loadable'); vi.mock('@plone/volto/components/manage/Form'); -beforeAll(async () => { - const { __setLoadables } = await import( - '@plone/volto/helpers/Loadable/Loadable' - ); - await __setLoadables(); -}); describe('Comments', () => { + beforeEach(() => { + // Freeze time so formatRelativeDate produces deterministic output + vi.useFakeTimers({ now: new Date('2017-04-23T03:38:04Z') }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + it('renders a comments component', () => { const store = mockStore({ comments: { @@ -37,7 +32,7 @@ describe('Comments', () => { '@id': 'someurl', comment_id: '1614094601171408', author_name: 'admin', - creation_date: '2017-11-06T19:36:01', + creation_date: '2017-04-23T03:38:00Z', text: { data: 'Some comment' }, is_deletable: true, is_editable: true, diff --git a/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap b/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap index 5e61cba9c39..5d6f5c00fb9 100644 --- a/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap +++ b/packages/volto/src/components/theme/Comments/__snapshots__/Comments.test.jsx.snap @@ -81,9 +81,9 @@ exports[`Comments > renders a comments component 1`] = ` - a few seconds ago + 4 seconds ago diff --git a/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap b/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap index 20f942f71ec..61adbe433ed 100644 --- a/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap +++ b/packages/volto/src/components/theme/EventDetails/__snapshots__/EventDetails.test.jsx.snap @@ -33,6 +33,45 @@ exports[`renders event details component with all props 1`] = ` > When +

+ + + Jun 23, 2019 + + + + + + 3:20 PM + + +  to  + + + Jun 24, 2019 + + + + + + 3:20 PM + + +

@@ -146,6 +185,27 @@ exports[`renders event details component with only required props 1`] = ` > When
+

+ + Jun 23, 2019 + +  from  + + 3:20 PM + +  to  + + 4:20 PM + +

@@ -188,6 +248,27 @@ exports[`renders event details component without links to api in the text 1`] = > When
+

+ + Jun 23, 2019 + +  from  + + 3:20 PM + +  to  + + 4:20 PM + +

diff --git a/packages/volto/src/components/theme/View/EventDatesInfo.jsx b/packages/volto/src/components/theme/View/EventDatesInfo.jsx index 232df550f64..730a8d74ea2 100644 --- a/packages/volto/src/components/theme/View/EventDatesInfo.jsx +++ b/packages/volto/src/components/theme/View/EventDatesInfo.jsx @@ -3,11 +3,18 @@ import PropTypes from 'prop-types'; import { List } from 'semantic-ui-react'; import cx from 'classnames'; -import { toBackendLang } from '@plone/volto/helpers/Utils/Utils'; import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; import { useSelector } from 'react-redux'; +import { formatDate } from '@plone/volto/helpers/Utils/Date'; +/** + * @deprecated Use the native Date API directly. Will be removed in Volto 20. + */ export const datesForDisplay = (start, end, moment) => { + // eslint-disable-next-line no-console + console.warn( + 'datesForDisplay is deprecated and will be removed in Volto 20. Use the native Date API directly.', + ); const mStart = moment(start); const mEnd = moment(end); if (!mStart.isValid() || !mEnd.isValid()) { @@ -25,17 +32,30 @@ export const datesForDisplay = (start, end, moment) => { }; }; -const When_ = ({ start, end, whole_day, open_end, moment: momentlib }) => { - const lang = useSelector((state) => state.intl.locale); - - const moment = momentlib.default; - moment.locale(toBackendLang(lang)); +const When_ = ({ start, end, whole_day, open_end }) => { + const locale = useSelector((state) => state.intl.locale); - const datesInfo = datesForDisplay(start, end, moment); - if (!datesInfo) { - return; + const dStart = new Date(start); + const dEnd = end != null ? new Date(end) : new Date(); + if (isNaN(dStart.getTime()) || isNaN(dEnd.getTime())) { + return null; } - // TODO I18N INTL + const sameDay = + dStart.getFullYear() === dEnd.getFullYear() && + dStart.getMonth() === dEnd.getMonth() && + dStart.getDate() === dEnd.getDate(); + const sameTime = + sameDay && + dStart.getHours() === dEnd.getHours() && + dStart.getMinutes() === dEnd.getMinutes(); + const datesInfo = { + sameDay, + sameTime, + startDate: formatDate({ date: dStart, format: 'll', locale }), + startTime: formatDate({ date: dStart, format: 'LT', locale }), + endDate: formatDate({ date: dEnd, format: 'll', locale }), + endTime: formatDate({ date: dEnd, format: 'LT', locale }), + }; return (

{ ); }; -export const When = injectLazyLibs(['moment'])(When_); +export const When = When_; When.propTypes = { start: PropTypes.string.isRequired, @@ -109,13 +129,8 @@ When.propTypes = { open_end: PropTypes.bool, }; -export const Recurrence_ = ({ - recurrence, - start, - moment: momentlib, - rrule, -}) => { - const moment = momentlib.default; +export const Recurrence_ = ({ recurrence, start, rrule }) => { + const locale = useSelector((state) => state.intl.locale); const { RRule, rrulestr } = rrule; if (recurrence.indexOf('DTSTART') < 0) { var dtstart = RRule.optionsToString({ @@ -129,12 +144,11 @@ export const Recurrence_ = ({ datesForDisplay(date, undefined, moment)) - .map((date) => date.startDate)} + .map((date) => formatDate({ date, format: 'll', locale }))} /> ); }; -export const Recurrence = injectLazyLibs(['moment', 'rrule'])(Recurrence_); +export const Recurrence = injectLazyLibs(['rrule'])(Recurrence_); Recurrence.propTypes = { recurrence: PropTypes.string.isRequired, diff --git a/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx b/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx index ac2d4e85c2c..a4952eec6cb 100644 --- a/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx +++ b/packages/volto/src/components/theme/View/EventDatesInfo.test.jsx @@ -93,3 +93,24 @@ test('same day, not whole day, not open end', () => { const json = component.toJSON(); expect(json).toMatchSnapshot(); }); + +test('returns null for invalid start date', () => { + const component = renderer.create( + + + , + ); + expect(component.toJSON()).toBeNull(); +}); + +test('uses current time when end is undefined', () => { + vi.useFakeTimers({ now: new Date('2019-06-23T15:20:00+00:00') }); + const component = renderer.create( + + + , + ); + const json = component.toJSON(); + expect(json).toMatchSnapshot(); + vi.useRealTimers(); +}); diff --git a/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap b/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap index 306d9a0c544..c27515d3b57 100644 --- a/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap +++ b/packages/volto/src/components/theme/View/__snapshots__/EventDatesInfo.test.jsx.snap @@ -121,3 +121,27 @@ exports[`same day, whole day 1`] = `

`; + +exports[`uses current time when end is undefined 1`] = ` +

+ + Jun 23, 2019 + +  from  + + 11:55 AM + +  to  + + 3:20 PM + +

+`; diff --git a/packages/volto/types/components/theme/View/EventDatesInfo.d.ts b/packages/volto/types/components/theme/View/EventDatesInfo.d.ts index 0bfeadcd54d..b8059c3a900 100644 --- a/packages/volto/types/components/theme/View/EventDatesInfo.d.ts +++ b/packages/volto/types/components/theme/View/EventDatesInfo.d.ts @@ -1,3 +1,4 @@ +/** @deprecated Will be removed in Volto 20. Use the native Date API directly. */ export function datesForDisplay(start: any, end: any, moment: any): { sameDay: any; sameTime: any; @@ -7,10 +8,9 @@ export function datesForDisplay(start: any, end: any, moment: any): { endTime: any; }; export const When: any; -export function Recurrence_({ recurrence, start, moment: momentlib, rrule, }: { +export function Recurrence_({ recurrence, start, rrule, }: { recurrence: any; start: any; - moment: any; rrule: any; }): import("react/jsx-runtime").JSX.Element; export const Recurrence: any;