@plone/volto 18.35.0 → 18.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/locales/af/LC_MESSAGES/volto.po +5 -5
  3. package/locales/af.json +1 -1
  4. package/locales/ar/LC_MESSAGES/volto.po +5 -5
  5. package/locales/ar.json +1 -1
  6. package/locales/bg/LC_MESSAGES/volto.po +5 -5
  7. package/locales/bg.json +1 -1
  8. package/locales/bn/LC_MESSAGES/volto.po +5 -5
  9. package/locales/bn.json +1 -1
  10. package/locales/ca/LC_MESSAGES/volto.po +5 -5
  11. package/locales/ca.json +1 -1
  12. package/locales/cs/LC_MESSAGES/volto.po +5 -5
  13. package/locales/cs.json +1 -1
  14. package/locales/cy/LC_MESSAGES/volto.po +5 -5
  15. package/locales/cy.json +1 -1
  16. package/locales/da/LC_MESSAGES/volto.po +5 -5
  17. package/locales/da.json +1 -1
  18. package/locales/de/LC_MESSAGES/volto.po +5 -5
  19. package/locales/de.json +1 -1
  20. package/locales/el/LC_MESSAGES/volto.po +5 -5
  21. package/locales/el.json +1 -1
  22. package/locales/en/LC_MESSAGES/volto.po +5 -5
  23. package/locales/en.json +1 -1
  24. package/locales/en_AU/LC_MESSAGES/volto.po +5 -5
  25. package/locales/en_AU.json +1 -1
  26. package/locales/en_GB/LC_MESSAGES/volto.po +5 -5
  27. package/locales/en_GB.json +1 -1
  28. package/locales/eo/LC_MESSAGES/volto.po +5 -5
  29. package/locales/eo.json +1 -1
  30. package/locales/es/LC_MESSAGES/volto.po +5 -5
  31. package/locales/es.json +1 -1
  32. package/locales/et/LC_MESSAGES/volto.po +5 -5
  33. package/locales/et.json +1 -1
  34. package/locales/eu/LC_MESSAGES/volto.po +5 -5
  35. package/locales/eu.json +1 -1
  36. package/locales/fa/LC_MESSAGES/volto.po +5 -5
  37. package/locales/fa.json +1 -1
  38. package/locales/fi/LC_MESSAGES/volto.po +5 -5
  39. package/locales/fi.json +1 -1
  40. package/locales/fr/LC_MESSAGES/volto.po +5 -5
  41. package/locales/fr.json +1 -1
  42. package/locales/fu/LC_MESSAGES/volto.po +5 -5
  43. package/locales/fu.json +1 -1
  44. package/locales/gl/LC_MESSAGES/volto.po +5 -5
  45. package/locales/gl.json +1 -1
  46. package/locales/he/LC_MESSAGES/volto.po +5 -5
  47. package/locales/he.json +1 -1
  48. package/locales/hi/LC_MESSAGES/volto.po +5 -5
  49. package/locales/hi.json +1 -1
  50. package/locales/hr/LC_MESSAGES/volto.po +5 -5
  51. package/locales/hr.json +1 -1
  52. package/locales/hu/LC_MESSAGES/volto.po +5 -5
  53. package/locales/hu.json +1 -1
  54. package/locales/hy/LC_MESSAGES/volto.po +5 -5
  55. package/locales/hy.json +1 -1
  56. package/locales/id/LC_MESSAGES/volto.po +5 -5
  57. package/locales/id.json +1 -1
  58. package/locales/it/LC_MESSAGES/volto.po +5 -5
  59. package/locales/it.json +1 -1
  60. package/locales/ja/LC_MESSAGES/volto.po +5 -5
  61. package/locales/ja.json +1 -1
  62. package/locales/ka/LC_MESSAGES/volto.po +5 -5
  63. package/locales/ka.json +1 -1
  64. package/locales/kn/LC_MESSAGES/volto.po +5 -5
  65. package/locales/kn.json +1 -1
  66. package/locales/ko/LC_MESSAGES/volto.po +5 -5
  67. package/locales/ko.json +1 -1
  68. package/locales/lt/LC_MESSAGES/volto.po +5 -5
  69. package/locales/lt.json +1 -1
  70. package/locales/lv/LC_MESSAGES/volto.po +5 -5
  71. package/locales/lv.json +1 -1
  72. package/locales/mi/LC_MESSAGES/volto.po +5 -5
  73. package/locales/mi.json +1 -1
  74. package/locales/mk/LC_MESSAGES/volto.po +5 -5
  75. package/locales/mk.json +1 -1
  76. package/locales/my/LC_MESSAGES/volto.po +5 -5
  77. package/locales/my.json +1 -1
  78. package/locales/nb_NO/LC_MESSAGES/volto.po +5 -5
  79. package/locales/nb_NO.json +1 -1
  80. package/locales/nl/LC_MESSAGES/volto.po +5 -5
  81. package/locales/nl.json +1 -1
  82. package/locales/nn/LC_MESSAGES/volto.po +5 -5
  83. package/locales/nn.json +1 -1
  84. package/locales/pl/LC_MESSAGES/volto.po +5 -5
  85. package/locales/pl.json +1 -1
  86. package/locales/pt/LC_MESSAGES/volto.po +5 -5
  87. package/locales/pt.json +1 -1
  88. package/locales/pt_BR/LC_MESSAGES/volto.po +5 -5
  89. package/locales/pt_BR.json +1 -1
  90. package/locales/rm/LC_MESSAGES/volto.po +5 -5
  91. package/locales/rm.json +1 -1
  92. package/locales/ro/LC_MESSAGES/volto.po +5 -5
  93. package/locales/ro.json +1 -1
  94. package/locales/ru/LC_MESSAGES/volto.po +5 -5
  95. package/locales/ru.json +1 -1
  96. package/locales/sk/LC_MESSAGES/volto.po +5 -5
  97. package/locales/sk.json +1 -1
  98. package/locales/sl/LC_MESSAGES/volto.po +5 -5
  99. package/locales/sl.json +1 -1
  100. package/locales/sm/LC_MESSAGES/volto.po +5 -5
  101. package/locales/sm.json +1 -1
  102. package/locales/sq/LC_MESSAGES/volto.po +5 -5
  103. package/locales/sq.json +1 -1
  104. package/locales/sr/LC_MESSAGES/volto.po +5 -5
  105. package/locales/sr.json +1 -1
  106. package/locales/sr@cyrl/LC_MESSAGES/volto.po +5 -5
  107. package/locales/sr@cyrl.json +1 -1
  108. package/locales/sr@latn/LC_MESSAGES/volto.po +5 -5
  109. package/locales/sr@latn.json +1 -1
  110. package/locales/sv/LC_MESSAGES/volto.po +5 -5
  111. package/locales/sv.json +1 -1
  112. package/locales/ta/LC_MESSAGES/volto.po +5 -5
  113. package/locales/ta.json +1 -1
  114. package/locales/te/LC_MESSAGES/volto.po +5 -5
  115. package/locales/te.json +1 -1
  116. package/locales/th/LC_MESSAGES/volto.po +5 -5
  117. package/locales/th.json +1 -1
  118. package/locales/to/LC_MESSAGES/volto.po +5 -5
  119. package/locales/to.json +1 -1
  120. package/locales/tr/LC_MESSAGES/volto.po +5 -5
  121. package/locales/tr.json +1 -1
  122. package/locales/uk/LC_MESSAGES/volto.po +5 -5
  123. package/locales/uk.json +1 -1
  124. package/locales/vi/LC_MESSAGES/volto.po +5 -5
  125. package/locales/vi.json +1 -1
  126. package/locales/volto.pot +6 -6
  127. package/locales/zh_CN/LC_MESSAGES/volto.po +5 -5
  128. package/locales/zh_CN.json +1 -1
  129. package/locales/zh_Hant/LC_MESSAGES/volto.po +5 -5
  130. package/locales/zh_Hant.json +1 -1
  131. package/locales/zh_Hant_HK/LC_MESSAGES/volto.po +5 -5
  132. package/locales/zh_Hant_HK.json +1 -1
  133. package/package.json +18 -22
  134. package/src/actions/querystringsearch/querystringsearch.js +4 -1
  135. package/src/actions/querystringsearch/querystringsearch.test.js +77 -0
  136. package/src/components/manage/BlockChooser/BlockChooser.jsx +7 -10
  137. package/src/components/manage/Blocks/Block/Edit.jsx +9 -10
  138. package/src/components/manage/Blocks/Block/Order/Item.jsx +9 -4
  139. package/src/components/manage/Blocks/Image/View.test.jsx +8 -8
  140. package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +7 -6
  141. package/src/components/manage/Sidebar/ObjectBrowser.jsx +7 -0
  142. package/src/components/manage/Sidebar/ObjectBrowserBody.jsx +7 -3
  143. package/src/components/manage/Sidebar/ObjectBrowserBody.test.jsx +52 -0
  144. package/src/components/manage/Widgets/ArrayWidget.jsx +3 -2
  145. package/src/components/manage/Widgets/FormFieldWrapper.jsx +16 -3
  146. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +1 -0
  147. package/src/components/manage/Widgets/QuerystringWidget.jsx +1 -18
  148. package/src/components/manage/Widgets/QuerystringWidget.test.jsx +45 -2
  149. package/src/components/manage/Widgets/SelectStyling.jsx +33 -1
  150. package/src/components/manage/Widgets/SelectWidget.jsx +3 -2
  151. package/src/components/manage/Widgets/TextWidget.test.jsx +44 -0
  152. package/src/components/theme/Unauthorized/Unauthorized.jsx +1 -2
  153. package/src/components/theme/View/EventView.stories.jsx +89 -0
  154. package/src/components/theme/View/FileView.stories.jsx +50 -0
  155. package/src/components/theme/View/LinkView.stories.jsx +57 -0
  156. package/src/components/theme/View/ListingView.stories.jsx +70 -0
  157. package/src/components/theme/View/NewsItemView.stories.jsx +58 -0
  158. package/src/components/theme/View/RenderBlocks.stories.jsx +112 -0
  159. package/src/components/theme/View/SummaryView.stories.jsx +71 -0
  160. package/src/components/theme/View/TabularView.stories.jsx +66 -0
  161. package/src/helpers/I18n/I18n.test.ts +44 -0
  162. package/src/helpers/I18n/I18n.ts +31 -0
  163. package/src/helpers/index.js +1 -0
  164. package/src/server.jsx +7 -1
  165. package/types/components/manage/Controlpanels/Relations/RelationsMatrix.d.ts +1 -1
  166. package/types/components/manage/Multilingual/ManageTranslations.d.ts +1 -1
  167. package/types/components/manage/Sidebar/ObjectBrowser.d.ts +1 -1
  168. package/types/components/manage/Sidebar/ObjectBrowserBody.test.d.ts +1 -0
  169. package/types/components/manage/Widgets/ImageWidget.d.ts +1 -1
  170. package/types/components/manage/Widgets/InternalUrlWidget.d.ts +1 -1
  171. package/types/components/manage/Widgets/QuerystringWidget.d.ts +0 -4
  172. package/types/components/manage/Widgets/UrlWidget.d.ts +1 -1
  173. package/types/components/manage/Widgets/index.d.ts +2 -2
  174. package/types/components/theme/View/EventView.stories.d.ts +19 -0
  175. package/types/components/theme/View/FileView.stories.d.ts +18 -0
  176. package/types/components/theme/View/LinkView.stories.d.ts +18 -0
  177. package/types/components/theme/View/ListingView.stories.d.ts +24 -0
  178. package/types/components/theme/View/NewsItemView.stories.d.ts +23 -0
  179. package/types/components/theme/View/RenderBlocks.stories.d.ts +23 -0
  180. package/types/components/theme/View/SummaryView.stories.d.ts +23 -0
  181. package/types/components/theme/View/TabularView.stories.d.ts +23 -0
  182. package/types/helpers/I18n/I18n.d.ts +20 -0
  183. package/types/helpers/index.d.ts +1 -0
