@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.
- package/CHANGELOG.md +48 -0
- package/addon-registry.js +15 -2
- package/locales/ca/LC_MESSAGES/volto.po +5 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +5 -0
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +5 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +5 -0
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +5 -0
- package/locales/eu.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +5 -0
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +10 -5
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +5 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +5 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +5 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +5 -0
- package/locales/ro.json +1 -1
- package/locales/volto.pot +6 -1
- package/package.json +4 -2
- package/razzle.config.js +1 -1
- package/src/components/index.js +2 -0
- package/src/components/manage/Add/Add.jsx +14 -3
- package/src/components/manage/Blocks/Block/BlocksForm.test.jsx +6 -0
- package/src/components/manage/Blocks/Listing/ListingBody.jsx +6 -0
- package/src/components/manage/Contents/Contents.jsx +15 -6
- package/src/components/manage/Contents/Contents.test.jsx +0 -7
- package/src/components/manage/Contents/ContentsIndexHeader.jsx +46 -34
- package/src/components/manage/Contents/ContentsItem.jsx +69 -61
- package/src/components/manage/Contents/ContentsUploadModal.jsx +2 -4
- package/src/components/manage/Controlpanels/ModerateComments.jsx +7 -5
- package/src/components/manage/Diff/Diff.jsx +13 -5
- package/src/components/manage/Diff/Diff.test.jsx +0 -6
- package/src/components/manage/Diff/DiffField.jsx +12 -3
- package/src/components/manage/Diff/DiffField.test.jsx +0 -6
- package/src/components/manage/DragDropList/DragDropList.jsx +4 -2
- package/src/components/manage/Form/Field.jsx +12 -2
- package/src/components/manage/History/History.jsx +6 -5
- package/src/components/manage/History/History.test.jsx +12 -7
- package/src/components/manage/Widgets/DatetimeWidget.jsx +10 -3
- package/src/components/manage/Widgets/DatetimeWidget.test.jsx +2 -2
- package/src/components/manage/Widgets/FormFieldWrapper.jsx +14 -1
- package/src/components/manage/Widgets/ObjectListWidget.stories.js +3 -3
- package/src/components/manage/Widgets/ObjectListWidget.test.js +6 -0
- package/src/components/manage/Widgets/RecurrenceWidget/ByDayField.jsx +4 -3
- package/src/components/manage/Widgets/RecurrenceWidget/MonthOfTheYearField.jsx +10 -3
- package/src/components/manage/Widgets/RecurrenceWidget/Occurences.jsx +8 -4
- package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +21 -13
- package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.test.jsx +6 -0
- package/src/components/manage/Widgets/RecurrenceWidget/Utils.js +1 -2
- package/src/components/manage/Widgets/RecurrenceWidget/WeekdayOfTheMonthField.jsx +5 -3
- package/src/components/manage/Widgets/SchemaWidget.jsx +4 -2
- package/src/components/manage/Widgets/SchemaWidget.test.jsx +6 -0
- package/src/components/manage/Widgets/SchemaWidgetFieldset.jsx +8 -4
- package/src/components/manage/Widgets/SchemaWidgetFieldset.test.jsx +7 -1
- package/src/components/manage/Widgets/VocabularyTermsWidget.jsx +41 -7
- package/src/components/manage/Widgets/VocabularyTermsWidget.stories.js +6 -21
- package/src/components/manage/Widgets/VocabularyTermsWidget.test.jsx +6 -0
- package/src/components/theme/Comments/Comments.jsx +3 -1
- package/src/components/theme/Comments/Comments.test.jsx +6 -0
- package/src/components/theme/FormattedDate/FormattedDate.jsx +42 -0
- package/src/components/theme/FormattedDate/FormattedDate.stories.jsx +91 -0
- package/src/components/theme/FormattedDate/FormattedRelativeDate.jsx +57 -0
- package/src/components/theme/FormattedDate/FormattedRelativeDate.stories.jsx +122 -0
- package/src/components/theme/Navigation/Navigation.jsx +1 -1
- package/src/components/theme/View/EventDatesInfo.jsx +12 -6
- package/src/components/theme/View/EventDatesInfo.test.jsx +6 -0
- package/src/components/theme/View/EventView.test.jsx +6 -0
- package/src/config/Loadables.jsx +3 -0
- package/src/config/index.js +1 -0
- package/src/helpers/Content/Content.js +16 -0
- package/src/helpers/Content/Content.test.js +20 -1
- package/src/helpers/FormValidation/FormValidation.js +1 -1
- package/src/helpers/Utils/Date.js +97 -0
- package/src/helpers/Utils/Date.test.js +197 -0
- package/src/helpers/Utils/Utils.js +1 -2
- package/src/helpers/Utils/Utils.test.js +25 -0
- package/src/helpers/index.js +1 -0
- package/theme/themes/pastanaga/extras/widgets.less +12 -0
- 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
|
-
|
|
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
|
|
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', () => {
|
package/src/config/Loadables.jsx
CHANGED
|
@@ -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
|
};
|
package/src/config/index.js
CHANGED
|
@@ -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 {
|
|
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;
|