@plone/volto 14.3.0 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/addon-registry.js +15 -2
  3. package/locales/ca/LC_MESSAGES/volto.po +5 -0
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +5 -0
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +5 -0
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +5 -0
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +5 -0
  12. package/locales/eu.json +1 -1
  13. package/locales/fr/LC_MESSAGES/volto.po +5 -0
  14. package/locales/fr.json +1 -1
  15. package/locales/it/LC_MESSAGES/volto.po +10 -5
  16. package/locales/it.json +1 -1
  17. package/locales/ja/LC_MESSAGES/volto.po +5 -0
  18. package/locales/ja.json +1 -1
  19. package/locales/nl/LC_MESSAGES/volto.po +5 -0
  20. package/locales/nl.json +1 -1
  21. package/locales/pt/LC_MESSAGES/volto.po +5 -0
  22. package/locales/pt.json +1 -1
  23. package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
  24. package/locales/pt_BR.json +1 -1
  25. package/locales/ro/LC_MESSAGES/volto.po +5 -0
  26. package/locales/ro.json +1 -1
  27. package/locales/volto.pot +6 -1
  28. package/package.json +4 -2
  29. package/razzle.config.js +1 -1
  30. package/src/components/index.js +2 -0
  31. package/src/components/manage/Add/Add.jsx +14 -3
  32. package/src/components/manage/Blocks/Block/BlocksForm.test.jsx +6 -0
  33. package/src/components/manage/Blocks/Listing/ListingBody.jsx +6 -0
  34. package/src/components/manage/Contents/Contents.jsx +15 -6
  35. package/src/components/manage/Contents/Contents.test.jsx +0 -7
  36. package/src/components/manage/Contents/ContentsIndexHeader.jsx +46 -34
  37. package/src/components/manage/Contents/ContentsItem.jsx +69 -61
  38. package/src/components/manage/Contents/ContentsUploadModal.jsx +2 -4
  39. package/src/components/manage/Controlpanels/ModerateComments.jsx +7 -5
  40. package/src/components/manage/Diff/Diff.jsx +13 -5
  41. package/src/components/manage/Diff/Diff.test.jsx +0 -6
  42. package/src/components/manage/Diff/DiffField.jsx +12 -3
  43. package/src/components/manage/Diff/DiffField.test.jsx +0 -6
  44. package/src/components/manage/DragDropList/DragDropList.jsx +4 -2
  45. package/src/components/manage/Form/Field.jsx +12 -2
  46. package/src/components/manage/History/History.jsx +6 -5
  47. package/src/components/manage/History/History.test.jsx +12 -7
  48. package/src/components/manage/Widgets/DatetimeWidget.jsx +10 -3
  49. package/src/components/manage/Widgets/DatetimeWidget.test.jsx +2 -2
  50. package/src/components/manage/Widgets/FormFieldWrapper.jsx +14 -1
  51. package/src/components/manage/Widgets/ObjectListWidget.stories.js +3 -3
  52. package/src/components/manage/Widgets/ObjectListWidget.test.js +6 -0
  53. package/src/components/manage/Widgets/RecurrenceWidget/ByDayField.jsx +4 -3
  54. package/src/components/manage/Widgets/RecurrenceWidget/MonthOfTheYearField.jsx +10 -3
  55. package/src/components/manage/Widgets/RecurrenceWidget/Occurences.jsx +8 -4
  56. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +21 -13
  57. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.test.jsx +6 -0
  58. package/src/components/manage/Widgets/RecurrenceWidget/Utils.js +1 -2
  59. package/src/components/manage/Widgets/RecurrenceWidget/WeekdayOfTheMonthField.jsx +5 -3
  60. package/src/components/manage/Widgets/SchemaWidget.jsx +4 -2
  61. package/src/components/manage/Widgets/SchemaWidget.test.jsx +6 -0
  62. package/src/components/manage/Widgets/SchemaWidgetFieldset.jsx +8 -4
  63. package/src/components/manage/Widgets/SchemaWidgetFieldset.test.jsx +7 -1
  64. package/src/components/manage/Widgets/VocabularyTermsWidget.jsx +41 -7
  65. package/src/components/manage/Widgets/VocabularyTermsWidget.stories.js +6 -21
  66. package/src/components/manage/Widgets/VocabularyTermsWidget.test.jsx +6 -0
  67. package/src/components/theme/Comments/Comments.jsx +3 -1
  68. package/src/components/theme/Comments/Comments.test.jsx +6 -0
  69. package/src/components/theme/FormattedDate/FormattedDate.jsx +42 -0
  70. package/src/components/theme/FormattedDate/FormattedDate.stories.jsx +91 -0
  71. package/src/components/theme/FormattedDate/FormattedRelativeDate.jsx +57 -0
  72. package/src/components/theme/FormattedDate/FormattedRelativeDate.stories.jsx +122 -0
  73. package/src/components/theme/Navigation/Navigation.jsx +1 -1
  74. package/src/components/theme/View/EventDatesInfo.jsx +12 -6
  75. package/src/components/theme/View/EventDatesInfo.test.jsx +6 -0
  76. package/src/components/theme/View/EventView.test.jsx +6 -0
  77. package/src/config/Loadables.jsx +3 -0
  78. package/src/config/index.js +1 -0
  79. package/src/helpers/Content/Content.js +16 -0
  80. package/src/helpers/Content/Content.test.js +20 -1
  81. package/src/helpers/FormValidation/FormValidation.js +1 -1
  82. package/src/helpers/Utils/Date.js +97 -0
  83. package/src/helpers/Utils/Date.test.js +197 -0
  84. package/src/helpers/Utils/Utils.js +1 -2
  85. package/src/helpers/Utils/Utils.test.js +25 -0
  86. package/src/helpers/index.js +1 -0
  87. package/theme/themes/pastanaga/extras/widgets.less +12 -0
  88. package/volto.config.js +3 -3
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import {
3
+ formatRelativeDate,
4
+ long_date_format,
5
+ toDate,
6
+ } from '@plone/volto/helpers/Utils/Date';
7
+ import { useSelector } from 'react-redux';
8
+
9
+ const FormattedRelativeDate = ({
10
+ date,
11
+ style,
12
+ relativeTo,
13
+ className,
14
+ locale,
15
+ children,
16
+ live = false,
17
+ refresh = 5000,
18
+ }) => {
19
+ const language = useSelector((state) => locale || state.intl.locale);
20
+ const [liveRelativeTo, setLiveRelativeTo] = React.useState(
21
+ relativeTo ? toDate(relativeTo) : new Date(),
22
+ );
23
+
24
+ const interval = React.useRef();
25
+
26
+ React.useEffect(() => {
27
+ if (live) {
28
+ interval.current = setInterval(() => {
29
+ setLiveRelativeTo(new Date());
30
+ }, refresh);
31
+ }
32
+ return () => interval.current && clearInterval(interval.current);
33
+ }, [refresh, live]);
34
+
35
+ const args = { locale: language, date, style, relativeTo: liveRelativeTo };
36
+
37
+ return (
38
+ <time
39
+ className={className}
40
+ dateTime={date}
41
+ title={new Intl.DateTimeFormat(language, long_date_format).format(
42
+ new Date(date),
43
+ )}
44
+ >
45
+ {children
46
+ ? children(
47
+ formatRelativeDate({
48
+ ...args,
49
+ formatToParts: true,
50
+ }),
51
+ )
52
+ : formatRelativeDate(args)}
53
+ </time>
54
+ );
55
+ };
56
+
57
+ export default FormattedRelativeDate;
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import Wrapper from '@plone/volto/storybook';
3
+ import FormattedRelativeDate from './FormattedRelativeDate';
4
+
5
+ const date = new Date(new Date() - 1000);
6
+ const relativeTo = new Date(new Date().getTime() + 123213124);
7
+
8
+ const toDate = (date) => (typeof date === 'number' ? new Date(date) : date);
9
+
10
+ function StoryComponent(args) {
11
+ const { style, locale, live, refresh } = args;
12
+ const date = toDate(args.date);
13
+ const relativeTo = args.relativeTo
14
+ ? toDate(args.relativeTo)
15
+ : args.relativeTo;
16
+ return (
17
+ <Wrapper
18
+ customStore={{ intl: { locale } }}
19
+ location={{ pathname: '/folder2/folder21/doc212' }}
20
+ >
21
+ <FormattedRelativeDate
22
+ date={date}
23
+ locale={locale}
24
+ style={style}
25
+ relativeTo={relativeTo}
26
+ live={live}
27
+ refresh={refresh}
28
+ >
29
+ {this.children}
30
+ </FormattedRelativeDate>
31
+ </Wrapper>
32
+ );
33
+ }
34
+
35
+ export const Default = StoryComponent.bind({});
36
+ Default.args = {
37
+ date,
38
+ };
39
+
40
+ export const Localized = StoryComponent.bind({});
41
+ Localized.args = {
42
+ date,
43
+ locale: 'de',
44
+ };
45
+
46
+ export const Style = StoryComponent.bind({});
47
+ Style.args = {
48
+ date,
49
+ style: 'short',
50
+ };
51
+
52
+ export const RelativeToDate = StoryComponent.bind({});
53
+ RelativeToDate.args = {
54
+ date,
55
+ relativeTo,
56
+ };
57
+
58
+ export const LiveRefresh = StoryComponent.bind({});
59
+ LiveRefresh.args = {
60
+ date,
61
+ live: true,
62
+ refresh: 1000,
63
+ };
64
+
65
+ export const SplitParts = StoryComponent.bind({
66
+ children: (parts) =>
67
+ parts.map((p, i) => (
68
+ <div key={i}>
69
+ <strong>{p.value}</strong> <small>({p.type}</small>)
70
+ </div>
71
+ )),
72
+ });
73
+ SplitParts.args = {
74
+ date,
75
+ long: true,
76
+ };
77
+
78
+ export default {
79
+ title: 'Internal Components/Formatted Relative Date',
80
+ component: FormattedRelativeDate,
81
+ decorators: [
82
+ (Story) => (
83
+ <div style={{ width: '400px' }}>
84
+ <Story />
85
+ </div>
86
+ ),
87
+ ],
88
+ argTypes: {
89
+ live: {
90
+ control: {
91
+ type: 'disabled',
92
+ },
93
+ },
94
+ refresh: {
95
+ control: {
96
+ type: 'disabled',
97
+ },
98
+ },
99
+ date: {
100
+ control: {
101
+ type: 'date',
102
+ },
103
+ },
104
+ relativeTo: {
105
+ control: {
106
+ type: 'date',
107
+ },
108
+ },
109
+ locale: {
110
+ control: {
111
+ type: 'select',
112
+ options: ['en', 'de', 'us'],
113
+ },
114
+ },
115
+ style: {
116
+ control: {
117
+ type: 'select',
118
+ options: ['long', 'short', 'narrow'],
119
+ },
120
+ },
121
+ },
122
+ };
@@ -124,7 +124,7 @@ class Navigation extends Component {
124
124
  */
125
125
  render() {
126
126
  return (
127
- <nav className="navigation" id="navigation">
127
+ <nav className="navigation" id="navigation" aria-label="navigation">
128
128
  <div className="hamburger-wrapper mobile tablet only">
129
129
  <button
130
130
  className={cx('hamburger hamburger--spin', {
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { List } from 'semantic-ui-react';
4
- import moment from 'moment';
5
4
  import { useIntl } from 'react-intl';
6
5
  import cx from 'classnames';
7
6
  import { RRule, rrulestr } from 'rrule';
7
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
8
8
 
9
- export const datesForDisplay = (start, end) => {
9
+ export const datesForDisplay = (start, end, moment) => {
10
10
  const mStart = moment(start);
11
11
  const mEnd = moment(end);
12
12
  if (!mStart.isValid() || !mEnd.isValid()) {
@@ -24,11 +24,13 @@ export const datesForDisplay = (start, end) => {
24
24
  };
25
25
  };
26
26
 
27
- export const When = ({ start, end, whole_day, open_end }) => {
27
+ const When_ = ({ start, end, whole_day, open_end, moment: momentlib }) => {
28
28
  const intl = useIntl();
29
+
30
+ const moment = momentlib.default;
29
31
  moment.locale(intl.locale);
30
32
 
31
- const datesInfo = datesForDisplay(start, end);
33
+ const datesInfo = datesForDisplay(start, end, moment);
32
34
  if (!datesInfo) {
33
35
  return;
34
36
  }
@@ -97,6 +99,8 @@ export const When = ({ start, end, whole_day, open_end }) => {
97
99
  );
98
100
  };
99
101
 
102
+ export const When = injectLazyLibs(['moment'])(When_);
103
+
100
104
  When.propTypes = {
101
105
  start: PropTypes.string.isRequired,
102
106
  end: PropTypes.string,
@@ -104,7 +108,8 @@ When.propTypes = {
104
108
  open_end: PropTypes.bool,
105
109
  };
106
110
 
107
- export const Recurrence = ({ recurrence, start }) => {
111
+ export const Recurrence_ = ({ recurrence, start, moment: momentlib }) => {
112
+ const moment = momentlib.default;
108
113
  if (recurrence.indexOf('DTSTART') < 0) {
109
114
  var dtstart = RRule.optionsToString({
110
115
  dtstart: new Date(start),
@@ -117,11 +122,12 @@ export const Recurrence = ({ recurrence, start }) => {
117
122
  <List
118
123
  items={rrule
119
124
  .all()
120
- .map((date) => datesForDisplay(date))
125
+ .map((date) => datesForDisplay(date, undefined, moment))
121
126
  .map((date) => date.startDate)}
122
127
  />
123
128
  );
124
129
  };
130
+ export const Recurrence = injectLazyLibs(['moment'])(Recurrence_);
125
131
 
126
132
  Recurrence.propTypes = {
127
133
  recurrence: PropTypes.string.isRequired,
@@ -5,6 +5,12 @@ import { When } from './EventDatesInfo';
5
5
  import configureStore from 'redux-mock-store';
6
6
  const mockStore = configureStore();
7
7
 
8
+ jest.mock('@plone/volto/helpers/Loadable/Loadable');
9
+ beforeAll(
10
+ async () =>
11
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
12
+ );
13
+
8
14
  const store = mockStore({
9
15
  intl: {
10
16
  locale: 'en',
@@ -14,6 +14,12 @@ const store = mockStore({
14
14
  },
15
15
  });
16
16
 
17
+ jest.mock('@plone/volto/helpers/Loadable/Loadable');
18
+ beforeAll(
19
+ async () =>
20
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
21
+ );
22
+
17
23
  const { settings } = config;
18
24
 
19
25
  test('renders an event view component with all props', () => {
@@ -29,4 +29,7 @@ export const loadables = {
29
29
  diffLib: loadable.lib(() => import('diff')),
30
30
  moment: loadable.lib(() => import('moment')),
31
31
  reactDates: loadable.lib(() => import('react-dates')),
32
+ reactDnd: loadable.lib(() => import('react-dnd')),
33
+ reactDndHtml5Backend: loadable.lib(() => import('react-dnd-html5-backend')),
34
+ reactBeautifulDnd: loadable.lib(() => import('react-beautiful-dnd')),
32
35
  };
@@ -133,6 +133,7 @@ let config = {
133
133
  'prismCore',
134
134
  'toastify',
135
135
  'reactSelect',
136
+ 'reactBeautifulDnd',
136
137
  // 'diffLib',
137
138
  ],
138
139
  },
@@ -63,3 +63,19 @@ export function getContentIcon(type, isFolderish) {
63
63
  if (type in contentIcons) return contentIcons[type];
64
64
  return isFolderish ? contentIcons.Folder : contentIcons.File;
65
65
  }
66
+
67
+ /**
68
+ * Get the language independent fields presents in a schema.
69
+ * @description Configurable in config
70
+ * @function getLanguageIndependentFields
71
+ * @param {string} schema content type JSON Schema serialization
72
+ * @returns {array} List of language independent fields
73
+ */
74
+ export function getLanguageIndependentFields(schema) {
75
+ const { properties } = schema;
76
+ return Object.keys(properties).filter(
77
+ (field) =>
78
+ Object.keys(properties[field]).includes('multilingual_options') &&
79
+ properties[field]['multilingual_options']?.['language_independent'],
80
+ );
81
+ }
@@ -1,4 +1,8 @@
1
- import { nestContent, getContentIcon } from './Content';
1
+ import {
2
+ nestContent,
3
+ getContentIcon,
4
+ getLanguageIndependentFields,
5
+ } from './Content';
2
6
  import contentExistingSVG from '@plone/volto/icons/content-existing.svg';
3
7
  import linkSVG from '@plone/volto/icons/link.svg';
4
8
  import calendarSVG from '@plone/volto/icons/calendar.svg';
@@ -77,4 +81,19 @@ describe('Content', () => {
77
81
  expect(getContentIcon('Custom', false)).toBe(fileSVG);
78
82
  });
79
83
  });
84
+
85
+ describe('getLanguageIndependentFields', () => {
86
+ it('returns the language independenr field', () => {
87
+ const schema = {
88
+ properties: {
89
+ lif: {
90
+ multilingual_options: {
91
+ language_independent: true,
92
+ },
93
+ },
94
+ },
95
+ };
96
+ expect(getLanguageIndependentFields(schema)).toStrictEqual(['lif']);
97
+ });
98
+ });
80
99
  });
@@ -61,7 +61,7 @@ const widgetValidation = {
61
61
  },
62
62
  url: {
63
63
  isValidURL: (urlValue, urlObj, intlFunc) => {
64
- const urlRegex = /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gm;
64
+ const urlRegex = /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([_.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gm;
65
65
  const isValid = urlRegex.test(urlValue);
66
66
  return !isValid ? intlFunc(messages.isValidURL) : null;
67
67
  },
@@ -0,0 +1,97 @@
1
+ const SECOND = 1000;
2
+ const MINUTE = SECOND * 60;
3
+ const HOUR = MINUTE * 60;
4
+ const DAY = HOUR * 24;
5
+ const MONTH = DAY * 30;
6
+ const YEAR = DAY * 365; // ? is this safe or should it be more accurate
7
+
8
+ export const short_date_format = {
9
+ // 12/9/2021
10
+ year: 'numeric',
11
+ month: 'numeric',
12
+ day: 'numeric',
13
+ };
14
+
15
+ export const short_date_and_time_format = {
16
+ // 12/9/21, 10:39 AM
17
+ dateStyle: 'short',
18
+ timeStyle: 'short',
19
+ };
20
+
21
+ export const long_date_format = {
22
+ // Thursday, December 9, 2021 at 10:39 AM
23
+ dateStyle: 'full',
24
+ timeStyle: 'short',
25
+ };
26
+
27
+ export const toDate = (d) =>
28
+ ['string', 'number'].includes(typeof d) ? new Date(d) : d;
29
+
30
+ /**
31
+ * Friendly formatting for dates
32
+ */
33
+ export function formatDate({
34
+ date, // Date() or '2022-01-03T19:26:08.999Z'
35
+ format, // format object, see https://tc39.es/ecma402/#datetimeformat-objects
36
+ locale = 'en',
37
+ long, // true if format should be in long readable form.
38
+ includeTime, // true if short date format should include time
39
+ formatToParts = false,
40
+ }) {
41
+ date = toDate(date);
42
+ format = format
43
+ ? format
44
+ : long && !includeTime
45
+ ? long_date_format
46
+ : includeTime
47
+ ? short_date_and_time_format
48
+ : short_date_format;
49
+
50
+ const formatter = new Intl.DateTimeFormat(locale, format);
51
+ return formatToParts ? formatter.formatToParts(date) : formatter.format(date);
52
+ }
53
+
54
+ export function formatRelativeDate({
55
+ date,
56
+ locale = 'en',
57
+ relativeTo,
58
+ style = 'long', // long|short|narrow
59
+ formatToParts = false,
60
+ }) {
61
+ date = toDate(date);
62
+ relativeTo = toDate(relativeTo || new Date());
63
+
64
+ const deltaMiliTime = date.getTime() - relativeTo.getTime();
65
+ const absDeltaMiliTime = Math.abs(deltaMiliTime);
66
+
67
+ const deltaSeconds = absDeltaMiliTime / SECOND;
68
+ const deltaMinutes = absDeltaMiliTime / MINUTE;
69
+ const deltaHours = absDeltaMiliTime / HOUR;
70
+ const deltaDays = absDeltaMiliTime / DAY;
71
+ const deltaMonths = absDeltaMiliTime / MONTH;
72
+ const deltaYears = absDeltaMiliTime / YEAR;
73
+ const deltas = [
74
+ deltaYears,
75
+ deltaMonths,
76
+ deltaDays,
77
+ deltaHours,
78
+ deltaMinutes,
79
+ deltaSeconds,
80
+ ];
81
+ const pos = deltas.map(Math.round).findIndex((d) => d > 0);
82
+ const tag = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'][pos];
83
+
84
+ const formatter = new Intl.RelativeTimeFormat(locale, {
85
+ numeric: 'auto',
86
+ style,
87
+ });
88
+
89
+ const v = Math.round(deltaMiliTime < 0 ? -1 * deltas[pos] : deltas[pos]);
90
+ // console.log({ date, relativeTo, v });
91
+
92
+ return isNaN(v)
93
+ ? ''
94
+ : formatToParts
95
+ ? formatter.formatToParts(v, tag)
96
+ : formatter.format(v, tag); // use "now" ?
97
+ }
@@ -0,0 +1,197 @@
1
+ import { formatDate, formatRelativeDate } from './Date';
2
+
3
+ const d = '2022-01-03T19:26:08.999Z';
4
+ const date = new Date(d);
5
+
6
+ const SECOND = 1000;
7
+ const MINUTE = SECOND * 60;
8
+ const HOUR = MINUTE * 60;
9
+ const DAY = HOUR * 24;
10
+ const MONTH = DAY * 30;
11
+ const YEAR = DAY * 365; // ? is this safe or should it be more accurate
12
+
13
+ describe('formatDate helper', () => {
14
+ it('accepts an iso date string', () => {
15
+ expect(formatDate({ date: d })).toBe('1/3/2022');
16
+ });
17
+
18
+ it('accepts a date object', () => {
19
+ expect(formatDate({ date })).toBe('1/3/2022');
20
+ });
21
+
22
+ it('formats a date object in other language', () => {
23
+ expect(formatDate({ date, locale: 'de' })).toBe('3.1.2022');
24
+ });
25
+
26
+ it('formats a date object with time in default en locale', () => {
27
+ expect(formatDate({ date, includeTime: true })).toBe('1/3/22, 7:26 PM');
28
+ });
29
+
30
+ it('formats a date object with time in other language', () => {
31
+ expect(formatDate({ date, locale: 'de', includeTime: true })).toBe(
32
+ '03.01.22, 19:26',
33
+ );
34
+ });
35
+
36
+ it('formats a date as long', () => {
37
+ expect(formatDate({ date, long: true })).toBe(
38
+ 'Monday, January 3, 2022 at 7:26 PM',
39
+ );
40
+ });
41
+
42
+ it('formats a date as long in other language', () => {
43
+ expect(formatDate({ date, long: true, locale: 'de' })).toBe(
44
+ 'Montag, 3. Januar 2022 um 19:26',
45
+ );
46
+ });
47
+
48
+ it('includeTime takes precedence over long', () => {
49
+ expect(formatDate({ date, long: true, includeTime: true })).toBe(
50
+ '1/3/22, 7:26 PM',
51
+ );
52
+ });
53
+
54
+ it('accepts custom format', () => {
55
+ expect(
56
+ formatDate({
57
+ date,
58
+ format: {
59
+ year: 'numeric',
60
+ month: 'narrow',
61
+ day: '2-digit',
62
+ },
63
+ }),
64
+ ).toBe('J 03, 2022');
65
+ });
66
+
67
+ it('custom format takes precedence over long and includeTime', () => {
68
+ expect(
69
+ formatDate({
70
+ date,
71
+ long: true,
72
+ includeTime: true,
73
+ format: {
74
+ year: 'numeric',
75
+ month: 'narrow',
76
+ day: '2-digit',
77
+ // dayPeriod: 'long',
78
+ },
79
+ }),
80
+ ).toBe('J 03, 2022');
81
+ });
82
+ });
83
+
84
+ describe('formatRelativeDate helper', () => {
85
+ it('accepts an iso date string', () => {
86
+ expect(formatRelativeDate({ date: d })).toBeTruthy();
87
+ });
88
+
89
+ it('accepts date object', () => {
90
+ expect(formatRelativeDate({ date })).toBeTruthy();
91
+ });
92
+
93
+ it('uses auto numeric to format close past days', () => {
94
+ const now = Date.now();
95
+ const d = new Date(now - 1 * DAY);
96
+ expect(formatRelativeDate({ date: d })).toBe('yesterday');
97
+ });
98
+
99
+ it('uses auto numeric to format close future days', () => {
100
+ const now = Date.now();
101
+ const d = new Date(now + 1 * DAY);
102
+ expect(formatRelativeDate({ date: d })).toBe('tomorrow');
103
+ });
104
+
105
+ it('uses auto numeric to format close future years', () => {
106
+ const now = Date.now();
107
+ const d = new Date(now + 1 * YEAR);
108
+ expect(formatRelativeDate({ date: d })).toBe('next year');
109
+ });
110
+
111
+ it('uses auto numeric to format close last years', () => {
112
+ const now = Date.now();
113
+ const d = new Date(now - 1 * YEAR);
114
+ expect(formatRelativeDate({ date: d })).toBe('last year');
115
+ });
116
+
117
+ it('accepts a relativeTo date', () => {
118
+ const relativeTo = new Date(date.getTime() + 4 * DAY);
119
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 days ago');
120
+ });
121
+
122
+ it('can format past seconds', () => {
123
+ const relativeTo = new Date(date.getTime() + 4 * SECOND);
124
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 seconds ago');
125
+ });
126
+
127
+ it('can format future seconds', () => {
128
+ const relativeTo = new Date(date.getTime() - 4 * SECOND);
129
+ expect(formatRelativeDate({ date, relativeTo })).toBe('in 4 seconds');
130
+ });
131
+
132
+ it('can format past minutes', () => {
133
+ const relativeTo = new Date(date.getTime() + 4 * MINUTE);
134
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 minutes ago');
135
+ });
136
+
137
+ it('can format future minutes', () => {
138
+ const relativeTo = new Date(date.getTime() - 4 * MINUTE);
139
+ expect(formatRelativeDate({ date, relativeTo })).toBe('in 4 minutes');
140
+ });
141
+
142
+ it('can format past hours', () => {
143
+ const relativeTo = new Date(date.getTime() + 4 * HOUR);
144
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 hours ago');
145
+ });
146
+
147
+ it('can format future hours', () => {
148
+ const relativeTo = new Date(date.getTime() - 4 * HOUR);
149
+ expect(formatRelativeDate({ date, relativeTo })).toBe('in 4 hours');
150
+ });
151
+
152
+ it('can format past days', () => {
153
+ const relativeTo = new Date(date.getTime() + 4 * DAY);
154
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 days ago');
155
+ });
156
+
157
+ it('can format future days', () => {
158
+ const relativeTo = new Date(date.getTime() - 4 * DAY);
159
+ expect(formatRelativeDate({ date, relativeTo })).toBe('in 4 days');
160
+ });
161
+
162
+ it('can format past months', () => {
163
+ const relativeTo = new Date(date.getTime() + 4 * MONTH);
164
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 months ago');
165
+ });
166
+
167
+ it('can format future months', () => {
168
+ const relativeTo = new Date(date.getTime() - 4 * MONTH);
169
+ expect(formatRelativeDate({ date, relativeTo })).toBe('in 4 months');
170
+ });
171
+
172
+ it('can format past years', () => {
173
+ const relativeTo = new Date(
174
+ date.getTime() + 4 * YEAR + 4 * MONTH + 4 * DAY,
175
+ );
176
+ expect(formatRelativeDate({ date, relativeTo })).toBe('4 years ago');
177
+ });
178
+
179
+ it('can format future years', () => {
180
+ const relativeTo = new Date(
181
+ date.getTime() - 4 * YEAR - +4 * MONTH - 4 * DAY,
182
+ );
183
+ expect(formatRelativeDate({ date, relativeTo })).toBe('in 4 years');
184
+ });
185
+
186
+ it('can use alternate style short', () => {
187
+ const now = Date.now();
188
+ const d = new Date(now + 3 * MONTH);
189
+ expect(formatRelativeDate({ date: d, style: 'short' })).toBe('in 3 mo.');
190
+ });
191
+
192
+ it('can use alternate style narrow', () => {
193
+ const now = Date.now();
194
+ const d = new Date(now + 3 * MONTH);
195
+ expect(formatRelativeDate({ date: d, style: 'narrow' })).toBe('in 3 mo.');
196
+ });
197
+ });
@@ -1,7 +1,6 @@
1
1
  import { flatten, isEqual, isObject, transform } from 'lodash';
2
2
  import React from 'react';
3
3
  import { matchPath } from 'react-router';
4
- import moment from 'moment';
5
4
  import config from '@plone/volto/registry';
6
5
 
7
6
  /**
@@ -152,7 +151,7 @@ export const getColor = (name) => {
152
151
  * @param {string} format Date format of choice
153
152
  * @returns {Object|string} Moment object or string if format is set
154
153
  */
155
- export const parseDateTime = (locale, value, format) => {
154
+ export const parseDateTime = (locale, value, format, moment) => {
156
155
  // Used to set a server timezone or UTC as default
157
156
  moment.defineLocale(locale, moment.localeData(locale)._config); // copy locale to moment-timezone
158
157
  let datetime = null;