@@ -36,13 +36,13 @@ config.blocks.blocksConfig = {
36
36
 
37
37
  describe('Image View Component', () => {
38
38
  test('renders a view image component with a local image', () => {
39
- const { getByRole } = render(<View data={{ url: '/image.jpg' }} />);
40
- const img = getByRole('img');
39
+ const { container } = render(<View data={{ url: '/image.jpg' }} />);
40
+ const img = container.querySelector('img');
41
41
  expect(img).toHaveAttribute('src', '/image.jpg/@@images/image');
42
42
  expect(img).toHaveAttribute('loading', 'lazy');
43
43
  });
44
44
  test('renders a view image component with a local image with a link', () => {
45
- const { container, getByRole } = render(
45
+ const { container } = render(
46
46
  <Provider store={store}>
47
47
  <MemoryRouter>
48
48
  <View
@@ -51,22 +51,22 @@ describe('Image View Component', () => {
51
51
  </MemoryRouter>
52
52
  </Provider>,
53
53
  );
54
- const img = getByRole('img');
54
+ const img = container.querySelector('img');
55
55
  const a = container.querySelector('a');
56
56
  expect(img).toHaveAttribute('src', '/image.jpg/@@images/image');
57
57
  expect(a).toHaveAttribute('href', '/front-page');
58
58
  });
59
59
  test('renders a view image component with an external image', () => {
60
- const { getByRole } = render(
60
+ const { container } = render(
61
61
  <Provider store={store}>
62
62
  <View data={{ url: 'https://plone.org/logo.jpg' }} />
63
63
  </Provider>,
64
64
  );
65
- const img = getByRole('img');
65
+ const img = container.querySelector('img');
66
66
  expect(img).toHaveAttribute('src', 'https://plone.org/logo.jpg');
67
67
  });
68
68
  test('renders a view image component with an external image with a link', () => {
69
- const { container, getByRole } = render(
69
+ const { container } = render(
70
70
  <Provider store={store}>
71
71
  <View
72
72
  data={{
@@ -76,7 +76,7 @@ describe('Image View Component', () => {
76
76
  />
77
77
  </Provider>,
78
78
  );
79
- const img = getByRole('img');
79
+ const img = container.querySelector('img');
80
80
  const a = container.querySelector('a');
81
81
  expect(img).toHaveAttribute('src', 'https://plone.org/logo.jpg');
82
82
  expect(a).toHaveAttribute('href', 'http://front-page');
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Breadcrumb } from 'semantic-ui-react';
3
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
3
4
  import { Link, useLocation } from 'react-router-dom';
4
5
  import { defineMessages, useIntl } from 'react-intl';
5
6
  import langmap from '@plone/volto/helpers/LanguageMap/LanguageMap';
@@ -41,22 +42,22 @@ const ContentsBreadcrumbs = (props) => {
41
42
  </>
42
43
  )}
43
44
  {settings.isMultilingual && pathname?.split('/')?.length > 2 && (
44
- <Link
45
- to={`/${lang}/contents`}
45
+ <UniversalLink
46
+ href={`/${lang}/contents`}
46
47
  className="section"
47
48
  title={intl.formatMessage(messages.home)}
48
49
  >
49
50
  {langmap?.[lang]?.nativeName ?? lang}
50
- </Link>
51
+ </UniversalLink>
51
52
  )}
52
53
  {!settings.isMultilingual && (
53
- <Link
54
- to="/contents"
54
+ <UniversalLink
55
+ href="/contents"
55
56
  className="section"
56
57
  title={intl.formatMessage(messages.home)}
57
58
  >
58
59
  <ContentsBreadcrumbsHomeItem />
59
- </Link>
60
+ </UniversalLink>
60
61
  )}
61
62
  {items.map((breadcrumb, index, breadcrumbs) => [
62
63
  <Breadcrumb.Divider key={`divider-${breadcrumb.url}`} />,
@@ -58,6 +58,7 @@ const withObjectBrowser = (WrappedComponent) =>
58
58
  selectableTypes,
59
59
  maximumSelectionSize,
60
60
  currentPath,
61
+ initialPath,
61
62
  onlyFolderishSelectable,
62
63
  } = {}) =>
63
64
  this.setState(() => ({
@@ -71,6 +72,7 @@ const withObjectBrowser = (WrappedComponent) =>
71
72
  selectableTypes,
72
73
  maximumSelectionSize,
73
74
  currentPath,
75
+ initialPath,
74
76
  onlyFolderishSelectable,
75
77
  }));
76
78
 
@@ -82,6 +84,10 @@ const withObjectBrowser = (WrappedComponent) =>
82
84
  this.props.pathname ||
83
85
  this.props.location?.pathname;
84
86
 
87
+ let initialPath = this.state?.initialPath
88
+ ? getBaseUrl(this.state.initialPath)
89
+ : null;
90
+
85
91
  return (
86
92
  <>
87
93
  <WrappedComponent
@@ -105,6 +111,7 @@ const withObjectBrowser = (WrappedComponent) =>
105
111
  : this.props.data
106
112
  }
107
113
  contextURL={getBaseUrl(contextURL)}
114
+ initialPath={initialPath}
108
115
  closeObjectBrowser={this.closeObjectBrowser}
109
116
  mode={this.state.mode}
110
117
  onSelectItem={this.state.onSelectItem}
@@ -84,6 +84,7 @@ class ObjectBrowserBody extends Component {
84
84
  onSelectItem: PropTypes.func,
85
85
  dataName: PropTypes.string,
86
86
  maximumSelectionSize: PropTypes.number,
87
+ initialPath: PropTypes.string,
87
88
  contextURL: PropTypes.string,
88
89
  searchableTypes: PropTypes.arrayOf(PropTypes.string),
89
90
  onlyFolderishSelectable: PropTypes.bool,
@@ -113,18 +114,21 @@ class ObjectBrowserBody extends Component {
113
114
  */
114
115
  constructor(props) {
115
116
  super(props);
117
+ const defaultMultiplePath = props.initialPath || '/';
116
118
  this.state = {
117
119
  currentFolder:
118
- this.props.mode === 'multiple' ? '/' : this.props.contextURL || '/',
120
+ this.props.mode === 'multiple'
121
+ ? defaultMultiplePath
122
+ : this.props.contextURL || '/',
119
123
  currentImageFolder:
120
124
  this.props.mode === 'multiple'
121
- ? '/'
125
+ ? defaultMultiplePath
122
126
  : this.props.mode === 'image' && this.props.data?.url
123
127
  ? getParentURL(this.props.data.url)
124
128
  : '/',
125
129
  currentLinkFolder:
126
130
  this.props.mode === 'multiple'
127
- ? '/'
131
+ ? defaultMultiplePath
128
132
  : this.props.mode === 'link' && this.props.data?.href
129
133
  ? getParentURL(this.props.data.href)
130
134
  : '/',
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import ObjectBrowserBody from './ObjectBrowserBody';
6
+
7
+ const mockStore = configureStore();
8
+
9
+ const baseState = {
10
+ search: { subrequests: {} },
11
+ intl: { locale: 'en', messages: {} },
12
+ };
13
+
14
+ const baseProps = {
15
+ block: 'test-block',
16
+ data: {},
17
+ closeObjectBrowser: () => {},
18
+ onChangeBlock: () => {},
19
+ };
20
+
21
+ const getInitialSearchPath = (actions) => {
22
+ const action = actions.find((a) => a.type === 'SEARCH_CONTENT');
23
+ return action?.request?.path?.split('/@search')[0];
24
+ };
25
+
26
+ describe('ObjectBrowserBody', () => {
27
+ it('uses initialPath as the default folder when mode=multiple', () => {
28
+ const store = mockStore(baseState);
29
+ render(
30
+ <Provider store={store}>
31
+ <ObjectBrowserBody
32
+ {...baseProps}
33
+ mode="multiple"
34
+ initialPath="/company/team"
35
+ />
36
+ </Provider>,
37
+ );
38
+
39
+ expect(getInitialSearchPath(store.getActions())).toBe('/company/team');
40
+ });
41
+
42
+ it('defaults to root when mode=multiple and initialPath is not provided', () => {
43
+ const store = mockStore(baseState);
44
+ render(
45
+ <Provider store={store}>
46
+ <ObjectBrowserBody {...baseProps} mode="multiple" />
47
+ </Provider>,
48
+ );
49
+
50
+ expect(getInitialSearchPath(store.getActions())).toBe('/');
51
+ });
52
+ });
@@ -315,7 +315,8 @@ class ArrayWidget extends Component {
315
315
  // small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
316
316
  getHelperDimensions={({ node }) => node.getBoundingClientRect()}
317
317
  id={`field-${this.props.id}`}
318
- aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${this.props.id}`}
318
+ fieldTitle={this.props.title}
319
+ aria-label={this.props.title || undefined}
319
320
  key={this.props.id}
320
321
  isDisabled={this.props.disabled || this.props.isDisabled}
321
322
  className="react-select-container"
@@ -384,7 +385,7 @@ class ArrayWidget extends Component {
384
385
  )
385
386
  )
386
387
  }
387
- isClearable
388
+ isClearable={!this.props.required}
388
389
  isMulti
389
390
  />
390
391
  </FormFieldWrapper>
@@ -2,9 +2,14 @@
2
2
  * FormFieldWrapper component.
3
3
  * @module components/manage/Widgets/FormFieldWrapper
4
4
  */
5
- import React, { Component } from 'react';
5
+ import React, {
6
+ Children,
7
+ Component,
8
+ isValidElement,
9
+ cloneElement,
10
+ } from 'react';
6
11
  import PropTypes from 'prop-types';
7
- import { Form, Grid, Icon as IconOld, Label } from 'semantic-ui-react';
12
+ import { Form, Grid, Icon as IconOld, Input, Label } from 'semantic-ui-react';
8
13
  import map from 'lodash/map';
9
14
  import cx from 'classnames';
10
15
  import { defineMessages, injectIntl } from 'react-intl';
@@ -103,7 +108,15 @@ class FormFieldWrapper extends Component {
103
108
 
104
109
  const wdg = (
105
110
  <>
106
- {this.props.children}
111
+ {Children.map(this.props.children, (child) => {
112
+ if (isValidElement(child) && required && child.type === Input) {
113
+ return cloneElement(child, {
114
+ 'aria-required': true,
115
+ 'aria-invalid': !!(error && error.length > 0),
116
+ });
117
+ }
118
+ return child;
119
+ })}
107
120
 
108
121
  <div aria-live="polite" aria-atomic="true">
109
122
  {map(error, (message) => (
@@ -307,6 +307,7 @@ export class ObjectBrowserWidgetComponent extends Component {
307
307
  this.props.openObjectBrowser({
308
308
  mode: this.props.mode,
309
309
  currentPath: this.props.initialPath || this.props.location.pathname,
310
+ initialPath: this.props.initialPath,
310
311
  propDataName: 'value',
311
312
  onSelectItem: (url, item) => {
312
313
  this.onChange(item);
@@ -7,10 +7,6 @@ const messages = defineMessages({
7
7
  id: 'Criteria',
8
8
  defaultMessage: 'Criteria',
9
9
  },
10
- depth: {
11
- id: 'Depth',
12
- defaultMessage: 'Depth',
13
- },
14
10
  SortOn: {
15
11
  id: 'Sort on',
16
12
  defaultMessage: 'Sort on',
@@ -38,16 +34,7 @@ export const objectSchema = ({ intl, isDisabled, value }) => ({
38
34
  {
39
35
  id: 'default',
40
36
  title: 'Default',
41
- fields: [
42
- 'query',
43
- ...(value?.query?.filter((q) => q.i === 'path').length > 0
44
- ? ['depth']
45
- : []),
46
- 'sort_on',
47
- 'sort_order_boolean',
48
- 'limit',
49
- 'b_size',
50
- ],
37
+ fields: ['query', 'sort_on', 'sort_order_boolean', 'limit', 'b_size'],
51
38
  },
52
39
  ],
53
40
  properties: {
@@ -55,10 +42,6 @@ export const objectSchema = ({ intl, isDisabled, value }) => ({
55
42
  title: intl.formatMessage(messages.Criteria),
56
43
  widget: 'query',
57
44
  },
58
- depth: {
59
- title: intl.formatMessage(messages.depth),
60
- type: 'number',
61
- },
62
45
  sort_on: {
63
46
  title: intl.formatMessage(messages.SortOn),
64
47
  widget: 'query_sort_on',
@@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
5
  import { waitFor } from '@testing-library/react';
6
6
 
7
- import QuerystringWidget from './QuerystringWidget';
7
+ import QuerystringWidget, { objectSchema } from './QuerystringWidget';
8
8
 
9
9
  const mockStore = configureStore();
10
10
 
@@ -25,6 +25,47 @@ test('renders an querystring widget component', async () => {
25
25
  expect(component.toJSON()).toMatchSnapshot();
26
26
  });
27
27
 
28
+ describe('objectSchema', () => {
29
+ const mockIntl = { formatMessage: ({ defaultMessage }) => defaultMessage };
30
+
31
+ it('does not include depth in the default fieldset', () => {
32
+ const schema = objectSchema({ intl: mockIntl });
33
+ const fields = schema.fieldsets[0].fields;
34
+
35
+ expect(fields).not.toContain('depth');
36
+ expect(fields).toEqual([
37
+ 'query',
38
+ 'sort_on',
39
+ 'sort_order_boolean',
40
+ 'limit',
41
+ 'b_size',
42
+ ]);
43
+ });
44
+
45
+ it('does not include depth in the schema properties', () => {
46
+ const schema = objectSchema({ intl: mockIntl });
47
+
48
+ expect(schema.properties).not.toHaveProperty('depth');
49
+ });
50
+
51
+ it('does not include depth even when value contains a path criterion', () => {
52
+ const value = {
53
+ query: [
54
+ {
55
+ i: 'path',
56
+ o: 'plone.app.querystring.operation.string.path',
57
+ v: '/folder',
58
+ },
59
+ ],
60
+ };
61
+ const schema = objectSchema({ intl: mockIntl, value });
62
+ const fields = schema.fieldsets[0].fields;
63
+
64
+ expect(fields).not.toContain('depth');
65
+ expect(schema.properties).not.toHaveProperty('depth');
66
+ });
67
+ });
68
+
28
69
  test('can take a schemaEnhancer', async () => {
29
70
  const store = mockStore({
30
71
  querystring: { indexes: {} },
@@ -51,6 +92,8 @@ test('can take a schemaEnhancer', async () => {
51
92
  />
52
93
  </Provider>,
53
94
  );
54
- await waitFor(() => {});
95
+ await waitFor(() => {
96
+ expect(component.toJSON()?.children).toHaveLength(3);
97
+ });
55
98
  expect(component.toJSON()).toMatchSnapshot();
56
99
  });
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { defineMessages, useIntl } from 'react-intl';
2
3
  import { Popup } from 'semantic-ui-react';
3
4
  import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
4
5
  import Icon from '@plone/volto/components/theme/Icon/Icon';
@@ -10,6 +11,13 @@ import checkSVG from '@plone/volto/icons/check.svg';
10
11
  import checkBlankSVG from '@plone/volto/icons/check-blank.svg';
11
12
  import clearSVG from '@plone/volto/icons/clear.svg';
12
13
 
14
+ const messages = defineMessages({
15
+ clearSelection: {
16
+ id: 'Clear selection',
17
+ defaultMessage: 'Clear selection',
18
+ },
19
+ });
20
+
13
21
  export const MenuList = ({ children }) => {
14
22
  return <DynamicHeightList>{children}</DynamicHeightList>;
15
23
  };
@@ -86,8 +94,32 @@ export const DropdownIndicator = injectLazyLibs('reactSelect')((props) => {
86
94
 
87
95
  export const ClearIndicator = injectLazyLibs('reactSelect')((props) => {
88
96
  const { ClearIndicator } = props.reactSelect.components;
97
+ const intl = useIntl();
98
+ const fieldLabelId = props.selectProps?.['aria-labelledby'];
99
+ const clearLabelId = `${props.selectProps?.inputId}-clear-label`;
89
100
  return (
90
- <ClearIndicator {...props}>
101
+ <ClearIndicator
102
+ {...props}
103
+ innerProps={{
104
+ ...props.innerProps,
105
+ 'aria-hidden': false,
106
+ ...(fieldLabelId
107
+ ? { 'aria-labelledby': `${fieldLabelId} ${clearLabelId}` }
108
+ : { 'aria-label': intl.formatMessage(messages.clearSelection) }),
109
+ role: 'button',
110
+ tabIndex: 0,
111
+ onKeyDown: (e) => {
112
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
113
+ e.preventDefault();
114
+ e.stopPropagation();
115
+ props.clearValue();
116
+ }
117
+ },
118
+ }}
119
+ >
120
+ <span id={clearLabelId} hidden>
121
+ {intl.formatMessage(messages.clearSelection)}
122
+ </span>
91
123
  <Icon name={clearSVG} size="18px" color="#e40166" />
92
124
  </ClearIndicator>
93
125
  );
@@ -263,9 +263,10 @@ class SelectWidget extends Component {
263
263
  <FormFieldWrapper {...this.props}>
264
264
  <Select
265
265
  id={`field-${id}`}
266
+ fieldTitle={this.props.title}
266
267
  key={choices}
267
268
  name={id}
268
- aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${id}`}
269
+ aria-label={this.props.title || undefined}
269
270
  menuShouldScrollIntoView={false}
270
271
  isDisabled={disabled}
271
272
  isSearchable={true}
@@ -304,7 +305,7 @@ class SelectWidget extends Component {
304
305
  : undefined,
305
306
  );
306
307
  }}
307
- isClearable={this.props.isClearable}
308
+ isClearable={!this.props.required && this.props.isClearable}
308
309
  />
309
310
  </FormFieldWrapper>
310
311
  );
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
+ import { render, screen } from '@testing-library/react';
3
4
  import configureStore from 'redux-mock-store';
4
5
  import { Provider } from 'react-intl-redux';
5
6
 
@@ -30,3 +31,46 @@ test('renders a text widget component', () => {
30
31
  const json = component.toJSON();
31
32
  expect(json).toMatchSnapshot();
32
33
  });
34
+
35
+ test('adds aria-required attribute to input when required prop is true', () => {
36
+ const store = mockStore({
37
+ intl: { locale: 'en', messages: {} },
38
+ });
39
+
40
+ render(
41
+ <Provider store={store}>
42
+ <TextWidget
43
+ id="my-field"
44
+ title="My field"
45
+ onChange={() => {}}
46
+ onBlur={() => {}}
47
+ onClick={() => {}}
48
+ required={true}
49
+ />
50
+ </Provider>,
51
+ );
52
+
53
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-required', 'true');
54
+ });
55
+
56
+ test('adds aria-invalid attribute to input when field has errors', () => {
57
+ const store = mockStore({
58
+ intl: { locale: 'en', messages: {} },
59
+ });
60
+
61
+ render(
62
+ <Provider store={store}>
63
+ <TextWidget
64
+ id="my-field"
65
+ title="My field"
66
+ onChange={() => {}}
67
+ onBlur={() => {}}
68
+ onClick={() => {}}
69
+ required={true}
70
+ error={['This field is required']}
71
+ />
72
+ </Provider>,
73
+ );
74
+
75
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
76
+ });
@@ -4,7 +4,6 @@ import { Container } from 'semantic-ui-react';
4
4
  import { useSelector } from 'react-redux';
5
5
  import { useLocation } from 'react-router-dom';
6
6
  import { withServerErrorCode } from '@plone/volto/helpers/Utils/Utils';
7
- import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
8
7
  import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
9
8
 
10
9
  const Unauthorized = () => {
@@ -33,7 +32,7 @@ const Unauthorized = () => {
33
32
  login: (
34
33
  <Link
35
34
  to={{
36
- pathname: `${getBaseUrl(location.pathname)}/login`,
35
+ pathname: `${location.pathname.replace(/\/$/, '')}/login`,
37
36
  state: {
38
37
  // This is needed to cover the use case of being logged in in
39
38
  // another backend (eg. in development), having a token for
@@ -0,0 +1,89 @@
1
+ import { injectIntl } from 'react-intl';
2
+ import React from 'react';
3
+ import EventViewComponent from './EventView';
4
+ import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
5
+ import config from '@plone/volto/registry';
6
+ const IntlEventViewComponent = injectIntl(EventViewComponent);
7
+ const { settings } = config;
8
+
9
+ function StoryComponent(args) {
10
+ return (
11
+ <Wrapper
12
+ customStore={{
13
+ intl: {
14
+ locale: 'en',
15
+ messages: {},
16
+ },
17
+ }}
18
+ >
19
+ <div id="toolbar" style={{ display: 'none' }} />
20
+ <IntlEventViewComponent
21
+ content={{
22
+ '@id': 'http://localhost:8080/Plone/my-page',
23
+ ...args,
24
+ }}
25
+ />
26
+ </Wrapper>
27
+ );
28
+ }
29
+
30
+ export const Default = StoryComponent.bind({});
31
+ Default.args = {
32
+ title: 'Hello World!',
33
+ end: '2019-06-23T16:20:00+00:00',
34
+ start: '2019-06-23T15:20:00+00:00',
35
+ attendees: [],
36
+ subjects: [],
37
+ };
38
+
39
+ export const EventViewAllProps = StoryComponent.bind({});
40
+ EventViewAllProps.args = {
41
+ title: 'Hello World!',
42
+ description: 'Hi',
43
+ text: {
44
+ data: '<p>Hello World!</p>',
45
+ },
46
+ attendees: ['John Doe', 'Mario Rossi'],
47
+ contact_email: 'test@example.com',
48
+ contact_name: 'John Doe',
49
+ contact_phone: '0123456789',
50
+ end: '2019-06-24T15:20:00+00:00',
51
+ event_url: 'https://www.example.com',
52
+ location: 'Volto, Plone',
53
+ open_end: false,
54
+ recurrence: 'RRULE:FREQ=DAILY;INTERVAL=7;COUNT=7',
55
+ start: '2019-06-23T15:20:00+00:00',
56
+ subjects: ['Guillotina', 'Volto'],
57
+ whole_day: false,
58
+ };
59
+
60
+ export const EventViewWithoutLinkToApiInTheText = StoryComponent.bind({});
61
+ EventViewWithoutLinkToApiInTheText.args = {
62
+ title: 'Hello World!',
63
+ attendees: [],
64
+ end: '2019-06-23T16:20:00+00:00',
65
+ start: '2019-06-23T15:20:00+00:00',
66
+ subjects: [],
67
+ text: {
68
+ data: `<p>Hello World!</p><p>This is an <a href="${settings.apiPath}/foo/bar">internal link</a> and a <a href="${settings.apiPath}/foo/baz">second link</a></p>`,
69
+ },
70
+ };
71
+ export default {
72
+ title: 'Public components/View/EventView',
73
+ component: EventViewComponent,
74
+ decorators: [
75
+ (Story) => (
76
+ <div className="ui segment form attached" style={{ width: '900px' }}>
77
+ <Story />
78
+ </div>
79
+ ),
80
+ ],
81
+ argTypes: {
82
+ end: {
83
+ control: 'date',
84
+ },
85
+ start: {
86
+ control: 'date',
87
+ },
88
+ },
89
+ };
@@ -0,0 +1,50 @@
1
+ import { injectIntl } from 'react-intl';
2
+ import React from 'react';
3
+ import FileViewComponent from './FileView';
4
+ import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
5
+
6
+ const IntlFileViewComponent = injectIntl(FileViewComponent);
7
+
8
+ function StoryComponent(args) {
9
+ return (
10
+ <Wrapper customStore={{}}>
11
+ <div id="toolbar" style={{ display: 'none' }} />
12
+ <IntlFileViewComponent
13
+ content={{
14
+ ...args,
15
+ file: {
16
+ download: 'file:///preview.pdf',
17
+ ...args,
18
+ },
19
+ }}
20
+ />
21
+ </Wrapper>
22
+ );
23
+ }
24
+
25
+ export const Default = StoryComponent.bind({});
26
+ Default.args = {
27
+ title: 'Hello World!',
28
+ description: 'Hi',
29
+ filename: 'preview.pdf',
30
+ };
31
+
32
+ export default {
33
+ title: 'Public components/View/FileView',
34
+ component: FileViewComponent,
35
+ decorators: [
36
+ (Story) => (
37
+ <div className="ui segment form attached" style={{ width: '900px' }}>
38
+ <Story />
39
+ </div>
40
+ ),
41
+ ],
42
+ argTypes: {
43
+ title: {
44
+ description: 'Title of the component',
45
+ },
46
+ filename: {
47
+ description: 'Name of the file',
48
+ },
49
+ },
50
+ };