@plone/volto 15.0.0-alpha.0 → 15.0.0-alpha.11

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 (236) hide show
  1. package/.eslintrc +1 -1
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +7 -6
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  4. package/.github/workflows/{core-sandbox.yml → coresandbox.yml} +1 -1
  5. package/.github/workflows/docs.yml +15 -55
  6. package/.storybook/main.js +3 -9
  7. package/CHANGELOG.md +140 -2
  8. package/CODE_OF_CONDUCT.md +0 -1
  9. package/COMMITLINT.md +19 -21
  10. package/README.md +2 -1
  11. package/SECURITY.md +0 -1
  12. package/addon-registry.js +20 -12
  13. package/create-addons-loader.js +1 -1
  14. package/cypress/support/commands.js +3 -1
  15. package/dist/assets.json +1 -0
  16. package/dist/chunks.json +571 -0
  17. package/dist/loadable-stats.json +2070 -0
  18. package/dist/public/android-chrome-192x192.png +0 -0
  19. package/dist/public/android-chrome-512x512.png +0 -0
  20. package/dist/public/apple-touch-icon.png +0 -0
  21. package/dist/public/favicon-16x16.png +0 -0
  22. package/dist/public/favicon-32x32.png +0 -0
  23. package/dist/public/favicon.ico +0 -0
  24. package/dist/public/icon.svg +13 -0
  25. package/dist/public/index.html.spa +34 -0
  26. package/dist/public/robots.txt +2 -0
  27. package/dist/public/site.webmanifest +19 -0
  28. package/dist/public/static/css/14.803b450c.chunk.css +409 -0
  29. package/dist/public/static/css/3.a3490306.chunk.css +2 -0
  30. package/dist/public/static/css/client.0ead607b.chunk.css +8 -0
  31. package/dist/public/static/css/plone-volto-components-manage-Widgets-DatetimeWidget.77a7ad0d.chunk.css +2 -0
  32. package/dist/public/static/js/0.3e794c57.chunk.js +2 -0
  33. package/dist/public/static/js/1.fece2b93.chunk.js +3 -0
  34. package/dist/public/static/js/1.fece2b93.chunk.js.LICENSE.txt +8 -0
  35. package/dist/public/static/js/14.803b450c.chunk.js +3 -0
  36. package/dist/public/static/js/14.803b450c.chunk.js.LICENSE.txt +146 -0
  37. package/dist/public/static/js/15.c1eb25b4.chunk.js +3 -0
  38. package/dist/public/static/js/15.c1eb25b4.chunk.js.LICENSE.txt +8 -0
  39. package/dist/public/static/js/16.70e24cba.chunk.js +2 -0
  40. package/dist/public/static/js/17.f03d2a82.chunk.js +2 -0
  41. package/dist/public/static/js/18.0f0b1dfa.chunk.js +2 -0
  42. package/dist/public/static/js/19.61ef8815.chunk.js +3 -0
  43. package/dist/public/static/js/19.61ef8815.chunk.js.LICENSE.txt +8 -0
  44. package/dist/public/static/js/20.6b37f925.chunk.js +2 -0
  45. package/dist/public/static/js/21.8704b265.chunk.js +2 -0
  46. package/dist/public/static/js/22.5a2b56ff.chunk.js +2 -0
  47. package/dist/public/static/js/23.a53c8d7b.chunk.js +2 -0
  48. package/dist/public/static/js/24.d44c620a.chunk.js +2 -0
  49. package/dist/public/static/js/25.3129d6c8.chunk.js +2 -0
  50. package/dist/public/static/js/26.9dfad319.chunk.js +2 -0
  51. package/dist/public/static/js/27.efbb318a.chunk.js +2 -0
  52. package/dist/public/static/js/28.b4840d60.chunk.js +2 -0
  53. package/dist/public/static/js/29.690d4b54.chunk.js +2 -0
  54. package/dist/public/static/js/3.a3490306.chunk.js +2 -0
  55. package/dist/public/static/js/30.5f174f94.chunk.js +2 -0
  56. package/dist/public/static/js/31.4a7cf109.chunk.js +2 -0
  57. package/dist/public/static/js/32.2247dcf7.chunk.js +2 -0
  58. package/dist/public/static/js/33.87874f33.chunk.js +2 -0
  59. package/dist/public/static/js/34.aec37d56.chunk.js +3 -0
  60. package/dist/public/static/js/34.aec37d56.chunk.js.LICENSE.txt +37 -0
  61. package/dist/public/static/js/35.73887e02.chunk.js +3 -0
  62. package/dist/public/static/js/35.73887e02.chunk.js.LICENSE.txt +7 -0
  63. package/dist/public/static/js/36.19d2e05a.chunk.js +3 -0
  64. package/dist/public/static/js/36.19d2e05a.chunk.js.LICENSE.txt +14 -0
  65. package/dist/public/static/js/37.96925a5f.chunk.js +3 -0
  66. package/dist/public/static/js/37.96925a5f.chunk.js.LICENSE.txt +8 -0
  67. package/dist/public/static/js/38.701a2e85.chunk.js +2 -0
  68. package/dist/public/static/js/39.4cbf0549.chunk.js +2 -0
  69. package/dist/public/static/js/4.bdc4a5c5.chunk.js +2 -0
  70. package/dist/public/static/js/6.69ba4792.chunk.js +2 -0
  71. package/dist/public/static/js/client.0ead607b.chunk.js +2 -0
  72. package/dist/public/static/js/draft-js-lib-isSoftNewlineEvent.ad4b1307.chunk.js +2 -0
  73. package/dist/public/static/js/moment.dfd83abb.chunk.js +2 -0
  74. package/dist/public/static/js/plone-volto-components-manage-Widgets-DatetimeWidget.77a7ad0d.chunk.js +2 -0
  75. package/dist/public/static/js/plone-volto-components-theme-View-EventView.c500936a.chunk.js +2 -0
  76. package/dist/public/static/js/react-dates.d94a34ee.chunk.js +2 -0
  77. package/dist/public/static/js/react-select.43f17559.chunk.js +2 -0
  78. package/dist/public/static/js/react-simple-code-editor.484ec69f.chunk.js +2 -0
  79. package/dist/public/static/js/runtime~client.c0921d19.js +2 -0
  80. package/dist/public/static/media/Logo.16e25cdf.svg +21 -0
  81. package/dist/public/static/media/block-image.8a4a14d5.svg +8 -0
  82. package/dist/public/static/media/block-maps.731e84f6.svg +7 -0
  83. package/dist/public/static/media/block-video.832fa3ff.svg +7 -0
  84. package/dist/public/static/media/default-image.9451b251.svg +13 -0
  85. package/dist/public/static/media/flags.9c74e172.png +0 -0
  86. package/dist/public/static/media/icons.022583c4.svg +320 -0
  87. package/dist/public/static/media/icons.06400145.ttf +0 -0
  88. package/dist/public/static/media/icons.dbc1ef8b.eot +0 -0
  89. package/dist/public/static/media/icons.df45b7de.woff +0 -0
  90. package/dist/server.js +2 -0
  91. package/docker-compose.yml +15 -10
  92. package/locales/ca/LC_MESSAGES/volto.po +33 -9
  93. package/locales/ca.json +1 -1
  94. package/locales/de/LC_MESSAGES/volto.po +33 -9
  95. package/locales/de.json +1 -1
  96. package/locales/en/LC_MESSAGES/volto.po +33 -9
  97. package/locales/en.json +1 -1
  98. package/locales/es/LC_MESSAGES/volto.po +33 -9
  99. package/locales/es.json +1 -1
  100. package/locales/eu/LC_MESSAGES/volto.po +33 -9
  101. package/locales/eu.json +1 -1
  102. package/locales/fr/LC_MESSAGES/volto.po +33 -9
  103. package/locales/fr.json +1 -1
  104. package/locales/it/LC_MESSAGES/volto.po +33 -9
  105. package/locales/it.json +1 -1
  106. package/locales/ja/LC_MESSAGES/volto.po +33 -9
  107. package/locales/ja.json +1 -1
  108. package/locales/nl/LC_MESSAGES/volto.po +33 -9
  109. package/locales/nl.json +1 -1
  110. package/locales/pt/LC_MESSAGES/volto.po +33 -9
  111. package/locales/pt.json +1 -1
  112. package/locales/pt_BR/LC_MESSAGES/volto.po +33 -9
  113. package/locales/pt_BR.json +1 -1
  114. package/locales/ro/LC_MESSAGES/volto.po +33 -9
  115. package/locales/ro.json +1 -1
  116. package/locales/volto.pot +34 -10
  117. package/logos/Logo.png +0 -0
  118. package/logos/volto-colorful.png +0 -0
  119. package/logos/volto-colorful.svg +35 -0
  120. package/logos/volto-guide.png +0 -0
  121. package/logos/volto-h-transparent.svg +6 -0
  122. package/logos/volto-transparent.png +0 -0
  123. package/logos/volto-transparent.svg +19 -0
  124. package/logos/volto-transparent2.svg +5 -0
  125. package/logos/volto-yellow.svg +29 -0
  126. package/logos/volto.sketch +0 -0
  127. package/netlify.toml +2 -0
  128. package/package.json +22 -12
  129. package/pyvenv.cfg +3 -0
  130. package/razzle.config.js +17 -13
  131. package/src/actions/language/language.js +23 -11
  132. package/src/actions/lazyLibraries/lazyLibraries.js +8 -4
  133. package/src/components/index.js +2 -0
  134. package/src/components/manage/Add/Add.jsx +1 -1
  135. package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +1 -2
  136. package/src/components/manage/AnchorPlugin/components/LinkButton/index.jsx +15 -6
  137. package/src/components/manage/AnchorPlugin/index.jsx +12 -5
  138. package/src/components/manage/AnchorPlugin/utils/EditorUtils.js +8 -7
  139. package/src/components/manage/Blocks/Description/Edit.jsx +33 -19
  140. package/src/components/manage/Blocks/Description/Edit.test.jsx +6 -0
  141. package/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +37 -26
  142. package/src/components/manage/Blocks/HeroImageLeft/Edit.test.jsx +6 -0
  143. package/src/components/manage/Blocks/Listing/View.jsx +1 -1
  144. package/src/components/manage/Blocks/Search/README.md +20 -0
  145. package/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx +16 -3
  146. package/src/components/manage/Blocks/Search/components/DateRangeFacet.jsx +120 -0
  147. package/src/components/manage/Blocks/Search/components/DateRangeFacet.test.jsx +34 -0
  148. package/src/components/manage/Blocks/Search/components/DateRangeFacetFilterListEntry.jsx +39 -0
  149. package/src/components/manage/Blocks/Search/components/Facets.jsx +24 -25
  150. package/src/components/manage/Blocks/Search/components/FilterList.jsx +37 -67
  151. package/src/components/manage/Blocks/Search/components/FilterList.test.jsx +49 -1
  152. package/src/components/manage/Blocks/Search/components/SelectFacet.jsx +9 -0
  153. package/src/components/manage/Blocks/Search/components/SelectFacetFilterListEntry.jsx +42 -0
  154. package/src/components/manage/Blocks/Search/components/SelectStyling.jsx +1 -1
  155. package/src/components/manage/Blocks/Search/components/ToggleFacet.jsx +38 -0
  156. package/src/components/manage/Blocks/Search/components/ToggleFacetFilterListEntry.jsx +39 -0
  157. package/src/components/manage/Blocks/Search/components/base.js +52 -0
  158. package/src/components/manage/Blocks/Search/components/index.js +5 -0
  159. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +68 -27
  160. package/src/components/manage/Blocks/Search/schema.js +7 -4
  161. package/src/components/manage/Blocks/Search/utils.js +16 -0
  162. package/src/components/manage/Blocks/Table/Cell.jsx +40 -14
  163. package/src/components/manage/Blocks/Table/Cell.test.jsx +7 -1
  164. package/src/components/manage/Blocks/Table/Edit.jsx +7 -3
  165. package/src/components/manage/Blocks/Table/Edit.test.jsx +6 -0
  166. package/src/components/manage/Blocks/Table/View.jsx +2 -2
  167. package/src/components/manage/Blocks/Text/Edit.jsx +56 -14
  168. package/src/components/manage/Blocks/Text/Edit.test.jsx +7 -1
  169. package/src/components/manage/Blocks/Text/View.jsx +2 -2
  170. package/src/components/manage/Blocks/Title/Edit.jsx +25 -13
  171. package/src/components/manage/Blocks/Title/Edit.test.jsx +6 -0
  172. package/src/components/manage/Contents/ContentsIndexHeader.test.jsx +2 -1
  173. package/src/components/manage/Contents/ContentsUploadModal.jsx +3 -1
  174. package/src/components/manage/Multilingual/CreateTranslation.jsx +1 -1
  175. package/src/components/manage/Multilingual/TranslationObject.jsx +1 -1
  176. package/src/components/manage/Pluggable/index.js +1 -1
  177. package/src/components/manage/Preferences/PersonalPreferences.jsx +1 -1
  178. package/src/components/manage/Sidebar/Sidebar.jsx +5 -1
  179. package/src/components/manage/Toolbar/Toolbar.jsx +3 -1
  180. package/src/components/manage/UniversalLink/UniversalLink.jsx +5 -2
  181. package/src/components/manage/Widgets/DatetimeWidget.jsx +3 -1
  182. package/src/components/manage/Widgets/IdWidget.jsx +234 -0
  183. package/src/components/manage/Widgets/IdWidget.stories.jsx +21 -0
  184. package/src/components/manage/Widgets/IdWidget.test.jsx +135 -0
  185. package/src/components/manage/Widgets/SelectAutoComplete.jsx +2 -2
  186. package/src/components/manage/Widgets/UrlWidget.jsx +1 -1
  187. package/src/components/manage/Widgets/WysiwygWidget.jsx +45 -19
  188. package/src/components/manage/Widgets/WysiwygWidget.test.jsx +7 -1
  189. package/src/components/theme/ContactForm/ContactForm.jsx +1 -1
  190. package/src/components/theme/EventDetails/EventDetails.jsx +140 -0
  191. package/src/components/theme/EventDetails/EventDetails.test.jsx +87 -0
  192. package/src/components/theme/LanguageSelector/LanguageSelector.js +2 -18
  193. package/src/components/theme/Login/Login.jsx +3 -1
  194. package/src/components/theme/MultilingualRedirector/MultilingualRedirector.jsx +10 -31
  195. package/src/components/theme/View/EventView.jsx +62 -140
  196. package/src/components/theme/View/LinkView.jsx +3 -3
  197. package/src/components/theme/View/NewsItemView.jsx +44 -35
  198. package/src/components/theme/View/View.jsx +5 -1
  199. package/src/config/Blocks.jsx +31 -0
  200. package/src/config/Loadables.jsx +22 -0
  201. package/src/config/RichTextEditor/Blocks.jsx +25 -20
  202. package/src/config/RichTextEditor/Plugins.jsx +38 -31
  203. package/src/config/RichTextEditor/Styles.jsx +48 -44
  204. package/src/config/RichTextEditor/index.js +25 -0
  205. package/src/config/Widgets.jsx +2 -0
  206. package/src/config/index.js +15 -18
  207. package/src/helpers/Api/Api.js +20 -4
  208. package/src/helpers/Extensions/index.js +1 -1
  209. package/src/helpers/Extensions/withBlockExtensions.js +4 -0
  210. package/src/helpers/Extensions/withBlockExtensions.test.js +26 -1
  211. package/src/helpers/Url/Url.js +41 -1
  212. package/src/helpers/Url/Url.test.js +7 -0
  213. package/src/helpers/Url/urlRegex.js +7 -1
  214. package/src/helpers/index.js +3 -0
  215. package/src/middleware/api.js +35 -3
  216. package/src/server.jsx +20 -6
  217. package/src/start-client.jsx +2 -2
  218. package/src/store.js +1 -1
  219. package/test-setup-config.js +22 -7
  220. package/theme/themes/pastanaga/extras/blocks.less +29 -0
  221. package/theme/themes/pastanaga/extras/contents.less +6 -0
  222. package/{webpack-bundle-analyze-plugin.js → webpack-plugins/webpack-bundle-analyze-plugin.js} +0 -0
  223. package/{webpack-less-plugin.js → webpack-plugins/webpack-less-plugin.js} +0 -0
  224. package/{webpack-relative-resolver.js → webpack-plugins/webpack-relative-resolver.js} +0 -0
  225. package/{webpack-root-resolver.js → webpack-plugins/webpack-root-resolver.js} +0 -0
  226. package/{webpack-sentry-plugin.js → webpack-plugins/webpack-sentry-plugin.js} +0 -0
  227. package/{webpack-svg-plugin.js → webpack-plugins/webpack-svg-plugin.js} +0 -0
  228. package/.surgeignore +0 -2
  229. package/Dockerfile.slim +0 -27
  230. package/patches/fixprettier.patch +0 -11
  231. package/patches/fixstylelint.patch +0 -11
  232. package/src/components/manage/AnchorPlugin/utils/URLUtils.js +0 -36
  233. package/src/components/manage/AnchorPlugin/utils/mailRegex.js +0 -2
  234. package/src/components/manage/AnchorPlugin/utils/telRegex.js +0 -1
  235. package/src/components/manage/AnchorPlugin/utils/urlRegex.js +0 -54
  236. package/styleguide.config.js +0 -42
