@plone/volto 19.0.0-alpha.36 → 19.0.0-alpha.38
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/README.md +1 -1
- package/locales/af/LC_MESSAGES/volto.po +29 -3
- package/locales/af.json +1 -1
- package/locales/ar/LC_MESSAGES/volto.po +29 -3
- package/locales/ar.json +1 -1
- package/locales/bg/LC_MESSAGES/volto.po +29 -3
- package/locales/bg.json +1 -1
- package/locales/bn/LC_MESSAGES/volto.po +29 -3
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +32 -6
- package/locales/ca.json +1 -1
- package/locales/cs/LC_MESSAGES/volto.po +30 -4
- package/locales/cs.json +1 -1
- package/locales/cy/LC_MESSAGES/volto.po +29 -3
- package/locales/cy.json +1 -1
- package/locales/da/LC_MESSAGES/volto.po +29 -3
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +32 -6
- package/locales/de.json +1 -1
- package/locales/el/LC_MESSAGES/volto.po +29 -3
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +25 -0
- package/locales/en.json +1 -1
- package/locales/en_AU/LC_MESSAGES/volto.po +29 -3
- package/locales/en_AU.json +1 -1
- package/locales/en_GB/LC_MESSAGES/volto.po +29 -3
- package/locales/en_GB.json +1 -1
- package/locales/eo/LC_MESSAGES/volto.po +29 -3
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +30 -5
- package/locales/es.json +1 -1
- package/locales/et/LC_MESSAGES/volto.po +29 -3
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +30 -5
- package/locales/eu.json +1 -1
- package/locales/fa/LC_MESSAGES/volto.po +29 -3
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +30 -4
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +208 -183
- package/locales/fr.json +1 -1
- package/locales/fu/LC_MESSAGES/volto.po +29 -3
- package/locales/fu.json +1 -1
- package/locales/gl/LC_MESSAGES/volto.po +27 -2
- package/locales/gl.json +1 -1
- package/locales/he/LC_MESSAGES/volto.po +29 -3
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +34 -8
- package/locales/hi.json +1 -1
- package/locales/hr/LC_MESSAGES/volto.po +30 -4
- package/locales/hr.json +1 -1
- package/locales/hu/LC_MESSAGES/volto.po +29 -3
- package/locales/hu.json +1 -1
- package/locales/hy/LC_MESSAGES/volto.po +29 -3
- package/locales/hy.json +1 -1
- package/locales/id/LC_MESSAGES/volto.po +29 -3
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +29 -4
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +29 -3
- package/locales/ja.json +1 -1
- package/locales/ka/LC_MESSAGES/volto.po +29 -3
- package/locales/ka.json +1 -1
- package/locales/kn/LC_MESSAGES/volto.po +29 -3
- package/locales/kn.json +1 -1
- package/locales/ko/LC_MESSAGES/volto.po +29 -3
- package/locales/ko.json +1 -1
- package/locales/lt/LC_MESSAGES/volto.po +30 -4
- package/locales/lt.json +1 -1
- package/locales/lv/LC_MESSAGES/volto.po +29 -3
- package/locales/lv.json +1 -1
- package/locales/mi/LC_MESSAGES/volto.po +29 -3
- package/locales/mi.json +1 -1
- package/locales/mk/LC_MESSAGES/volto.po +29 -3
- package/locales/mk.json +1 -1
- package/locales/my/LC_MESSAGES/volto.po +29 -3
- package/locales/my.json +1 -1
- package/locales/nb_NO/LC_MESSAGES/volto.po +29 -3
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +69 -43
- package/locales/nl.json +1 -1
- package/locales/nn/LC_MESSAGES/volto.po +29 -3
- package/locales/nn.json +1 -1
- package/locales/pl/LC_MESSAGES/volto.po +30 -4
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +30 -4
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +54 -29
- package/locales/pt_BR.json +1 -1
- package/locales/rm/LC_MESSAGES/volto.po +29 -3
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +30 -5
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +30 -4
- package/locales/ru.json +1 -1
- package/locales/sk/LC_MESSAGES/volto.po +30 -4
- package/locales/sk.json +1 -1
- package/locales/sl/LC_MESSAGES/volto.po +29 -3
- package/locales/sl.json +1 -1
- package/locales/sm/LC_MESSAGES/volto.po +29 -3
- package/locales/sm.json +1 -1
- package/locales/sq/LC_MESSAGES/volto.po +29 -3
- package/locales/sq.json +1 -1
- package/locales/sr/LC_MESSAGES/volto.po +30 -4
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl/LC_MESSAGES/volto.po +29 -3
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn/LC_MESSAGES/volto.po +29 -3
- package/locales/sr@latn.json +1 -1
- package/locales/sv/LC_MESSAGES/volto.po +31 -5
- package/locales/sv.json +1 -1
- package/locales/ta/LC_MESSAGES/volto.po +30 -5
- package/locales/ta.json +1 -1
- package/locales/te/LC_MESSAGES/volto.po +29 -3
- package/locales/te.json +1 -1
- package/locales/th/LC_MESSAGES/volto.po +29 -3
- package/locales/th.json +1 -1
- package/locales/to/LC_MESSAGES/volto.po +29 -3
- package/locales/to.json +1 -1
- package/locales/tr/LC_MESSAGES/volto.po +29 -4
- package/locales/tr.json +1 -1
- package/locales/uk/LC_MESSAGES/volto.po +30 -4
- package/locales/uk.json +1 -1
- package/locales/vi/LC_MESSAGES/volto.po +29 -3
- package/locales/vi.json +1 -1
- package/locales/volto.pot +26 -1
- package/locales/zh_CN/LC_MESSAGES/volto.po +29 -4
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant/LC_MESSAGES/volto.po +29 -3
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +29 -3
- package/locales/zh_Hant_HK.json +1 -1
- package/package.json +9 -9
- package/src/components/manage/BlockChooser/BlockChooser.jsx +7 -10
- package/src/components/manage/Blocks/Block/Edit.jsx +19 -10
- package/src/components/manage/Blocks/Block/Order/Item.jsx +9 -4
- package/src/components/manage/Contents/DropZoneContent.jsx +1 -0
- package/src/components/manage/Controlpanels/BlockType.tsx +2 -3
- package/src/components/manage/Controlpanels/ContentTypes.jsx +9 -2
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +58 -5
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.ssr.test.jsx +624 -0
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.test.jsx +8 -0
- package/src/components/manage/Form/Form.jsx +6 -1
- package/src/components/manage/Form/ModalForm.jsx +164 -88
- package/src/components/manage/Sidebar/ObjectBrowser.jsx +7 -0
- package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +7 -3
- package/src/components/manage/Sidebar/ObjectBrowserBody.test.jsx +52 -0
- package/src/components/manage/Sidebar/Sidebar.jsx +2 -0
- package/src/components/manage/Toolbar/Toolbar.jsx +89 -7
- package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +1 -0
- package/src/components/manage/Widgets/TokenWidget.jsx +142 -186
- package/src/components/theme/Search/Search.jsx +230 -327
- package/src/components/theme/Search/Search.test.jsx +5 -10
- package/src/components/theme/Sitemap/Sitemap.jsx +22 -30
- package/src/components/theme/Sitemap/Sitemap.test.jsx +18 -0
- package/src/components/theme/Widgets/DateWidget.jsx +4 -5
- package/src/components/theme/Widgets/DatetimeWidget.jsx +4 -5
- package/src/components/theme/Widgets/RichTextWidget.jsx +1 -1
- package/src/config/index.js +1 -0
- package/src/helpers/I18n/I18n.test.ts +44 -0
- package/src/helpers/I18n/I18n.ts +31 -0
- package/src/helpers/Utils/Date.js +26 -1
- package/src/helpers/Utils/Date.test.js +237 -0
- package/src/helpers/index.js +1 -0
- package/theme/themes/pastanaga/collections/form.overrides +21 -0
- package/theme/themes/pastanaga/elements/button.overrides +30 -3
- package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
- package/types/components/manage/Controlpanels/Users/UsersControlpanel.d.ts +2 -6
- package/types/components/manage/Controlpanels/Users/UsersControlpanel.ssr.test.d.ts +1 -0
- package/types/components/manage/Controlpanels/index.d.ts +1 -1
- package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
- package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
- package/types/components/manage/Sidebar/ObjectBrowserBody.test.d.ts +1 -0
- package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
- package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
- package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
- package/types/components/manage/Widgets/index.d.ts +2 -2
- package/types/components/theme/Search/Search.d.ts +1 -1
- package/types/helpers/I18n/I18n.d.ts +20 -0
- package/types/helpers/index.d.ts +1 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { compose } from 'redux';
|
|
4
|
-
import { connect } from 'react-redux';
|
|
4
|
+
import { connect, useSelector } from 'react-redux';
|
|
5
5
|
import { asyncConnect } from '@plone/volto/helpers/AsyncConnect';
|
|
6
|
+
import { expandToBackendURL } from '@plone/volto/helpers/Url/Url';
|
|
6
7
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
7
8
|
import { Container as SemanticContainer } from 'semantic-ui-react';
|
|
8
9
|
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
9
|
-
import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
|
|
10
10
|
import { Link } from 'react-router-dom';
|
|
11
11
|
import config from '@plone/volto/registry';
|
|
12
12
|
|
|
@@ -20,6 +20,9 @@ const messages = defineMessages({
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
export function getSitemapPath(pathname = '', lang) {
|
|
23
|
+
/* This function is deprecated
|
|
24
|
+
* We keep it for backwards compatibility.
|
|
25
|
+
*/
|
|
23
26
|
const prefix = pathname.replace(/\/sitemap$/gm, '').replace(/^\//, '');
|
|
24
27
|
const path = prefix || lang || '';
|
|
25
28
|
return path;
|
|
@@ -32,18 +35,12 @@ export function getSitemapPath(pathname = '', lang) {
|
|
|
32
35
|
* @returns {JSX.Element} - Rendered component.
|
|
33
36
|
*/
|
|
34
37
|
function Sitemap(props) {
|
|
35
|
-
const {
|
|
36
|
-
location: { pathname },
|
|
37
|
-
lang,
|
|
38
|
-
getNavigation,
|
|
39
|
-
isMultilingual,
|
|
40
|
-
} = props;
|
|
41
|
-
|
|
38
|
+
const { getNavigation, navroot } = props;
|
|
42
39
|
useEffect(() => {
|
|
43
|
-
const
|
|
44
|
-
const path =
|
|
45
|
-
getNavigation(path,
|
|
46
|
-
}, [
|
|
40
|
+
const { settings } = config;
|
|
41
|
+
const path = `${expandToBackendURL(navroot?.navroot?.['@id'])}`;
|
|
42
|
+
getNavigation(path, settings.siteMapDepth);
|
|
43
|
+
}, [navroot, getNavigation]);
|
|
47
44
|
|
|
48
45
|
const renderItems = (items) => {
|
|
49
46
|
return (
|
|
@@ -64,12 +61,13 @@ function Sitemap(props) {
|
|
|
64
61
|
const Container =
|
|
65
62
|
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
66
63
|
|
|
64
|
+
const items = useSelector((state) => state.navigation.items);
|
|
67
65
|
return (
|
|
68
66
|
<div id="page-sitemap">
|
|
69
67
|
<Helmet title={props.intl.formatMessage(messages.Sitemap)} />
|
|
70
68
|
<Container className="view-wrapper">
|
|
71
69
|
<h1>{props.intl.formatMessage(messages.Sitemap)} </h1>
|
|
72
|
-
{
|
|
70
|
+
{items && renderItems(items)}
|
|
73
71
|
</Container>
|
|
74
72
|
</div>
|
|
75
73
|
);
|
|
@@ -77,18 +75,15 @@ function Sitemap(props) {
|
|
|
77
75
|
|
|
78
76
|
Sitemap.propTypes = {
|
|
79
77
|
getNavigation: PropTypes.func.isRequired,
|
|
80
|
-
|
|
78
|
+
navroot: PropTypes.object.isRequired,
|
|
81
79
|
intl: PropTypes.object.isRequired,
|
|
82
|
-
lang: PropTypes.string.isRequired,
|
|
83
|
-
items: PropTypes.array.isRequired,
|
|
84
80
|
};
|
|
85
81
|
|
|
86
82
|
export const __test__ = compose(
|
|
87
83
|
injectIntl,
|
|
88
84
|
connect(
|
|
89
85
|
(state) => ({
|
|
90
|
-
|
|
91
|
-
lang: state.intl.locale,
|
|
86
|
+
navroot: state.navroot?.data,
|
|
92
87
|
}),
|
|
93
88
|
{ getNavigation },
|
|
94
89
|
),
|
|
@@ -98,25 +93,22 @@ export default compose(
|
|
|
98
93
|
injectIntl,
|
|
99
94
|
connect(
|
|
100
95
|
(state) => ({
|
|
101
|
-
|
|
102
|
-
lang: state.intl.locale,
|
|
103
|
-
isMultilingual: state.site.data.features?.multilingual,
|
|
96
|
+
navroot: state.navroot?.data,
|
|
104
97
|
}),
|
|
105
98
|
{ getNavigation },
|
|
106
99
|
),
|
|
107
100
|
asyncConnect([
|
|
108
101
|
{
|
|
109
102
|
key: 'navigation',
|
|
110
|
-
promise: ({
|
|
103
|
+
promise: ({ store: { dispatch, getState } }) => {
|
|
111
104
|
if (!__SERVER__) return;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
const navroot = getState().navroot.data?.navroot?.['@id'];
|
|
106
|
+
return dispatch(
|
|
107
|
+
getNavigation(
|
|
108
|
+
expandToBackendURL(navroot),
|
|
109
|
+
config.settings.siteMapDepth,
|
|
110
|
+
),
|
|
118
111
|
);
|
|
119
|
-
return dispatch(getNavigation(path, 4));
|
|
120
112
|
},
|
|
121
113
|
},
|
|
122
114
|
]),
|
|
@@ -43,6 +43,15 @@ describe('Sitemap', () => {
|
|
|
43
43
|
locale: 'en',
|
|
44
44
|
messages: {},
|
|
45
45
|
},
|
|
46
|
+
navroot: {
|
|
47
|
+
data: {
|
|
48
|
+
navroot: {
|
|
49
|
+
'@id': `http://localhost:8080/Plone/`,
|
|
50
|
+
'@type': 'Plone Site',
|
|
51
|
+
title: 'Plone Site',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
46
55
|
});
|
|
47
56
|
const component = renderer.create(
|
|
48
57
|
<Provider store={store}>
|
|
@@ -93,6 +102,15 @@ describe('Sitemap in a multilingual site', () => {
|
|
|
93
102
|
locale: 'en',
|
|
94
103
|
messages: {},
|
|
95
104
|
},
|
|
105
|
+
navroot: {
|
|
106
|
+
data: {
|
|
107
|
+
navroot: {
|
|
108
|
+
'@id': `http://localhost:8080/Plone/en`,
|
|
109
|
+
'@type': 'LRF',
|
|
110
|
+
title: 'English',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
96
114
|
});
|
|
97
115
|
const component = renderer.create(
|
|
98
116
|
<Provider store={store}>
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import moment from 'moment';
|
|
4
3
|
import { useSelector } from 'react-redux';
|
|
5
4
|
import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
|
|
5
|
+
import { formatDate } from '@plone/volto/helpers/Utils/Date';
|
|
6
6
|
|
|
7
7
|
const DateWidget = ({ value, children, className, format = 'll' }) => {
|
|
8
8
|
const lang = useSelector((state) => state.intl.locale);
|
|
9
|
-
|
|
9
|
+
const locale = toBackendLang(lang);
|
|
10
|
+
const formatted = formatDate({ date: value, format, locale });
|
|
10
11
|
return value ? (
|
|
11
12
|
<span className={cx(className, 'date', 'widget')}>
|
|
12
|
-
{children
|
|
13
|
-
? children(moment(value).format(format))
|
|
14
|
-
: moment(value).format(format)}
|
|
13
|
+
{children ? children(formatted) : formatted}
|
|
15
14
|
</span>
|
|
16
15
|
) : (
|
|
17
16
|
''
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import moment from 'moment';
|
|
4
3
|
import { useSelector } from 'react-redux';
|
|
5
4
|
import { toBackendLang } from '@plone/volto/helpers/Utils/Utils';
|
|
5
|
+
import { formatDate } from '@plone/volto/helpers/Utils/Date';
|
|
6
6
|
|
|
7
7
|
const DatetimeWidget = ({ value, children, className, format = 'lll' }) => {
|
|
8
8
|
const lang = useSelector((state) => state.intl.locale);
|
|
9
|
-
|
|
9
|
+
const locale = toBackendLang(lang);
|
|
10
|
+
const formatted = formatDate({ date: value, format, locale });
|
|
10
11
|
return value ? (
|
|
11
12
|
<span className={cx(className, 'datetime', 'widget')}>
|
|
12
|
-
{children
|
|
13
|
-
? children(moment(value).format(format))
|
|
14
|
-
: moment(value).format(format)}
|
|
13
|
+
{children ? children(formatted) : formatted}
|
|
15
14
|
</span>
|
|
16
15
|
) : (
|
|
17
16
|
''
|
package/src/config/index.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createIntl, createIntlCache } from 'react-intl';
|
|
2
|
+
import type { IntlShape } from 'react-intl';
|
|
3
|
+
import { formatMessageWithFallback } from './I18n';
|
|
4
|
+
|
|
5
|
+
const buildIntl = (
|
|
6
|
+
locale: string,
|
|
7
|
+
messages: Record<string, string>,
|
|
8
|
+
): IntlShape => createIntl({ locale, messages }, createIntlCache());
|
|
9
|
+
|
|
10
|
+
describe('formatMessageWithFallback', () => {
|
|
11
|
+
it('returns the translated string when the locale catalog has an entry', () => {
|
|
12
|
+
const intl = buildIntl('pt-BR', { Image: 'Imagem' });
|
|
13
|
+
expect(formatMessageWithFallback(intl, 'Image')).toBe('Imagem');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('falls back to the input message when no translation is registered', () => {
|
|
17
|
+
const intl = buildIntl('pt-BR', {});
|
|
18
|
+
expect(formatMessageWithFallback(intl, 'Image')).toBe('Image');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns undefined unchanged', () => {
|
|
22
|
+
const intl = buildIntl('en', {});
|
|
23
|
+
expect(formatMessageWithFallback(intl, undefined)).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null unchanged', () => {
|
|
27
|
+
const intl = buildIntl('en', {});
|
|
28
|
+
expect(formatMessageWithFallback(intl, null)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns an empty string unchanged', () => {
|
|
32
|
+
const intl = buildIntl('en', {});
|
|
33
|
+
expect(formatMessageWithFallback(intl, '')).toBe('');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not call intl.formatMessage when the message is falsy', () => {
|
|
37
|
+
const intl = buildIntl('en', {});
|
|
38
|
+
const spy = vi.spyOn(intl, 'formatMessage');
|
|
39
|
+
formatMessageWithFallback(intl, undefined);
|
|
40
|
+
formatMessageWithFallback(intl, null);
|
|
41
|
+
formatMessageWithFallback(intl, '');
|
|
42
|
+
expect(spy).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18n helpers.
|
|
3
|
+
* @module helpers/I18n/I18n
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IntlShape } from 'react-intl';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format a `react-intl` message using a plain string as both the id and the
|
|
10
|
+
* default fallback. Useful when a UI value (e.g. a block title configured as
|
|
11
|
+
* a plain string) should be looked up in the locale catalog when available,
|
|
12
|
+
* and rendered as-is otherwise.
|
|
13
|
+
*
|
|
14
|
+
* Returns the input untouched when it is falsy, so the result can be used
|
|
15
|
+
* directly in `||` fallback chains.
|
|
16
|
+
*
|
|
17
|
+
* @param intl react-intl `intl` instance, typically from `useIntl()`.
|
|
18
|
+
* @param message The string to translate; used as both `id` and
|
|
19
|
+
* `defaultMessage`.
|
|
20
|
+
* @returns Translated text, or `message` unchanged if falsy / no translation.
|
|
21
|
+
*/
|
|
22
|
+
export function formatMessageWithFallback<T extends string | undefined | null>(
|
|
23
|
+
intl: IntlShape,
|
|
24
|
+
message: T,
|
|
25
|
+
): T extends string ? string : T {
|
|
26
|
+
if (!message) return message as T extends string ? string : T;
|
|
27
|
+
return intl.formatMessage({
|
|
28
|
+
id: message,
|
|
29
|
+
defaultMessage: message,
|
|
30
|
+
}) as T extends string ? string : T;
|
|
31
|
+
}
|
|
@@ -5,6 +5,21 @@ const DAY = HOUR * 24;
|
|
|
5
5
|
const MONTH = DAY * 30;
|
|
6
6
|
const YEAR = DAY * 365; // ? is this safe or should it be more accurate
|
|
7
7
|
|
|
8
|
+
// moment-style format tokens → Intl.DateTimeFormat options (for backward compat)
|
|
9
|
+
const MOMENT_FORMAT_MAP = {
|
|
10
|
+
ll: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
11
|
+
lll: {
|
|
12
|
+
year: 'numeric',
|
|
13
|
+
month: 'short',
|
|
14
|
+
day: 'numeric',
|
|
15
|
+
hour: 'numeric',
|
|
16
|
+
minute: 'numeric',
|
|
17
|
+
},
|
|
18
|
+
LLLL: { dateStyle: 'full', timeStyle: 'short' },
|
|
19
|
+
L: { year: 'numeric', month: '2-digit', day: '2-digit' },
|
|
20
|
+
LT: { timeStyle: 'short' },
|
|
21
|
+
};
|
|
22
|
+
|
|
8
23
|
export const short_date_format = {
|
|
9
24
|
// 12/9/2021
|
|
10
25
|
year: 'numeric',
|
|
@@ -32,13 +47,23 @@ export const toDate = (d) =>
|
|
|
32
47
|
*/
|
|
33
48
|
export function formatDate({
|
|
34
49
|
date, // Date() or '2022-01-03T19:26:08.999Z'
|
|
35
|
-
format, // format object,
|
|
50
|
+
format, // format object (Intl.DateTimeFormat options) or moment-style token string (e.g. 'll', 'lll', 'LLLL', 'L', 'LT')
|
|
36
51
|
locale = 'en',
|
|
37
52
|
long, // true if format should be in long readable form.
|
|
38
53
|
includeTime, // true if short date format should include time
|
|
39
54
|
formatToParts = false,
|
|
40
55
|
}) {
|
|
41
56
|
date = toDate(date);
|
|
57
|
+
// Resolve moment-style token strings to Intl format options
|
|
58
|
+
if (typeof format === 'string') {
|
|
59
|
+
if (process.env.NODE_ENV !== 'production' && !MOMENT_FORMAT_MAP[format]) {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.warn(
|
|
62
|
+
`[formatDate] Unknown format token "${format}". Supported tokens: ${Object.keys(MOMENT_FORMAT_MAP).join(', ')}. Falling back to short_date_format.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
format = MOMENT_FORMAT_MAP[format] || short_date_format;
|
|
66
|
+
}
|
|
42
67
|
format = format
|
|
43
68
|
? format
|
|
44
69
|
: long && !includeTime
|
|
@@ -79,6 +79,243 @@ describe('formatDate helper', () => {
|
|
|
79
79
|
}),
|
|
80
80
|
).toBe('J 03, 2022');
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
describe('moment-style format tokens', () => {
|
|
84
|
+
it('formats with "ll" token (short date)', () => {
|
|
85
|
+
expect(formatDate({ date, format: 'll' })).toBe('Jan 3, 2022');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('formats with "lll" token (short date + time)', () => {
|
|
89
|
+
expect(formatDate({ date, format: 'lll' })).toBe('Jan 3, 2022, 7:26 PM');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('formats with "LLLL" token (full date + time)', () => {
|
|
93
|
+
expect(formatDate({ date, format: 'LLLL' })).toBe(
|
|
94
|
+
'Monday, January 3, 2022 at 7:26 PM',
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('formats with "L" token (numeric short date)', () => {
|
|
99
|
+
expect(formatDate({ date, format: 'L' })).toBe('01/03/2022');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('formats with "LT" token (time only)', () => {
|
|
103
|
+
expect(formatDate({ date, format: 'LT' })).toBe('7:26 PM');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('moment token works with other locales', () => {
|
|
107
|
+
expect(formatDate({ date, format: 'll', locale: 'de' })).toBe(
|
|
108
|
+
'3. Jan. 2022',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('unknown string format falls back to short_date_format', () => {
|
|
113
|
+
expect(formatDate({ date, format: 'unknown' })).toBe('1/3/2022');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Italian locale (it)', () => {
|
|
118
|
+
it('formats a basic date', () => {
|
|
119
|
+
expect(formatDate({ date, locale: 'it' })).toBe('03/01/2022');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('formats a date with time', () => {
|
|
123
|
+
expect(formatDate({ date, locale: 'it', includeTime: true })).toBe(
|
|
124
|
+
'03/01/22, 19:26',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('formats a date as long', () => {
|
|
129
|
+
expect(formatDate({ date, locale: 'it', long: true })).toBe(
|
|
130
|
+
'lunedì 3 gennaio 2022 alle ore 19:26',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('formats with "ll" token', () => {
|
|
135
|
+
expect(formatDate({ date, format: 'll', locale: 'it' })).toBe(
|
|
136
|
+
'3 gen 2022',
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('formats with "lll" token', () => {
|
|
141
|
+
expect(formatDate({ date, format: 'lll', locale: 'it' })).toBe(
|
|
142
|
+
'3 gen 2022, 19:26',
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('formats with "LLLL" token', () => {
|
|
147
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'it' })).toBe(
|
|
148
|
+
'lunedì 3 gennaio 2022 alle ore 19:26',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('formats with "L" token', () => {
|
|
153
|
+
expect(formatDate({ date, format: 'L', locale: 'it' })).toBe(
|
|
154
|
+
'03/01/2022',
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('formats with "LT" token', () => {
|
|
159
|
+
expect(formatDate({ date, format: 'LT', locale: 'it' })).toBe('19:26');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('German locale (de)', () => {
|
|
164
|
+
it('formats a basic date', () => {
|
|
165
|
+
expect(formatDate({ date, locale: 'de' })).toBe('3.1.2022');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('formats a date with time', () => {
|
|
169
|
+
expect(formatDate({ date, locale: 'de', includeTime: true })).toBe(
|
|
170
|
+
'03.01.22, 19:26',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('formats a date as long', () => {
|
|
175
|
+
expect(formatDate({ date, locale: 'de', long: true })).toBe(
|
|
176
|
+
'Montag, 3. Januar 2022 um 19:26',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('formats with "ll" token', () => {
|
|
181
|
+
expect(formatDate({ date, format: 'll', locale: 'de' })).toBe(
|
|
182
|
+
'3. Jan. 2022',
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('formats with "lll" token', () => {
|
|
187
|
+
expect(formatDate({ date, format: 'lll', locale: 'de' })).toBe(
|
|
188
|
+
'3. Jan. 2022, 19:26',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('formats with "LLLL" token', () => {
|
|
193
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'de' })).toBe(
|
|
194
|
+
'Montag, 3. Januar 2022 um 19:26',
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('formats with "L" token', () => {
|
|
199
|
+
expect(formatDate({ date, format: 'L', locale: 'de' })).toBe(
|
|
200
|
+
'03.01.2022',
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('formats with "LT" token', () => {
|
|
205
|
+
expect(formatDate({ date, format: 'LT', locale: 'de' })).toBe('19:26');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Brazilian Portuguese locale (pt-BR)', () => {
|
|
210
|
+
it('formats a basic date', () => {
|
|
211
|
+
expect(formatDate({ date, locale: 'pt-BR' })).toBe('03/01/2022');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('formats a date with time', () => {
|
|
215
|
+
expect(formatDate({ date, locale: 'pt-BR', includeTime: true })).toBe(
|
|
216
|
+
'03/01/2022, 19:26',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('formats a date as long', () => {
|
|
221
|
+
expect(formatDate({ date, locale: 'pt-BR', long: true })).toBe(
|
|
222
|
+
'segunda-feira, 3 de janeiro de 2022 às 19:26',
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('formats with "ll" token', () => {
|
|
227
|
+
expect(formatDate({ date, format: 'll', locale: 'pt-BR' })).toBe(
|
|
228
|
+
'3 de jan. de 2022',
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('formats with "lll" token', () => {
|
|
233
|
+
expect(formatDate({ date, format: 'lll', locale: 'pt-BR' })).toBe(
|
|
234
|
+
'3 de jan. de 2022, 19:26',
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('formats with "LLLL" token', () => {
|
|
239
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'pt-BR' })).toBe(
|
|
240
|
+
'segunda-feira, 3 de janeiro de 2022 às 19:26',
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('formats with "L" token', () => {
|
|
245
|
+
expect(formatDate({ date, format: 'L', locale: 'pt-BR' })).toBe(
|
|
246
|
+
'03/01/2022',
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('formats with "LT" token', () => {
|
|
251
|
+
expect(formatDate({ date, format: 'LT', locale: 'pt-BR' })).toBe('19:26');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('Romanian locale (ro)', () => {
|
|
256
|
+
it('formats a basic date', () => {
|
|
257
|
+
expect(formatDate({ date, locale: 'ro' })).toBe('03.01.2022');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('formats a date with time', () => {
|
|
261
|
+
expect(formatDate({ date, locale: 'ro', includeTime: true })).toBe(
|
|
262
|
+
'03.01.2022, 19:26',
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('formats a date as long', () => {
|
|
267
|
+
expect(formatDate({ date, locale: 'ro', long: true })).toBe(
|
|
268
|
+
'luni, 3 ianuarie 2022 la 19:26',
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('formats with "ll" token', () => {
|
|
273
|
+
expect(formatDate({ date, format: 'll', locale: 'ro' })).toBe(
|
|
274
|
+
'3 ian. 2022',
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('formats with "lll" token', () => {
|
|
279
|
+
expect(formatDate({ date, format: 'lll', locale: 'ro' })).toBe(
|
|
280
|
+
'3 ian. 2022, 19:26',
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('formats with "LLLL" token', () => {
|
|
285
|
+
expect(formatDate({ date, format: 'LLLL', locale: 'ro' })).toBe(
|
|
286
|
+
'luni, 3 ianuarie 2022 la 19:26',
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('formats with "L" token', () => {
|
|
291
|
+
expect(formatDate({ date, format: 'L', locale: 'ro' })).toBe(
|
|
292
|
+
'03.01.2022',
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('formats with "LT" token', () => {
|
|
297
|
+
expect(formatDate({ date, format: 'LT', locale: 'ro' })).toBe('19:26');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// moment vs Intl comparison
|
|
302
|
+
// This section documents known differences between the former moment.js output
|
|
303
|
+
// and our Intl-based formatDate implementation. Reference values are hardcoded
|
|
304
|
+
// from moment@2.29.4 to avoid a runtime dependency on the library we are removing.
|
|
305
|
+
//
|
|
306
|
+
// Tokens that match moment exactly: ll, L, LT (most locales), LLLL (pt-BR only)
|
|
307
|
+
//
|
|
308
|
+
// Known differences from moment@2.29.4:
|
|
309
|
+
// - lll (all locales): Intl inserts a comma between date and time;
|
|
310
|
+
// moment did not (e.g. "Jan 3, 2022, 7:26 PM" vs "Jan 3, 2022 7:26 PM")
|
|
311
|
+
// - LLLL (en, it, de, ro): Intl inserts a locale-specific preposition before
|
|
312
|
+
// the time; moment did not
|
|
313
|
+
// en: "...at 7:26 PM" vs "...7:26 PM"
|
|
314
|
+
// it: "...alle ore 19:26" vs "...19:26"
|
|
315
|
+
// de: "...um 19:26" vs "...19:26"
|
|
316
|
+
// ro: "...la 19:26" vs "...19:26"
|
|
317
|
+
// - ll/lll (pt-BR): Intl adds a period after abbreviated month ("jan.");
|
|
318
|
+
// moment did not ("jan")
|
|
82
319
|
});
|
|
83
320
|
|
|
84
321
|
describe('formatRelativeDate helper', () => {
|
package/src/helpers/index.js
CHANGED
|
@@ -112,6 +112,7 @@ export {
|
|
|
112
112
|
normalizeString,
|
|
113
113
|
} from '@plone/volto/helpers/Utils/Utils';
|
|
114
114
|
export { messages } from './MessageLabels/MessageLabels';
|
|
115
|
+
export { formatMessageWithFallback } from './I18n/I18n';
|
|
115
116
|
export {
|
|
116
117
|
withBlockSchemaEnhancer,
|
|
117
118
|
withVariationSchemaEnhancer,
|
|
@@ -31,6 +31,27 @@
|
|
|
31
31
|
line-height: initial;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Restore visible focus for keyboard navigation — overrides Semantic UI's `outline: none` on form inputs
|
|
35
|
+
.ui.form input:not([type]),
|
|
36
|
+
.ui.form input[type='date'],
|
|
37
|
+
.ui.form input[type='datetime-local'],
|
|
38
|
+
.ui.form input[type='email'],
|
|
39
|
+
.ui.form input[type='number'],
|
|
40
|
+
.ui.form input[type='password'],
|
|
41
|
+
.ui.form input[type='search'],
|
|
42
|
+
.ui.form input[type='tel'],
|
|
43
|
+
.ui.form input[type='time'],
|
|
44
|
+
.ui.form input[type='text'],
|
|
45
|
+
.ui.form input[type='file'],
|
|
46
|
+
.ui.form input[type='url'],
|
|
47
|
+
.ui.form textarea {
|
|
48
|
+
padding-left: 5px;
|
|
49
|
+
|
|
50
|
+
&:focus-visible {
|
|
51
|
+
outline: revert;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
.ui.form .ui.input input:not([type]),
|
|
35
56
|
.ui.form .ui.input input[type='date'],
|
|
36
57
|
.ui.form .ui.input input[type='datetime-local'],
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
#main,
|
|
30
|
-
.slate-inline-toolbar.slate-toolbar
|
|
30
|
+
.slate-inline-toolbar.slate-toolbar,
|
|
31
|
+
.contenttype-plone-site .ui.page.modals {
|
|
31
32
|
.ui.basic.buttons .button,
|
|
32
33
|
.ui.basic.button {
|
|
33
34
|
-webkit-box-shadow: 0px 0px 0px @basicBorderSize transparent inset !important;
|
|
@@ -42,11 +43,16 @@
|
|
|
42
43
|
cursor: pointer;
|
|
43
44
|
text-align: initial;
|
|
44
45
|
|
|
45
|
-
&:focus {
|
|
46
|
+
&:focus:not(:focus-visible) {
|
|
46
47
|
outline: none;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
.ui.basic.buttons .button:focus-visible,
|
|
52
|
+
.ui.basic.button:focus-visible {
|
|
53
|
+
outline: revert;
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
.ui.basic.primary.button,
|
|
51
57
|
.ui.basic.secondary.button {
|
|
52
58
|
box-shadow: none !important;
|
|
@@ -69,7 +75,7 @@
|
|
|
69
75
|
cursor: pointer;
|
|
70
76
|
text-align: initial;
|
|
71
77
|
|
|
72
|
-
&:focus {
|
|
78
|
+
&:focus:not(:focus-visible) {
|
|
73
79
|
outline: none;
|
|
74
80
|
}
|
|
75
81
|
}
|
|
@@ -85,3 +91,24 @@
|
|
|
85
91
|
margin: 0;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
94
|
+
|
|
95
|
+
.modals {
|
|
96
|
+
.modal {
|
|
97
|
+
.actions {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: flex-end;
|
|
100
|
+
gap: 0.5em;
|
|
101
|
+
|
|
102
|
+
.ui.basic.button {
|
|
103
|
+
display: flex;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
padding: 0.3em;
|
|
106
|
+
margin: 0;
|
|
107
|
+
|
|
108
|
+
svg {
|
|
109
|
+
margin: 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|