@plone/volto 18.33.0 → 18.34.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 +40 -0
- package/README.md +0 -1
- package/locales/af/LC_MESSAGES/volto.po +27 -2
- package/locales/af.json +1 -1
- package/locales/ar/LC_MESSAGES/volto.po +27 -2
- package/locales/ar.json +1 -1
- package/locales/bg/LC_MESSAGES/volto.po +27 -2
- package/locales/bg.json +1 -1
- package/locales/bn/LC_MESSAGES/volto.po +27 -2
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +26 -1
- package/locales/ca.json +1 -1
- package/locales/cs/LC_MESSAGES/volto.po +27 -2
- package/locales/cs.json +1 -1
- package/locales/cy/LC_MESSAGES/volto.po +27 -2
- package/locales/cy.json +1 -1
- package/locales/da/LC_MESSAGES/volto.po +27 -2
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +26 -1
- package/locales/de.json +1 -1
- package/locales/el/LC_MESSAGES/volto.po +27 -2
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +27 -2
- package/locales/en.json +1 -1
- package/locales/en_AU/LC_MESSAGES/volto.po +27 -2
- package/locales/en_AU.json +1 -1
- package/locales/en_GB/LC_MESSAGES/volto.po +27 -2
- package/locales/en_GB.json +1 -1
- package/locales/eo/LC_MESSAGES/volto.po +27 -2
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +43 -18
- package/locales/es.json +1 -1
- package/locales/et/LC_MESSAGES/volto.po +27 -2
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +26 -1
- package/locales/eu.json +1 -1
- package/locales/fa/LC_MESSAGES/volto.po +27 -2
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +26 -1
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +26 -1
- package/locales/fr.json +1 -1
- package/locales/fu/LC_MESSAGES/volto.po +27 -2
- package/locales/fu.json +1 -1
- package/locales/gl/LC_MESSAGES/volto.po +31 -6
- package/locales/gl.json +1 -1
- package/locales/he/LC_MESSAGES/volto.po +27 -2
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +26 -1
- package/locales/hi.json +1 -1
- package/locales/hr/LC_MESSAGES/volto.po +27 -2
- package/locales/hr.json +1 -1
- package/locales/hu/LC_MESSAGES/volto.po +27 -2
- package/locales/hu.json +1 -1
- package/locales/hy/LC_MESSAGES/volto.po +27 -2
- package/locales/hy.json +1 -1
- package/locales/id/LC_MESSAGES/volto.po +27 -2
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +26 -1
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +26 -1
- package/locales/ja.json +1 -1
- package/locales/ka/LC_MESSAGES/volto.po +27 -2
- package/locales/ka.json +1 -1
- package/locales/kn/LC_MESSAGES/volto.po +27 -2
- package/locales/kn.json +1 -1
- package/locales/ko/LC_MESSAGES/volto.po +27 -2
- package/locales/ko.json +1 -1
- package/locales/lt/LC_MESSAGES/volto.po +27 -2
- package/locales/lt.json +1 -1
- package/locales/lv/LC_MESSAGES/volto.po +27 -2
- package/locales/lv.json +1 -1
- package/locales/mi/LC_MESSAGES/volto.po +27 -2
- package/locales/mi.json +1 -1
- package/locales/mk/LC_MESSAGES/volto.po +27 -2
- package/locales/mk.json +1 -1
- package/locales/my/LC_MESSAGES/volto.po +27 -2
- package/locales/my.json +1 -1
- package/locales/nb_NO/LC_MESSAGES/volto.po +27 -2
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +26 -1
- package/locales/nl.json +1 -1
- package/locales/nn/LC_MESSAGES/volto.po +27 -2
- package/locales/nn.json +1 -1
- package/locales/pl/LC_MESSAGES/volto.po +27 -2
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +26 -1
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +26 -1
- package/locales/pt_BR.json +1 -1
- package/locales/rm/LC_MESSAGES/volto.po +27 -2
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +26 -1
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +26 -1
- package/locales/ru.json +1 -1
- package/locales/sk/LC_MESSAGES/volto.po +27 -2
- package/locales/sk.json +1 -1
- package/locales/sl/LC_MESSAGES/volto.po +27 -2
- package/locales/sl.json +1 -1
- package/locales/sm/LC_MESSAGES/volto.po +27 -2
- package/locales/sm.json +1 -1
- package/locales/sq/LC_MESSAGES/volto.po +27 -2
- package/locales/sq.json +1 -1
- package/locales/sr/LC_MESSAGES/volto.po +27 -2
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl/LC_MESSAGES/volto.po +27 -2
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn/LC_MESSAGES/volto.po +27 -2
- package/locales/sr@latn.json +1 -1
- package/locales/sv/LC_MESSAGES/volto.po +26 -1
- package/locales/sv.json +1 -1
- package/locales/ta/LC_MESSAGES/volto.po +26 -1
- package/locales/ta.json +1 -1
- package/locales/te/LC_MESSAGES/volto.po +27 -2
- package/locales/te.json +1 -1
- package/locales/th/LC_MESSAGES/volto.po +27 -2
- package/locales/th.json +1 -1
- package/locales/to/LC_MESSAGES/volto.po +27 -2
- package/locales/to.json +1 -1
- package/locales/tr/LC_MESSAGES/volto.po +27 -2
- package/locales/tr.json +1 -1
- package/locales/uk/LC_MESSAGES/volto.po +27 -2
- package/locales/uk.json +1 -1
- package/locales/vi/LC_MESSAGES/volto.po +27 -2
- package/locales/vi.json +1 -1
- package/locales/volto.pot +27 -2
- package/locales/zh_CN/LC_MESSAGES/volto.po +26 -1
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant/LC_MESSAGES/volto.po +27 -2
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +27 -2
- package/locales/zh_Hant_HK.json +1 -1
- package/package.json +9 -9
- package/razzle.config.js +1 -0
- package/src/actions/users/users.js +2 -2
- package/src/components/manage/Blocks/Block/Order/Item.jsx +18 -10
- package/src/components/manage/Blocks/Block/Order/Item.test.jsx +90 -0
- package/src/components/manage/Controlpanels/AddonsControlpanel.jsx +7 -0
- package/src/components/manage/Controlpanels/DatabaseInformation.jsx +9 -0
- package/src/components/manage/Controlpanels/ModerateComments.jsx +8 -0
- package/src/components/manage/Controlpanels/Users/UserGroupMembershipControlPanel.test.jsx +3 -0
- package/src/components/manage/Sharing/Sharing.jsx +11 -3
- package/src/components/manage/Widgets/DatetimeWidget.jsx +92 -58
- package/src/components/manage/Widgets/DatetimeWidget.test.jsx +55 -0
- package/src/components/manage/Widgets/FormFieldWrapper.jsx +7 -5
- package/src/components/manage/Widgets/RegistryImageWidget.jsx +1 -1
- package/src/components/manage/Widgets/TextWidget.jsx +4 -0
- package/src/components/manage/Widgets/UrlWidget.jsx +51 -6
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +1 -0
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +30 -0
- package/src/components/theme/Unauthorized/Unauthorized.jsx +30 -22
- package/src/components/theme/Unauthorized/Unauthorized.test.jsx +28 -1
- package/theme/themes/default/globals/site.variables +2 -2
- package/theme/themes/pastanaga/collections/form.overrides +1 -1
- package/theme/themes/pastanaga/elements/segment.variables +1 -1
- package/theme/themes/pastanaga/extras/main.less +15 -0
- package/theme/themes/pastanaga/globals/site.variables +0 -2
- package/types/components/manage/Blocks/Block/Order/Item.test.d.ts +1 -0
- package/webpack-plugins/webpack-less-plugin.js +1 -1
package/razzle.config.js
CHANGED
|
@@ -310,6 +310,7 @@ const defaultModify = ({
|
|
|
310
310
|
'load-volto-addons': addonsLoaderPath,
|
|
311
311
|
...registry.getResolveAliases(),
|
|
312
312
|
'@plone/volto': `${registry.voltoPath}/src`,
|
|
313
|
+
'@plone/volto-slate': `${registry.voltoPath}/../volto-slate/src`,
|
|
313
314
|
// to be able to reference path uncustomized by webpack
|
|
314
315
|
'@plone/volto-original': `${registry.voltoPath}/src`,
|
|
315
316
|
// be able to reference current package from customized package
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module actions/users/users
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import qs from 'query-string';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
CREATE_USER,
|
|
@@ -92,7 +92,7 @@ export function listUsers(options = {}) {
|
|
|
92
92
|
|
|
93
93
|
let filterarg =
|
|
94
94
|
groups_filter.length > 0
|
|
95
|
-
? stringify(
|
|
95
|
+
? qs.stringify(
|
|
96
96
|
{ 'groups-filter': groups_filter },
|
|
97
97
|
{ arrayFormat: 'colon-list-separator' },
|
|
98
98
|
)
|
|
@@ -44,6 +44,12 @@ export const Item = forwardRef(
|
|
|
44
44
|
config.blocks.blocksConfig[data?.['@type']]?.icon ||
|
|
45
45
|
config.blocks.blocksConfig.title?.icon;
|
|
46
46
|
|
|
47
|
+
const required =
|
|
48
|
+
typeof data?.required === 'boolean'
|
|
49
|
+
? data.required
|
|
50
|
+
: includes(config.blocks.requiredBlocks, data?.['@type']);
|
|
51
|
+
const fixed = !!data?.fixed;
|
|
52
|
+
|
|
47
53
|
return (
|
|
48
54
|
<li
|
|
49
55
|
className={classNames(
|
|
@@ -93,15 +99,17 @@ export const Item = forwardRef(
|
|
|
93
99
|
ref={ref}
|
|
94
100
|
style={style}
|
|
95
101
|
>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
{!fixed && (
|
|
103
|
+
<button
|
|
104
|
+
ref={ref}
|
|
105
|
+
{...handleProps}
|
|
106
|
+
className={classNames('action', 'drag')}
|
|
107
|
+
tabIndex={0}
|
|
108
|
+
data-cypress="draggable-handle"
|
|
109
|
+
>
|
|
110
|
+
<Icon name={dragSVG} size="16px" />
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
105
113
|
<span
|
|
106
114
|
className={cx('text', {
|
|
107
115
|
errored: errors && Object.keys(errors).length > 0,
|
|
@@ -118,7 +126,7 @@ export const Item = forwardRef(
|
|
|
118
126
|
config.blocks.blocksConfig[data?.['@type']]?.title ||
|
|
119
127
|
data?.title}
|
|
120
128
|
</span>
|
|
121
|
-
{!clone && onRemove && (
|
|
129
|
+
{!clone && onRemove && !required && (
|
|
122
130
|
<button
|
|
123
131
|
onClick={onRemove}
|
|
124
132
|
className={classNames('action', 'delete')}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import configureStore from 'redux-mock-store';
|
|
3
|
+
import { Provider } from 'react-intl-redux';
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
import config from '@plone/volto/registry';
|
|
6
|
+
|
|
7
|
+
import { Item } from './Item';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
const defaultStoreState = {
|
|
12
|
+
intl: {
|
|
13
|
+
locale: 'en',
|
|
14
|
+
messages: {},
|
|
15
|
+
},
|
|
16
|
+
form: {
|
|
17
|
+
ui: {
|
|
18
|
+
selected: null,
|
|
19
|
+
hovered: null,
|
|
20
|
+
multiSelected: [],
|
|
21
|
+
gridSelected: null,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const renderItem = (data = {}) => {
|
|
27
|
+
const store = mockStore(defaultStoreState);
|
|
28
|
+
|
|
29
|
+
return render(
|
|
30
|
+
<Provider store={store}>
|
|
31
|
+
<Item
|
|
32
|
+
id="title-block-id"
|
|
33
|
+
data={{ '@type': 'title', ...data }}
|
|
34
|
+
depth={0}
|
|
35
|
+
indentationWidth={25}
|
|
36
|
+
onRemove={() => {}}
|
|
37
|
+
onSelectBlock={() => {}}
|
|
38
|
+
handleProps={{}}
|
|
39
|
+
/>
|
|
40
|
+
</Provider>,
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('Order Item', () => {
|
|
45
|
+
let requiredBlocks;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
requiredBlocks = [...(config.blocks.requiredBlocks || [])];
|
|
49
|
+
config.blocks.requiredBlocks = [];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
config.blocks.requiredBlocks = requiredBlocks;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('renders drag and delete actions for movable and removable blocks', () => {
|
|
57
|
+
const { container } = renderItem();
|
|
58
|
+
|
|
59
|
+
expect(container.querySelector('.action.drag')).not.toBeNull();
|
|
60
|
+
expect(container.querySelector('.action.delete')).not.toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('hides delete action for required blocks', () => {
|
|
64
|
+
const { container } = renderItem({ required: true });
|
|
65
|
+
|
|
66
|
+
expect(container.querySelector('.action.delete')).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('hides delete action for block types configured as required', () => {
|
|
70
|
+
config.blocks.requiredBlocks = ['title'];
|
|
71
|
+
|
|
72
|
+
const { container } = renderItem();
|
|
73
|
+
|
|
74
|
+
expect(container.querySelector('.action.delete')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('allows explicit required=false to override required block types', () => {
|
|
78
|
+
config.blocks.requiredBlocks = ['title'];
|
|
79
|
+
|
|
80
|
+
const { container } = renderItem({ required: false });
|
|
81
|
+
|
|
82
|
+
expect(container.querySelector('.action.delete')).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('hides drag action for fixed blocks', () => {
|
|
86
|
+
const { container } = renderItem({ fixed: true });
|
|
87
|
+
|
|
88
|
+
expect(container.querySelector('.action.drag')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -27,6 +27,7 @@ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
|
27
27
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
28
28
|
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
|
|
29
29
|
import Toast from '@plone/volto/components/manage/Toast/Toast';
|
|
30
|
+
import Error from '@plone/volto/components/theme/Error/Error';
|
|
30
31
|
import circleBottomSVG from '@plone/volto/icons/circle-bottom.svg';
|
|
31
32
|
import circleTopSVG from '@plone/volto/icons/circle-top.svg';
|
|
32
33
|
import backSVG from '@plone/volto/icons/back.svg';
|
|
@@ -148,6 +149,7 @@ const AddonsControlpanel = (props) => {
|
|
|
148
149
|
shallowEqual,
|
|
149
150
|
);
|
|
150
151
|
const loadingAddons = useSelector((state) => state.addons.loading);
|
|
152
|
+
const addonsError = useSelector((state) => state.addons.error);
|
|
151
153
|
|
|
152
154
|
useEffect(() => {
|
|
153
155
|
dispatch(listAddons());
|
|
@@ -245,6 +247,11 @@ const AddonsControlpanel = (props) => {
|
|
|
245
247
|
setactiveIndex(newIndex);
|
|
246
248
|
};
|
|
247
249
|
|
|
250
|
+
// Error handling for unauthorized access
|
|
251
|
+
if (addonsError) {
|
|
252
|
+
return <Error error={addonsError} />;
|
|
253
|
+
}
|
|
254
|
+
|
|
248
255
|
return (
|
|
249
256
|
<Container id="page-addons" className="controlpanel-addons">
|
|
250
257
|
<Helmet title={intl.formatMessage(messages.addOns)} />
|
|
@@ -10,6 +10,7 @@ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
|
10
10
|
import { useClient } from '@plone/volto/hooks/client/useClient';
|
|
11
11
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
12
12
|
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
|
|
13
|
+
import Error from '@plone/volto/components/theme/Error/Error';
|
|
13
14
|
import backSVG from '@plone/volto/icons/back.svg';
|
|
14
15
|
|
|
15
16
|
const messages = defineMessages({
|
|
@@ -31,11 +32,19 @@ const DatabaseInformation = () => {
|
|
|
31
32
|
const databaseInformation = useSelector(
|
|
32
33
|
(state) => state.controlpanels.databaseinformation,
|
|
33
34
|
);
|
|
35
|
+
const databaseError = useSelector(
|
|
36
|
+
(state) => state.controlpanels.database?.error,
|
|
37
|
+
);
|
|
34
38
|
|
|
35
39
|
useEffect(() => {
|
|
36
40
|
dispatch(getDatabaseInformation());
|
|
37
41
|
}, [dispatch]);
|
|
38
42
|
|
|
43
|
+
// Error handling for unauthorized access
|
|
44
|
+
if (databaseError) {
|
|
45
|
+
return <Error error={databaseError} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
return databaseInformation ? (
|
|
40
49
|
<Container id="database-page" className="controlpanel-database">
|
|
41
50
|
<Helmet title={intl.formatMessage(messages.databaseInformation)} />
|
|
@@ -19,6 +19,7 @@ import { searchContent } from '@plone/volto/actions/search/search';
|
|
|
19
19
|
import FormattedRelativeDate from '@plone/volto/components/theme/FormattedDate/FormattedRelativeDate';
|
|
20
20
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
21
21
|
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
|
|
22
|
+
import Error from '@plone/volto/components/theme/Error/Error';
|
|
22
23
|
import { CommentEditModal } from '@plone/volto/components/theme/Comments';
|
|
23
24
|
|
|
24
25
|
import backSVG from '@plone/volto/icons/back.svg';
|
|
@@ -65,6 +66,7 @@ class ModerateComments extends Component {
|
|
|
65
66
|
loaded: PropTypes.bool,
|
|
66
67
|
}).isRequired,
|
|
67
68
|
pathname: PropTypes.string.isRequired,
|
|
69
|
+
searchError: PropTypes.object,
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -186,6 +188,11 @@ class ModerateComments extends Component {
|
|
|
186
188
|
* @returns {string} Markup for the component.
|
|
187
189
|
*/
|
|
188
190
|
render() {
|
|
191
|
+
// Error handling for unauthorized access
|
|
192
|
+
if (this.props.searchError) {
|
|
193
|
+
return <Error error={this.props.searchError} />;
|
|
194
|
+
}
|
|
195
|
+
|
|
189
196
|
return (
|
|
190
197
|
<div id="page-moderate-comments">
|
|
191
198
|
<CommentEditModal
|
|
@@ -297,6 +304,7 @@ export default compose(
|
|
|
297
304
|
items: state.search.items,
|
|
298
305
|
deleteRequest: state.comments.delete,
|
|
299
306
|
pathname: props.location.pathname,
|
|
307
|
+
searchError: state.search.error,
|
|
300
308
|
}),
|
|
301
309
|
{ deleteComment, searchContent },
|
|
302
310
|
),
|
|
@@ -14,6 +14,9 @@ vi.mock('../../Toolbar/Toolbar', () => ({
|
|
|
14
14
|
describe('UserGroupMembershipControlPanel', () => {
|
|
15
15
|
it('renders a user group membership control component', () => {
|
|
16
16
|
const store = mockStore({
|
|
17
|
+
userSession: {
|
|
18
|
+
token: '1234',
|
|
19
|
+
},
|
|
17
20
|
controlpanels: {
|
|
18
21
|
controlpanel: {
|
|
19
22
|
data: {
|
|
@@ -473,13 +473,21 @@ class SharingComponent extends Component {
|
|
|
473
473
|
<p className="help">
|
|
474
474
|
<FormattedMessage
|
|
475
475
|
id="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, the symbol {inherited} indicates an inherited value. Similarly, the symbol {global} indicates a global role, which is managed by the site administrator."
|
|
476
|
-
defaultMessage="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview,
|
|
476
|
+
defaultMessage="By default, permissions from the container of this item are inherited. If you disable this, only the explicitly defined sharing permissions will be valid. In the overview, inherited values are explicitly labeled as 'Inherited value' and receive a green check mark {inherited}. Similarly, roles managed by the site administrator are labeled as 'Global role' and receive a blue check mark {global}."
|
|
477
477
|
values={{
|
|
478
478
|
inherited: (
|
|
479
|
-
<IconOld
|
|
479
|
+
<IconOld
|
|
480
|
+
aria-hidden="true"
|
|
481
|
+
name="check circle outline"
|
|
482
|
+
color="green"
|
|
483
|
+
/>
|
|
480
484
|
),
|
|
481
485
|
global: (
|
|
482
|
-
<IconOld
|
|
486
|
+
<IconOld
|
|
487
|
+
aria-hidden="true"
|
|
488
|
+
name="check circle outline"
|
|
489
|
+
color="blue"
|
|
490
|
+
/>
|
|
483
491
|
),
|
|
484
492
|
}}
|
|
485
493
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import loadable from '@loadable/component';
|
|
@@ -85,16 +85,22 @@ const DatetimeWidgetComponent = (props) => {
|
|
|
85
85
|
noPastDates: propNoPastDates,
|
|
86
86
|
isDisabled,
|
|
87
87
|
formData,
|
|
88
|
+
required,
|
|
88
89
|
} = props;
|
|
89
90
|
|
|
90
91
|
const intl = useIntl();
|
|
91
92
|
const lang = intl.locale;
|
|
92
93
|
|
|
94
|
+
// timeInputRef: for aria-required (rc-time-picker has no aria props)
|
|
95
|
+
const timeInputRef = useRef(null);
|
|
96
|
+
|
|
93
97
|
const [focused, setFocused] = useState(false);
|
|
94
98
|
const [isDefault, setIsDefault] = useState(false);
|
|
95
99
|
|
|
96
100
|
const { SingleDatePicker } = reactDates;
|
|
97
101
|
|
|
102
|
+
const renderWidget = !(id === 'end' && formData?.open_end);
|
|
103
|
+
|
|
98
104
|
useEffect(() => {
|
|
99
105
|
const parsedDateTime = parseDateTime(
|
|
100
106
|
toBackendLang(lang),
|
|
@@ -107,11 +113,6 @@ const DatetimeWidgetComponent = (props) => {
|
|
|
107
113
|
);
|
|
108
114
|
}, [value, lang, moment]);
|
|
109
115
|
|
|
110
|
-
// If open_end is checked and this is the end field, don't render
|
|
111
|
-
if (id === 'end' && formData?.open_end) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
116
|
const getInternalValue = () => {
|
|
116
117
|
return parseDateTime(toBackendLang(lang), value, undefined, moment.default);
|
|
117
118
|
};
|
|
@@ -165,68 +166,101 @@ const DatetimeWidgetComponent = (props) => {
|
|
|
165
166
|
const datetime = getInternalValue();
|
|
166
167
|
const isDateOnly = getDateOnly();
|
|
167
168
|
|
|
169
|
+
// aria-required for the time input (rc-time-picker is lazy-loaded,
|
|
170
|
+
// so MutationObserver is needed to catch when it mounts its input)
|
|
171
|
+
|
|
172
|
+
// rc-time-picker does not have aria props, so we need to set aria-required
|
|
173
|
+
// manually on the input element when the required prop changes
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!renderWidget || isDateOnly) return;
|
|
177
|
+
|
|
178
|
+
function applyTimeAria() {
|
|
179
|
+
const input = timeInputRef.current?.querySelector('input');
|
|
180
|
+
if (!input) return;
|
|
181
|
+
if (required) input.setAttribute('aria-required', 'true');
|
|
182
|
+
else input.removeAttribute('aria-required');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
applyTimeAria();
|
|
186
|
+
|
|
187
|
+
const observer = new MutationObserver(applyTimeAria);
|
|
188
|
+
if (timeInputRef.current) {
|
|
189
|
+
observer.observe(timeInputRef.current, {
|
|
190
|
+
childList: true,
|
|
191
|
+
subtree: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return () => observer.disconnect();
|
|
196
|
+
}, [required, isDateOnly, renderWidget]);
|
|
197
|
+
|
|
168
198
|
return (
|
|
169
199
|
<FormFieldWrapper {...props}>
|
|
170
|
-
|
|
171
|
-
<div
|
|
172
|
-
className={cx('ui input date-input', {
|
|
173
|
-
'default-date': isDefault,
|
|
174
|
-
})}
|
|
175
|
-
>
|
|
176
|
-
<SingleDatePicker
|
|
177
|
-
date={datetime}
|
|
178
|
-
disabled={isDisabled}
|
|
179
|
-
onDateChange={onDateChange}
|
|
180
|
-
focused={focused}
|
|
181
|
-
numberOfMonths={1}
|
|
182
|
-
{...(noPastDates ? {} : { isOutsideRange: () => false })}
|
|
183
|
-
onFocusChange={onFocusChange}
|
|
184
|
-
noBorder
|
|
185
|
-
displayFormat={moment.default
|
|
186
|
-
.localeData(toBackendLang(lang))
|
|
187
|
-
.longDateFormat('L')}
|
|
188
|
-
navPrev={<PrevIcon />}
|
|
189
|
-
navNext={<NextIcon />}
|
|
190
|
-
id={`${id}-date`}
|
|
191
|
-
placeholder={intl.formatMessage(messages.date)}
|
|
192
|
-
/>
|
|
193
|
-
</div>
|
|
194
|
-
{!isDateOnly && (
|
|
200
|
+
{renderWidget && (
|
|
201
|
+
<div className="date-time-widget-wrapper">
|
|
195
202
|
<div
|
|
196
|
-
className={cx('ui input
|
|
203
|
+
className={cx('ui input date-input', {
|
|
197
204
|
'default-date': isDefault,
|
|
198
205
|
})}
|
|
199
206
|
>
|
|
200
|
-
<
|
|
207
|
+
<SingleDatePicker
|
|
208
|
+
date={datetime}
|
|
201
209
|
disabled={isDisabled}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
onDateChange={onDateChange}
|
|
211
|
+
focused={focused}
|
|
212
|
+
numberOfMonths={1}
|
|
213
|
+
{...(noPastDates ? {} : { isOutsideRange: () => false })}
|
|
214
|
+
onFocusChange={onFocusChange}
|
|
215
|
+
noBorder
|
|
216
|
+
required={required}
|
|
217
|
+
displayFormat={moment.default
|
|
210
218
|
.localeData(toBackendLang(lang))
|
|
211
|
-
.longDateFormat('
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
.longDateFormat('L')}
|
|
220
|
+
navPrev={<PrevIcon />}
|
|
221
|
+
navNext={<NextIcon />}
|
|
222
|
+
id={`${id}-date`}
|
|
223
|
+
placeholder={intl.formatMessage(messages.date)}
|
|
215
224
|
/>
|
|
216
225
|
</div>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
{!isDateOnly && (
|
|
227
|
+
<div
|
|
228
|
+
ref={timeInputRef}
|
|
229
|
+
className={cx('ui input time-input', {
|
|
230
|
+
'default-date': isDefault,
|
|
231
|
+
})}
|
|
232
|
+
>
|
|
233
|
+
<TimePicker
|
|
234
|
+
disabled={isDisabled}
|
|
235
|
+
defaultValue={datetime}
|
|
236
|
+
value={datetime}
|
|
237
|
+
onChange={onTimeChange}
|
|
238
|
+
allowEmpty={false}
|
|
239
|
+
showSecond={false}
|
|
240
|
+
use12Hours={lang === 'en'}
|
|
241
|
+
id={`${id}-time`}
|
|
242
|
+
format={moment.default
|
|
243
|
+
.localeData(toBackendLang(lang))
|
|
244
|
+
.longDateFormat('LT')}
|
|
245
|
+
placeholder={intl.formatMessage(messages.time)}
|
|
246
|
+
focusOnOpen
|
|
247
|
+
placement="bottomRight"
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
{resettable && (
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
disabled={isDisabled || !datetime}
|
|
255
|
+
onClick={onResetDates}
|
|
256
|
+
className="item ui noborder button"
|
|
257
|
+
aria-label={intl.formatMessage(messages.clearDateTime)}
|
|
258
|
+
>
|
|
259
|
+
<Icon name={clearSVG} size="24px" className="close" />
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
230
264
|
</FormFieldWrapper>
|
|
231
265
|
);
|
|
232
266
|
};
|
|
@@ -71,3 +71,58 @@ test('datetime widget converts UTC date and adapts to local datetime', async ()
|
|
|
71
71
|
await waitFor(() => screen.getByPlaceholderText('Time'));
|
|
72
72
|
expect(container).toMatchSnapshot();
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test('applies aria-required attribute to the date input when required prop is true', async () => {
|
|
76
|
+
const store = mockStore({
|
|
77
|
+
intl: {
|
|
78
|
+
locale: 'en',
|
|
79
|
+
messages: {},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { container } = render(
|
|
84
|
+
<Provider store={store}>
|
|
85
|
+
<DatetimeWidget
|
|
86
|
+
id="required-field"
|
|
87
|
+
title="Required Field"
|
|
88
|
+
onChange={() => {}}
|
|
89
|
+
required={true}
|
|
90
|
+
/>
|
|
91
|
+
</Provider>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await waitFor(() => screen.getByPlaceholderText('Date'));
|
|
95
|
+
|
|
96
|
+
const dateInput = container.querySelector('.date-input input');
|
|
97
|
+
|
|
98
|
+
expect(dateInput).toHaveAttribute('required');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('applies aria-required attribute to the time input when required prop is true', async () => {
|
|
102
|
+
const store = mockStore({
|
|
103
|
+
intl: {
|
|
104
|
+
locale: 'en',
|
|
105
|
+
messages: {},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<Provider store={store}>
|
|
111
|
+
<DatetimeWidget
|
|
112
|
+
id="required-field"
|
|
113
|
+
title="Required Field"
|
|
114
|
+
onChange={() => {}}
|
|
115
|
+
required={true}
|
|
116
|
+
/>
|
|
117
|
+
</Provider>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Wait for the lazy-loaded TimePicker to be mounted in the DOM
|
|
121
|
+
await waitFor(() => screen.getByPlaceholderText('Time'));
|
|
122
|
+
|
|
123
|
+
// The rc-time-picker doesn't support aria-required natively,
|
|
124
|
+
// so we verify if our MutationObserver/useEffect successfully injected it.
|
|
125
|
+
const timeInput = container.querySelector('.time-input input');
|
|
126
|
+
|
|
127
|
+
expect(timeInput).toHaveAttribute('aria-required', 'true');
|
|
128
|
+
});
|
|
@@ -105,11 +105,13 @@ class FormFieldWrapper extends Component {
|
|
|
105
105
|
<>
|
|
106
106
|
{this.props.children}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{message}
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
<div aria-live="polite" aria-atomic="true">
|
|
109
|
+
{map(error, (message) => (
|
|
110
|
+
<Label key={message} basic color="red" className="form-error-label">
|
|
111
|
+
{message}
|
|
112
|
+
</Label>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
113
115
|
</>
|
|
114
116
|
);
|
|
115
117
|
|
|
@@ -99,7 +99,7 @@ const RegistryImageWidget = (props) => {
|
|
|
99
99
|
|
|
100
100
|
readAsDataURL(file).then((data) => {
|
|
101
101
|
const fields = data.match(/^data:(.*);(.*),(.*)$/);
|
|
102
|
-
onChange(id, `filenameb64:${btoa(file.name)};datab64:${fields[3]}
|
|
102
|
+
onChange(id, `filenameb64:${btoa(file.name)};datab64:${fields[3]}`);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
let reader = new FileReader();
|
|
@@ -19,6 +19,8 @@ const TextWidget = (props) => {
|
|
|
19
19
|
placeholder,
|
|
20
20
|
isDisabled,
|
|
21
21
|
focus,
|
|
22
|
+
required,
|
|
23
|
+
error,
|
|
22
24
|
} = props;
|
|
23
25
|
|
|
24
26
|
const ref = useRef();
|
|
@@ -49,6 +51,8 @@ const TextWidget = (props) => {
|
|
|
49
51
|
onClick={() => onClick()}
|
|
50
52
|
minLength={minLength || null}
|
|
51
53
|
maxLength={maxLength || null}
|
|
54
|
+
aria-required={required ? 'true' : undefined}
|
|
55
|
+
aria-invalid={error?.length > 0 ? 'true' : undefined}
|
|
52
56
|
/>
|
|
53
57
|
{icon && iconAction && (
|
|
54
58
|
<button className={`field-${id}-action-button`} onClick={iconAction}>
|