@@ -0,0 +1,120 @@
1
+ import React, { useState } from 'react';
2
+ import { Header } from 'semantic-ui-react';
3
+ import { defineMessages, injectIntl } from 'react-intl';
4
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
5
+ import { compose } from 'redux';
6
+ import { Icon } from '@plone/volto/components';
7
+
8
+ import leftKey from '@plone/volto/icons/left-key.svg';
9
+ import rightKey from '@plone/volto/icons/right-key.svg';
10
+ import clearSVG from '@plone/volto/icons/clear.svg';
11
+
12
+ import 'react-dates/initialize';
13
+ import 'react-dates/lib/css/_datepicker.css';
14
+
15
+ const messages = defineMessages({
16
+ startDate: {
17
+ id: 'Start Date',
18
+ defaultMessage: 'Start Date',
19
+ },
20
+ endDate: {
21
+ id: 'End Date',
22
+ defaultMessage: 'End Date',
23
+ },
24
+ });
25
+
26
+ const PrevIcon = () => (
27
+ <div
28
+ className="prev-icon"
29
+ style={{
30
+ color: '#000',
31
+ left: '22px',
32
+ padding: '5px',
33
+ position: 'absolute',
34
+ top: '15px',
35
+ }}
36
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
37
+ tabIndex="0"
38
+ >
39
+ <Icon name={leftKey} size="30px" />
40
+ </div>
41
+ );
42
+ const NextIcon = () => (
43
+ <div
44
+ className="next-icon"
45
+ style={{
46
+ color: '#000',
47
+ right: '22px',
48
+ padding: '5px',
49
+ position: 'absolute',
50
+ top: '15px',
51
+ }}
52
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
53
+ tabIndex="0"
54
+ >
55
+ <Icon name={rightKey} size="30px" />
56
+ </div>
57
+ );
58
+
59
+ const CloseIcon = () => <Icon name={clearSVG} size="24px" className="close" />;
60
+
61
+ const DateRangeFacet = (props) => {
62
+ const { facet, isEditMode, onChange, value, reactDates, intl } = props;
63
+ const moment = props.moment.default;
64
+ const { DateRangePicker } = reactDates;
65
+ const [focused, setFocused] = useState(null);
66
+
67
+ return (
68
+ <div className="daterange-facet">
69
+ <Header as="h4">{facet?.title ?? facet?.field?.label}</Header>
70
+ <div className="ui form date-time-widget-wrapper">
71
+ <div className="ui input date-input">
72
+ <DateRangePicker
73
+ startDate={value && value[0] ? moment(value[0]) : null}
74
+ startDateId={`${facet['@id']}-start-date`}
75
+ startDatePlaceholderText={intl.formatMessage(messages.startDate)}
76
+ endDate={value && value[1] ? moment(value[1]) : null}
77
+ endDateId={`${facet['@id']}-end-date`}
78
+ endDatePlaceholderText={intl.formatMessage(messages.endDate)}
79
+ numberOfMonths={1}
80
+ disabled={isEditMode}
81
+ noBorder
82
+ showClearDates
83
+ customCloseIcon={<CloseIcon />}
84
+ displayFormat={moment.localeData(intl.locale).longDateFormat('L')}
85
+ focusedInput={focused}
86
+ onFocusChange={(focusedInput) => setFocused(focusedInput)}
87
+ onDatesChange={({ startDate, endDate }) => {
88
+ onChange(facet.field.value, [
89
+ startDate ? startDate.format('YYYY-MM-DD') : null,
90
+ endDate ? endDate.format('YYYY-MM-DD') : null,
91
+ ]);
92
+ }}
93
+ isOutsideRange={() => false}
94
+ navPrev={<PrevIcon />}
95
+ navNext={<NextIcon />}
96
+ />
97
+ </div>
98
+ </div>
99
+ </div>
100
+ );
101
+ };
102
+
103
+ DateRangeFacet.stateToValue = ({ facetSettings, index, selectedValue }) => {
104
+ return selectedValue || [null, null];
105
+ };
106
+
107
+ DateRangeFacet.valueToQuery = ({ value, facet }) => {
108
+ return value && typeof value[0] === 'string' && typeof value[1] === 'string'
109
+ ? {
110
+ i: facet.field.value,
111
+ o: 'plone.app.querystring.operation.date.between',
112
+ v: value,
113
+ }
114
+ : null;
115
+ };
116
+
117
+ export default compose(
118
+ injectLazyLibs(['reactDates', 'moment']),
119
+ injectIntl,
120
+ )(DateRangeFacet);
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import configureStore from 'redux-mock-store';
3
+ import { Provider } from 'react-intl-redux';
4
+ import { waitFor, render, screen } from '@testing-library/react';
5
+
6
+ import DateRangeFacet from './DateRangeFacet';
7
+
8
+ const mockStore = configureStore();
9
+
10
+ jest.mock('@plone/volto/helpers/Loadable/Loadable');
11
+ beforeAll(
12
+ async () =>
13
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
14
+ );
15
+
16
+ describe('DateRangeFaceg', () => {
17
+ it('renders a facet component with a date range widget', async () => {
18
+ const store = mockStore({
19
+ userSession: { token: null },
20
+ content: { data: { '@id': 'myid' } },
21
+ intl: {
22
+ locale: 'en',
23
+ messages: {},
24
+ },
25
+ });
26
+ const { container } = render(
27
+ <Provider store={store}>
28
+ <DateRangeFacet facet={{ title: 'Test Facet', field: {} }} />
29
+ </Provider>,
30
+ );
31
+ await waitFor(() => screen.getByText('Test Facet'));
32
+ expect(container).toMatchSnapshot();
33
+ });
34
+ });
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Label, Icon } from 'semantic-ui-react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+
5
+ const messages = defineMessages({
6
+ yes: {
7
+ id: 'yes',
8
+ defaultMessage: 'Yes',
9
+ },
10
+ no: {
11
+ id: 'no',
12
+ defaultMessage: 'No',
13
+ },
14
+ });
15
+
16
+ function DateRangeFacetFilterListEntry(props) {
17
+ const { facet, isEditMode, setFacets, facets } = props;
18
+ const intl = useIntl();
19
+
20
+ return (
21
+ <Label size="small">
22
+ {intl.formatMessage(facets[facet] ? messages.yes : messages.no)}
23
+ <Icon
24
+ name="delete"
25
+ onClick={() => {
26
+ const filteredFacets = Object.assign(
27
+ {},
28
+ ...Object.keys(facets)
29
+ .filter((f) => f !== facet)
30
+ .map((f) => ({ [f]: facets[f] })),
31
+ );
32
+ !isEditMode && setFacets(filteredFacets);
33
+ }}
34
+ />
35
+ </Label>
36
+ );
37
+ }
38
+
39
+ export default DateRangeFacetFilterListEntry;
@@ -1,6 +1,17 @@
1
1
  import React from 'react';
2
2
  import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
3
3
  import config from '@plone/volto/registry';
4
+ import { hasNonValueOperation, hasDateOperation } from '../utils';
5
+
6
+ const showFacet = (index) => {
7
+ const { values } = index;
8
+ return index
9
+ ? hasNonValueOperation(index.operations || []) ||
10
+ hasDateOperation(index.operations || [])
11
+ ? true
12
+ : values && Object.keys(values).length > 0
13
+ : values && Object.keys(values).length > 0;
14
+ };
4
15
 
5
16
  const Facets = (props) => {
6
17
  const {
@@ -22,9 +33,9 @@ const Facets = (props) => {
22
33
  return (
23
34
  <>
24
35
  {data?.facets
25
- ?.filter((facet) => !facet.hidden)
26
- .map((facet) => {
27
- const field = facet?.field?.value;
36
+ ?.filter((facetSettings) => !facetSettings.hidden)
37
+ .map((facetSettings) => {
38
+ const field = facetSettings?.field?.value;
28
39
  const index = querystring.indexes[field] || {};
29
40
  const { values = {} } = index;
30
41
 
@@ -41,40 +52,28 @@ const Facets = (props) => {
41
52
  : true,
42
53
  );
43
54
 
44
- const isMulti = facet.multiple;
45
- const selectedValue = facets[facet?.field?.value];
55
+ const isMulti = facetSettings.multiple;
56
+ const selectedValue = facets[facetSettings?.field?.value];
46
57
 
47
58
  // TODO :handle changing the type of facet (multi/nonmulti)
48
59
 
49
- let value = selectedValue
50
- ? isMulti
51
- ? Array.isArray(selectedValue)
52
- ? selectedValue.map((v) => ({
53
- value: v,
54
- label: index.values?.[v]?.title,
55
- }))
56
- : []
57
- : {
58
- value: selectedValue,
59
- label: index.values?.[selectedValue]?.title,
60
- }
61
- : [];
62
-
63
- const { view: FacetWidget } = resolveExtension(
60
+ const { view: FacetWidget, stateToValue } = resolveExtension(
64
61
  'type',
65
62
  search.extensions.facetWidgets.types,
66
- facet,
63
+ facetSettings,
67
64
  );
68
65
 
66
+ let value = stateToValue({ facetSettings, index, selectedValue });
67
+
69
68
  const {
70
69
  rewriteOptions = (name, options) => options,
71
70
  } = search.extensions.facetWidgets;
72
71
 
73
- return FacetWrapper && Object.keys(values).length ? (
74
- <FacetWrapper key={facet['@id']}>
72
+ return FacetWrapper && (isEditMode || showFacet(index)) ? (
73
+ <FacetWrapper key={facetSettings['@id']}>
75
74
  <FacetWidget
76
- facet={facet}
77
- choices={rewriteOptions(facet?.field?.value, choices)}
75
+ facet={facetSettings}
76
+ choices={rewriteOptions(facetSettings?.field?.value, choices)}
78
77
  isMulti={isMulti}
79
78
  value={value}
80
79
  isEditMode={isEditMode}
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
- import { Accordion, Button, Label, Icon } from 'semantic-ui-react';
2
+ import { Accordion, Button, Icon } from 'semantic-ui-react';
3
3
  import { Icon as VoltoIcon } from '@plone/volto/components';
4
4
  import downSVG from '@plone/volto/icons/down-key.svg';
5
5
  import { defineMessages, useIntl } from 'react-intl';
6
+ import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
7
+ import config from '@plone/volto/registry';
6
8
 
7
9
  const messages = defineMessages({
8
10
  currentFilters: {
@@ -15,26 +17,27 @@ const messages = defineMessages({
15
17
  },
16
18
  });
17
19
 
20
+ /**
21
+ * A list of active filtered values and controls to clear those filters.
22
+ *
23
+ */
18
24
  const FilterList = (props) => {
19
- const { data, facets, setFacets, isEditMode } = props;
25
+ const { data = {}, facets = {}, setFacets, isEditMode } = props;
26
+ const definedFacets = data.facets || [];
20
27
  const [isOpened, setIsOpened] = React.useState(false);
21
28
 
22
- // TODO: the facets are merged with the default query. We need to untangle
23
- // and claim only the original facet values
24
- const showFilterList = !Object.values(facets).every((facet) => !facet.length);
29
+ const totalFilters = definedFacets.filter(
30
+ ({ field }) =>
31
+ field && Object.keys(facets).includes(field.value) && facets[field.value],
32
+ ).length;
25
33
 
26
- const currentFilters = Object.fromEntries(
27
- Object.entries(facets).filter((v) => v[1] && v[0] !== 'SearchableText'),
28
- );
29
-
30
- const totalFilters = [].concat.apply([], Object.values(currentFilters))
31
- .length;
32
-
33
- // console.log({ data, facets, showFilterList, currentFilters, totalFilters });
34
+ const {
35
+ types: facetWidgetTypes,
36
+ } = config.blocks.blocksConfig.search.extensions.facetWidgets;
34
37
 
35
38
  const intl = useIntl();
36
39
 
37
- return showFilterList && Object.keys(currentFilters).length ? (
40
+ return totalFilters ? (
38
41
  <Accordion className="filter-listing">
39
42
  <Accordion.Title
40
43
  className="filter-list-header"
@@ -61,59 +64,26 @@ const FilterList = (props) => {
61
64
  </Accordion.Title>
62
65
  <Accordion.Content className="filter-list-content" active={isOpened}>
63
66
  <div className="filter-list">
64
- {Object.keys(facets || {}).map((facet, i) => (
65
- <div key={i}>
66
- {facets[facet].length > 0 && (
67
- <div className="filter-list-group" key={i}>
68
- {data?.facets?.map((f, i) => {
69
- return facet === f?.field?.value ? (
70
- <span className="label-title" key={i}>
71
- {f.title || f?.field?.label}:
72
- </span>
73
- ) : (
74
- ''
75
- );
76
- })}
77
- {typeof facets[facet] === 'string' ? (
78
- <Label size="small" key={i}>
79
- {facets[facet]}
80
- <Icon
81
- name="delete"
82
- onClick={() => {
83
- !isEditMode &&
84
- setFacets({
85
- ...facets,
86
- [facet]: '',
87
- });
88
- }}
89
- />
90
- </Label>
91
- ) : (
92
- <>
93
- {facets[facet].map((entry, i) => (
94
- <Label size="small" key={i}>
95
- {entry}
96
- <Icon
97
- name="delete"
98
- onClick={() => {
99
- const entries = facets[facet].filter(
100
- (item) => item !== entry,
101
- );
102
- !isEditMode &&
103
- setFacets({
104
- ...facets,
105
- [facet]: entries,
106
- });
107
- }}
108
- />
109
- </Label>
110
- ))}
111
- </>
112
- )}
113
- </div>
114
- )}
115
- </div>
116
- ))}
67
+ {data.facets?.map((facetSettings, i) => {
68
+ const {
69
+ filterListComponent: FilterListComponent,
70
+ } = resolveExtension('type', facetWidgetTypes, facetSettings);
71
+ const facet = facetSettings?.field?.value;
72
+ if (!facet) return null;
73
+
74
+ return (
75
+ <div key={i}>
76
+ {Object.keys(facets).includes(facet) && !!facets[facet] && (
77
+ <div className="filter-list-group" key={i}>
78
+ <span className="label-title">
79
+ {facetSettings.title ?? facetSettings?.field?.label}
80
+ </span>
81
+ <FilterListComponent {...props} facet={facet} />
82
+ </div>
83
+ )}
84
+ </div>
85
+ );
86
+ })}
117
87
  </div>
118
88
  </Accordion.Content>
119
89
  </Accordion>
@@ -2,11 +2,45 @@ import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
3
  import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
+ import config from '@plone/volto/registry';
5
6
 
6
7
  import FilterList from './FilterList';
8
+ import SelectFacet from './SelectFacet';
9
+ import SelectFacetFilterListEntry from './SelectFacetFilterListEntry';
7
10
 
8
11
  const mockStore = configureStore();
9
12
 
13
+ config.blocks.blocksConfig = {
14
+ search: {
15
+ id: 'search',
16
+ title: 'Search',
17
+ restricted: false,
18
+ mostUsed: false,
19
+ sidebarTab: 1,
20
+ security: {
21
+ addPermission: [],
22
+ view: [],
23
+ },
24
+ variations: [],
25
+ extensions: {
26
+ facetWidgets: {
27
+ types: [
28
+ {
29
+ id: 'selectFacet',
30
+ title: 'Select',
31
+ view: SelectFacet,
32
+ isDefault: true,
33
+ schemaEnhancer: SelectFacet.schemaEnhancer,
34
+ stateToValue: SelectFacet.stateToValue,
35
+ valueToQuery: SelectFacet.valueToQuery,
36
+ filterListComponent: SelectFacetFilterListEntry,
37
+ },
38
+ ],
39
+ },
40
+ },
41
+ },
42
+ };
43
+
10
44
  describe('FilterList', () => {
11
45
  it('renders filters listing component for search block', () => {
12
46
  const store = mockStore({
@@ -17,9 +51,23 @@ describe('FilterList', () => {
17
51
  messages: {},
18
52
  },
19
53
  });
54
+ const data = {
55
+ facets: [
56
+ {
57
+ '@id': 'a',
58
+ field: {
59
+ value: 'portal_type',
60
+ label: 'Portal type',
61
+ },
62
+ },
63
+ ],
64
+ };
20
65
  const component = renderer.create(
21
66
  <Provider store={store}>
22
- <FilterList facets={{ portal_type: ['Document', 'Folder'] }} />
67
+ <FilterList
68
+ data={data}
69
+ facets={{ portal_type: ['Document', 'Folder'] }}
70
+ />
23
71
  </Provider>,
24
72
  );
25
73
  const json = component.toJSON();
@@ -5,6 +5,11 @@ import {
5
5
  DropdownIndicator,
6
6
  } from '@plone/volto/components/manage/Widgets/SelectStyling';
7
7
  import { selectTheme, customSelectStyles } from './SelectStyling';
8
+ import {
9
+ selectFacetSchemaEnhancer,
10
+ selectFacetStateToValue,
11
+ selectFacetValueToQuery,
12
+ } from './base';
8
13
 
9
14
  const SelectFacet = (props) => {
10
15
  const {
@@ -47,4 +52,8 @@ const SelectFacet = (props) => {
47
52
  );
48
53
  };
49
54
 
55
+ SelectFacet.schemaEnhancer = selectFacetSchemaEnhancer;
56
+ SelectFacet.stateToValue = selectFacetStateToValue;
57
+ SelectFacet.valueToQuery = selectFacetValueToQuery;
58
+
50
59
  export default injectLazyLibs('reactSelect')(SelectFacet);
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { Label, Icon } from 'semantic-ui-react';
3
+
4
+ function SelectFacetFilterListEntry(props) {
5
+ const { facet, isEditMode, setFacets, facets } = props;
6
+ return typeof facets[facet] === 'string' ? (
7
+ <Label size="small">
8
+ {facets[facet]}
9
+ <Icon
10
+ name="delete"
11
+ onClick={() => {
12
+ !isEditMode &&
13
+ setFacets({
14
+ ...facets,
15
+ [facet]: '',
16
+ });
17
+ }}
18
+ />
19
+ </Label>
20
+ ) : (
21
+ <>
22
+ {facets[facet].map((entry, i) => (
23
+ <Label size="small" key={i}>
24
+ {entry}
25
+ <Icon
26
+ name="delete"
27
+ onClick={() => {
28
+ const entries = facets[facet].filter((item) => item !== entry);
29
+ !isEditMode &&
30
+ setFacets({
31
+ ...facets,
32
+ [facet]: entries,
33
+ });
34
+ }}
35
+ />
36
+ </Label>
37
+ ))}
38
+ </>
39
+ );
40
+ }
41
+
42
+ export default SelectFacetFilterListEntry;
@@ -15,7 +15,7 @@ export const customSelectStyles = {
15
15
  borderBottom: '1px solid #c7d5d8',
16
16
  boxShadow: 'none',
17
17
  borderBottomStyle: state.menuIsOpen ? 'dotted' : 'solid',
18
- minHeight: '40px',
18
+ minHeight: '37px',
19
19
  }),
20
20
  menu: (styles, state) => ({
21
21
  ...styles,
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { Radio, Header } from 'semantic-ui-react';
3
+
4
+ const ToggleFacet = (props) => {
5
+ const { facet, isEditMode, onChange, value } = props; // value, choices, isMulti, onChange,
6
+
7
+ return (
8
+ <div className="checkbox-facet">
9
+ <Header as="h4">{facet?.title ?? facet?.field?.label}</Header>
10
+ <div className="radio">
11
+ <Radio
12
+ toggle
13
+ checked={value || typeof value === 'string'}
14
+ disabled={isEditMode}
15
+ onChange={(e, { checked }) => {
16
+ onChange(facet.field.value, checked);
17
+ }}
18
+ />
19
+ </div>
20
+ </div>
21
+ );
22
+ };
23
+
24
+ ToggleFacet.stateToValue = ({ facetSettings, index, selectedValue }) => {
25
+ return selectedValue || typeof selectedValue === 'string';
26
+ };
27
+
28
+ ToggleFacet.valueToQuery = ({ value, facet }) => {
29
+ return value || typeof value === 'string'
30
+ ? {
31
+ i: facet.field.value,
32
+ o: 'plone.app.querystring.operation.boolean.isTrue',
33
+ v: '',
34
+ }
35
+ : null;
36
+ };
37
+
38
+ export default ToggleFacet;
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Label, Icon } from 'semantic-ui-react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+
5
+ const messages = defineMessages({
6
+ yes: {
7
+ id: 'yes',
8
+ defaultMessage: 'Yes',
9
+ },
10
+ no: {
11
+ id: 'no',
12
+ defaultMessage: 'No',
13
+ },
14
+ });
15
+
16
+ function ToggleFacetFilterListEntry(props) {
17
+ const { facet, isEditMode, setFacets, facets } = props;
18
+ const intl = useIntl();
19
+
20
+ return (
21
+ <Label size="small">
22
+ {intl.formatMessage(facets[facet] ? messages.yes : messages.no)}
23
+ <Icon
24
+ name="delete"
25
+ onClick={() => {
26
+ const filteredFacets = Object.assign(
27
+ {},
28
+ ...Object.keys(facets)
29
+ .filter((f) => f !== facet)
30
+ .map((f) => ({ [f]: facets[f] })),
31
+ );
32
+ !isEditMode && setFacets(filteredFacets);
33
+ }}
34
+ />
35
+ </Label>
36
+ );
37
+ }
38
+
39
+ export default ToggleFacetFilterListEntry;
@@ -0,0 +1,52 @@
1
+ import { isEmpty } from 'lodash';
2
+
3
+ /**
4
+ * Converts the state (extracted from URL) to options for the facet control
5
+ *
6
+ * Shared by the SelectFacet and CheckboxFacet
7
+ */
8
+ export const selectFacetStateToValue = ({
9
+ facetSettings,
10
+ index,
11
+ selectedValue,
12
+ }) => {
13
+ const isMulti = facetSettings.multiple;
14
+ return selectedValue
15
+ ? isMulti
16
+ ? Array.isArray(selectedValue)
17
+ ? selectedValue.map((v) => ({
18
+ value: v,
19
+ label: index.values?.[v]?.title,
20
+ }))
21
+ : []
22
+ : {
23
+ value: selectedValue,
24
+ label: index.values?.[selectedValue]?.title,
25
+ }
26
+ : [];
27
+ };
28
+
29
+ export const selectFacetSchemaEnhancer = ({ schema, formData }) => {
30
+ // adds (enables) the 'multiple' field after the 'type' dropdown
31
+ let { fields } = schema.fieldsets[0];
32
+ const pos = fields.indexOf('type') + 1;
33
+ fields = [
34
+ ...fields.slice(0, pos),
35
+ 'multiple',
36
+ ...fields.slice(pos, fields.length),
37
+ ];
38
+ schema.fieldsets[0].fields = fields;
39
+ return schema;
40
+ };
41
+
42
+ export const selectFacetValueToQuery = ({ value, facet }) => {
43
+ return !isEmpty(value)
44
+ ? {
45
+ i: facet.field.value,
46
+ o: Array.isArray(value)
47
+ ? 'plone.app.querystring.operation.list.contains'
48
+ : 'plone.app.querystring.operation.selection.is',
49
+ v: value,
50
+ }
51
+ : undefined;
52
+ };