@plone/volto 15.0.0-alpha.5 → 15.0.0-alpha.6

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 (194) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +7 -6
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -1
  3. package/.github/workflows/docs.yml +15 -55
  4. package/CHANGELOG.md +32 -2
  5. package/CODE_OF_CONDUCT.md +0 -1
  6. package/COMMITLINT.md +19 -21
  7. package/README.md +1 -1
  8. package/SECURITY.md +0 -1
  9. package/dist/assets.json +1 -0
  10. package/dist/chunks.json +572 -0
  11. package/dist/loadable-stats.json +2099 -0
  12. package/dist/public/android-chrome-192x192.png +0 -0
  13. package/dist/public/android-chrome-512x512.png +0 -0
  14. package/dist/public/apple-touch-icon.png +0 -0
  15. package/dist/public/favicon-16x16.png +0 -0
  16. package/dist/public/favicon-32x32.png +0 -0
  17. package/dist/public/favicon.ico +0 -0
  18. package/dist/public/icon.svg +13 -0
  19. package/dist/public/index.html.spa +34 -0
  20. package/dist/public/robots.txt +2 -0
  21. package/dist/public/site.webmanifest +19 -0
  22. package/dist/public/static/css/15.2cdd5437.chunk.css +409 -0
  23. package/dist/public/static/css/15.2cdd5437.chunk.css.map +1 -0
  24. package/dist/public/static/css/8.37cef611.chunk.css +2 -0
  25. package/dist/public/static/css/8.37cef611.chunk.css.map +1 -0
  26. package/dist/public/static/css/client.76ea022d.chunk.css +8 -0
  27. package/dist/public/static/css/client.76ea022d.chunk.css.map +1 -0
  28. package/dist/public/static/css/plone-volto-components-manage-Widgets-DatetimeWidget.c847a331.chunk.css +2 -0
  29. package/dist/public/static/css/plone-volto-components-manage-Widgets-DatetimeWidget.c847a331.chunk.css.map +1 -0
  30. package/dist/public/static/js/0.3e794c57.chunk.js +2 -0
  31. package/dist/public/static/js/0.3e794c57.chunk.js.map +1 -0
  32. package/dist/public/static/js/1.fece2b93.chunk.js +3 -0
  33. package/dist/public/static/js/1.fece2b93.chunk.js.LICENSE.txt +8 -0
  34. package/dist/public/static/js/1.fece2b93.chunk.js.map +1 -0
  35. package/dist/public/static/js/15.2cdd5437.chunk.js +3 -0
  36. package/dist/public/static/js/15.2cdd5437.chunk.js.LICENSE.txt +120 -0
  37. package/dist/public/static/js/15.2cdd5437.chunk.js.map +1 -0
  38. package/dist/public/static/js/16.a3a0662d.chunk.js +3 -0
  39. package/dist/public/static/js/16.a3a0662d.chunk.js.LICENSE.txt +8 -0
  40. package/dist/public/static/js/16.a3a0662d.chunk.js.map +1 -0
  41. package/dist/public/static/js/17.9478c50d.chunk.js +2 -0
  42. package/dist/public/static/js/17.9478c50d.chunk.js.map +1 -0
  43. package/dist/public/static/js/18.388e65d0.chunk.js +2 -0
  44. package/dist/public/static/js/18.388e65d0.chunk.js.map +1 -0
  45. package/dist/public/static/js/19.7c6b4a80.chunk.js +2 -0
  46. package/dist/public/static/js/19.7c6b4a80.chunk.js.map +1 -0
  47. package/dist/public/static/js/2.e6b0d690.chunk.js +2 -0
  48. package/dist/public/static/js/2.e6b0d690.chunk.js.map +1 -0
  49. package/dist/public/static/js/20.ada67f0e.chunk.js +3 -0
  50. package/dist/public/static/js/20.ada67f0e.chunk.js.LICENSE.txt +8 -0
  51. package/dist/public/static/js/20.ada67f0e.chunk.js.map +1 -0
  52. package/dist/public/static/js/21.3de49ac8.chunk.js +2 -0
  53. package/dist/public/static/js/21.3de49ac8.chunk.js.map +1 -0
  54. package/dist/public/static/js/22.ecefb8f2.chunk.js +2 -0
  55. package/dist/public/static/js/22.ecefb8f2.chunk.js.map +1 -0
  56. package/dist/public/static/js/23.3027bc00.chunk.js +2 -0
  57. package/dist/public/static/js/23.3027bc00.chunk.js.map +1 -0
  58. package/dist/public/static/js/24.aaaf4abf.chunk.js +2 -0
  59. package/dist/public/static/js/24.aaaf4abf.chunk.js.map +1 -0
  60. package/dist/public/static/js/25.c4d7d5e0.chunk.js +2 -0
  61. package/dist/public/static/js/25.c4d7d5e0.chunk.js.map +1 -0
  62. package/dist/public/static/js/26.d59ac3c1.chunk.js +2 -0
  63. package/dist/public/static/js/26.d59ac3c1.chunk.js.map +1 -0
  64. package/dist/public/static/js/27.266d95dd.chunk.js +2 -0
  65. package/dist/public/static/js/27.266d95dd.chunk.js.map +1 -0
  66. package/dist/public/static/js/28.d965d304.chunk.js +2 -0
  67. package/dist/public/static/js/28.d965d304.chunk.js.map +1 -0
  68. package/dist/public/static/js/29.7d8d1992.chunk.js +2 -0
  69. package/dist/public/static/js/29.7d8d1992.chunk.js.map +1 -0
  70. package/dist/public/static/js/3.025ba843.chunk.js +2 -0
  71. package/dist/public/static/js/3.025ba843.chunk.js.map +1 -0
  72. package/dist/public/static/js/30.a3d54e31.chunk.js +2 -0
  73. package/dist/public/static/js/30.a3d54e31.chunk.js.map +1 -0
  74. package/dist/public/static/js/31.9edf64ee.chunk.js +2 -0
  75. package/dist/public/static/js/31.9edf64ee.chunk.js.map +1 -0
  76. package/dist/public/static/js/32.649dbfc2.chunk.js +2 -0
  77. package/dist/public/static/js/32.649dbfc2.chunk.js.map +1 -0
  78. package/dist/public/static/js/33.45949ca4.chunk.js +2 -0
  79. package/dist/public/static/js/33.45949ca4.chunk.js.map +1 -0
  80. package/dist/public/static/js/34.084257cf.chunk.js +2 -0
  81. package/dist/public/static/js/34.084257cf.chunk.js.map +1 -0
  82. package/dist/public/static/js/35.00679602.chunk.js +3 -0
  83. package/dist/public/static/js/35.00679602.chunk.js.LICENSE.txt +37 -0
  84. package/dist/public/static/js/35.00679602.chunk.js.map +1 -0
  85. package/dist/public/static/js/36.4a29fdbd.chunk.js +3 -0
  86. package/dist/public/static/js/36.4a29fdbd.chunk.js.LICENSE.txt +7 -0
  87. package/dist/public/static/js/36.4a29fdbd.chunk.js.map +1 -0
  88. package/dist/public/static/js/37.fbbe07b3.chunk.js +3 -0
  89. package/dist/public/static/js/37.fbbe07b3.chunk.js.LICENSE.txt +14 -0
  90. package/dist/public/static/js/37.fbbe07b3.chunk.js.map +1 -0
  91. package/dist/public/static/js/38.b31c59ad.chunk.js +3 -0
  92. package/dist/public/static/js/38.b31c59ad.chunk.js.LICENSE.txt +8 -0
  93. package/dist/public/static/js/38.b31c59ad.chunk.js.map +1 -0
  94. package/dist/public/static/js/39.69d6a789.chunk.js +2 -0
  95. package/dist/public/static/js/39.69d6a789.chunk.js.map +1 -0
  96. package/dist/public/static/js/4.bdc4a5c5.chunk.js +2 -0
  97. package/dist/public/static/js/4.bdc4a5c5.chunk.js.map +1 -0
  98. package/dist/public/static/js/40.90de6e4a.chunk.js +2 -0
  99. package/dist/public/static/js/40.90de6e4a.chunk.js.map +1 -0
  100. package/dist/public/static/js/7.7acc66bc.chunk.js +2 -0
  101. package/dist/public/static/js/7.7acc66bc.chunk.js.map +1 -0
  102. package/dist/public/static/js/8.37cef611.chunk.js +3 -0
  103. package/dist/public/static/js/8.37cef611.chunk.js.LICENSE.txt +25 -0
  104. package/dist/public/static/js/8.37cef611.chunk.js.map +1 -0
  105. package/dist/public/static/js/client.76ea022d.chunk.js +2 -0
  106. package/dist/public/static/js/client.76ea022d.chunk.js.map +1 -0
  107. package/dist/public/static/js/draft-js-lib-isSoftNewlineEvent.6cd05e34.chunk.js +2 -0
  108. package/dist/public/static/js/draft-js-lib-isSoftNewlineEvent.6cd05e34.chunk.js.map +1 -0
  109. package/dist/public/static/js/moment.2caa76d1.chunk.js +2 -0
  110. package/dist/public/static/js/moment.2caa76d1.chunk.js.map +1 -0
  111. package/dist/public/static/js/plone-volto-components-manage-Widgets-DatetimeWidget.c847a331.chunk.js +2 -0
  112. package/dist/public/static/js/plone-volto-components-manage-Widgets-DatetimeWidget.c847a331.chunk.js.map +1 -0
  113. package/dist/public/static/js/plone-volto-components-theme-View-EventView.433dd14b.chunk.js +2 -0
  114. package/dist/public/static/js/plone-volto-components-theme-View-EventView.433dd14b.chunk.js.map +1 -0
  115. package/dist/public/static/js/react-dates.f71abb7c.chunk.js +2 -0
  116. package/dist/public/static/js/react-dates.f71abb7c.chunk.js.map +1 -0
  117. package/dist/public/static/js/react-simple-code-editor.33478dc3.chunk.js +2 -0
  118. package/dist/public/static/js/react-simple-code-editor.33478dc3.chunk.js.map +1 -0
  119. package/dist/public/static/js/runtime~client.d372df0e.js +2 -0
  120. package/dist/public/static/js/runtime~client.d372df0e.js.map +1 -0
  121. package/dist/public/static/media/Logo.16e25cdf.svg +21 -0
  122. package/dist/public/static/media/block-image.8a4a14d5.svg +8 -0
  123. package/dist/public/static/media/block-maps.731e84f6.svg +7 -0
  124. package/dist/public/static/media/block-video.832fa3ff.svg +7 -0
  125. package/dist/public/static/media/default-image.9451b251.svg +13 -0
  126. package/dist/public/static/media/flags.9c74e172.png +0 -0
  127. package/dist/public/static/media/icons.022583c4.svg +320 -0
  128. package/dist/public/static/media/icons.06400145.ttf +0 -0
  129. package/dist/public/static/media/icons.dbc1ef8b.eot +0 -0
  130. package/dist/public/static/media/icons.df45b7de.woff +0 -0
  131. package/dist/server.js +2 -0
  132. package/dist/server.js.map +1 -0
  133. package/locales/ca/LC_MESSAGES/volto.po +14 -0
  134. package/locales/ca.json +1 -1
  135. package/locales/de/LC_MESSAGES/volto.po +14 -0
  136. package/locales/de.json +1 -1
  137. package/locales/en/LC_MESSAGES/volto.po +14 -0
  138. package/locales/en.json +1 -1
  139. package/locales/es/LC_MESSAGES/volto.po +14 -0
  140. package/locales/es.json +1 -1
  141. package/locales/eu/LC_MESSAGES/volto.po +14 -0
  142. package/locales/eu.json +1 -1
  143. package/locales/fr/LC_MESSAGES/volto.po +14 -0
  144. package/locales/fr.json +1 -1
  145. package/locales/it/LC_MESSAGES/volto.po +14 -0
  146. package/locales/it.json +1 -1
  147. package/locales/ja/LC_MESSAGES/volto.po +14 -0
  148. package/locales/ja.json +1 -1
  149. package/locales/nl/LC_MESSAGES/volto.po +14 -0
  150. package/locales/nl.json +1 -1
  151. package/locales/pt/LC_MESSAGES/volto.po +14 -0
  152. package/locales/pt.json +1 -1
  153. package/locales/pt_BR/LC_MESSAGES/volto.po +14 -0
  154. package/locales/pt_BR.json +1 -1
  155. package/locales/ro/LC_MESSAGES/volto.po +14 -0
  156. package/locales/ro.json +1 -1
  157. package/locales/volto.pot +15 -1
  158. package/logos/Logo.png +0 -0
  159. package/logos/volto-colorful.png +0 -0
  160. package/logos/volto-colorful.svg +35 -0
  161. package/logos/volto-guide.png +0 -0
  162. package/logos/volto-h-transparent.svg +6 -0
  163. package/logos/volto-transparent.png +0 -0
  164. package/logos/volto-transparent.svg +19 -0
  165. package/logos/volto-transparent2.svg +5 -0
  166. package/logos/volto-yellow.svg +29 -0
  167. package/logos/volto.sketch +0 -0
  168. package/netlify.toml +2 -0
  169. package/package.json +1 -1
  170. package/src/components/manage/Blocks/Search/README.md +20 -0
  171. package/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx +16 -3
  172. package/src/components/manage/Blocks/Search/components/DateRangeFacet.jsx +120 -0
  173. package/src/components/manage/Blocks/Search/components/DateRangeFacet.test.jsx +34 -0
  174. package/src/components/manage/Blocks/Search/components/DateRangeFacetFilterListEntry.jsx +39 -0
  175. package/src/components/manage/Blocks/Search/components/Facets.jsx +24 -25
  176. package/src/components/manage/Blocks/Search/components/FilterList.jsx +37 -67
  177. package/src/components/manage/Blocks/Search/components/FilterList.test.jsx +49 -1
  178. package/src/components/manage/Blocks/Search/components/SelectFacet.jsx +9 -0
  179. package/src/components/manage/Blocks/Search/components/SelectFacetFilterListEntry.jsx +42 -0
  180. package/src/components/manage/Blocks/Search/components/SelectStyling.jsx +1 -1
  181. package/src/components/manage/Blocks/Search/components/ToggleFacet.jsx +38 -0
  182. package/src/components/manage/Blocks/Search/components/ToggleFacetFilterListEntry.jsx +39 -0
  183. package/src/components/manage/Blocks/Search/components/base.js +52 -0
  184. package/src/components/manage/Blocks/Search/components/index.js +5 -0
  185. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +68 -27
  186. package/src/components/manage/Blocks/Search/schema.js +7 -4
  187. package/src/components/manage/Blocks/Search/utils.js +16 -0
  188. package/src/components/manage/Widgets/DatetimeWidget.jsx +3 -1
  189. package/src/config/Blocks.jsx +31 -0
  190. package/src/helpers/Extensions/index.js +1 -1
  191. package/src/helpers/Extensions/withBlockExtensions.js +4 -0
  192. package/src/helpers/Extensions/withBlockExtensions.test.js +26 -1
  193. package/src/helpers/index.js +1 -0
  194. package/theme/themes/pastanaga/extras/blocks.less +29 -0
@@ -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
+ };
@@ -2,6 +2,11 @@ export SearchDetails from './SearchDetails';
2
2
  export Facets from './Facets';
3
3
  export SelectFacet from './SelectFacet';
4
4
  export CheckboxFacet from './CheckboxFacet';
5
+ export DateRangeFacet from './DateRangeFacet';
5
6
  export SearchInput from './SearchInput';
6
7
  export FilterList from './FilterList';
7
8
  export SortOn from './SortOn';
9
+ export ToggleFacet from './ToggleFacet';
10
+ export SelectFacetFilterListEntry from './SelectFacetFilterListEntry';
11
+ export ToggleFacetFilterListEntry from './ToggleFacetFilterListEntry';
12
+ export DateRangeFacetFilterListEntry from './DateRangeFacetFilterListEntry';
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { useSelector } from 'react-redux';
3
- import { isEmpty } from 'lodash';
4
3
  import qs from 'query-string';
5
4
  import { useLocation, useHistory } from 'react-router-dom';
6
5
 
6
+ import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
7
+ import config from '@plone/volto/registry';
8
+
7
9
  function getDisplayName(WrappedComponent) {
8
10
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
9
11
  }
@@ -25,16 +27,30 @@ const PAQO = 'plone.app.querystring.operation';
25
27
  *
26
28
  */
27
29
  function getInitialState(data, facets, urlSearchText, id) {
30
+ const {
31
+ types: facetWidgetTypes,
32
+ } = config.blocks.blocksConfig.search.extensions.facetWidgets;
33
+ const facetSettings = data?.facets || [];
34
+
28
35
  return {
29
36
  query: [
30
37
  ...(data.query?.query || []),
31
- ...Object.keys(facets).map((name) => ({
32
- i: name,
33
- v: facets[name],
34
-
35
- // TODO: make the facet operator pluggable
36
- o: 'plone.app.querystring.operation.selection.is',
37
- })),
38
+ ...(facetSettings || [])
39
+ .map((facet) => {
40
+ if (!facet?.field) return null;
41
+
42
+ const { valueToQuery } = resolveExtension(
43
+ 'type',
44
+ facetWidgetTypes,
45
+ facet,
46
+ );
47
+
48
+ const name = facet.field.value;
49
+ const value = facets[name];
50
+
51
+ return valueToQuery({ value, facet });
52
+ })
53
+ .filter((f) => !!f),
38
54
  ...(urlSearchText
39
55
  ? [
40
56
  {
@@ -54,27 +70,42 @@ function getInitialState(data, facets, urlSearchText, id) {
54
70
  }
55
71
 
56
72
  /**
57
- * "Normalizes" the search state to something that's serializable and usable
58
- * as initial state
73
+ * "Normalizes" the search state to something that's serializable
74
+ * (for querying) and used to compute data for the ListingBody
59
75
  *
60
76
  * @function normalizeState
61
77
  *
62
78
  */
63
- function normalizeState({ query, facets, id, searchText, sortOn, sortOrder }) {
79
+ function normalizeState({
80
+ query, // base query
81
+ facets, // facet values
82
+ id, // block id
83
+ searchText, // SearchableText
84
+ sortOn,
85
+ sortOrder,
86
+ facetSettings, // data.facets extracted from block data
87
+ }) {
88
+ const {
89
+ types: facetWidgetTypes,
90
+ } = config.blocks.blocksConfig.search.extensions.facetWidgets;
91
+
64
92
  const params = {
65
93
  query: [
66
94
  ...(query.query || []),
67
- ...Object.keys(facets).map((name) =>
68
- !isEmpty(facets[name])
69
- ? {
70
- i: name,
71
- o: Array.isArray(facets[name])
72
- ? 'plone.app.querystring.operation.list.contains'
73
- : 'plone.app.querystring.operation.selection.is',
74
- v: facets[name],
75
- }
76
- : undefined,
77
- ),
95
+ ...(facetSettings || []).map((facet) => {
96
+ if (!facet?.field) return null;
97
+
98
+ const { valueToQuery } = resolveExtension(
99
+ 'type',
100
+ facetWidgetTypes,
101
+ facet,
102
+ );
103
+
104
+ const name = facet.field.value;
105
+ const value = facets[name];
106
+
107
+ return valueToQuery({ value, facet });
108
+ }),
78
109
  ].filter((o) => !!o),
79
110
  sort_on: sortOn || query.sort_on,
80
111
  sort_order: sortOrder || query.sort_order,
@@ -217,7 +248,7 @@ const withSearch = (options) => (WrappedComponent) => {
217
248
  const [facets, setFacets] = React.useState(
218
249
  Object.assign(
219
250
  {},
220
- ...urlQuery.map(({ i, v }) => ({ [i]: v })),
251
+ ...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
221
252
 
222
253
  // support for simple filters like ?Subject=something
223
254
  // TODO: since the move to hash params this is no longer working.
@@ -245,19 +276,21 @@ const withSearch = (options) => (WrappedComponent) => {
245
276
  );
246
277
 
247
278
  const timeoutRef = React.useRef();
279
+ const facetSettings = data?.facets;
248
280
 
249
281
  const onTriggerSearch = React.useCallback(
250
- (toSearch, toSearchFacets, toSortOn, toSortOrder) => {
282
+ (toSearchText, toSearchFacets, toSortOn, toSortOrder) => {
251
283
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
252
284
  timeoutRef.current = setTimeout(
253
285
  () => {
254
286
  const searchData = normalizeState({
287
+ id,
255
288
  query: data.query || {},
256
289
  facets: toSearchFacets || facets,
257
- id,
258
- searchText: toSearch,
290
+ searchText: toSearchText,
259
291
  sortOn: toSortOn || sortOn,
260
292
  sortOrder: toSortOrder || sortOrder,
293
+ facetSettings,
261
294
  });
262
295
  if (toSearchFacets) setFacets(toSearchFacets);
263
296
  if (toSortOn) setSortOn(toSortOn);
@@ -268,7 +301,15 @@ const withSearch = (options) => (WrappedComponent) => {
268
301
  toSearchFacets ? inputDelay / 3 : inputDelay,
269
302
  );
270
303
  },
271
- [data.query, facets, id, setLocationSearchData, sortOn, sortOrder],
304
+ [
305
+ data.query,
306
+ facets,
307
+ id,
308
+ setLocationSearchData,
309
+ sortOn,
310
+ sortOrder,
311
+ facetSettings,
312
+ ],
272
313
  );
273
314
 
274
315
  const querystringResults = useSelector(
@@ -1,6 +1,7 @@
1
1
  import config from '@plone/volto/registry';
2
2
  import { defineMessages } from 'react-intl';
3
3
  import { cloneDeep } from 'lodash';
4
+ import { hasNonValueOperation, hasDateOperation } from './utils';
4
5
 
5
6
  const messages = defineMessages({
6
7
  searchBlock: {
@@ -99,9 +100,9 @@ const messages = defineMessages({
99
100
 
100
101
  const enhanceSchema = (originalSchema, formData) => {
101
102
  const extensionName = 'facetWidgets';
102
- const extensionType = 'type';
103
+ const extensionType = 'type'; // property name in stored block data
103
104
  const variations =
104
- config.blocks.blocksConfig.search.extensions[extensionName][extensionType];
105
+ config.blocks.blocksConfig.search.extensions[extensionName].types;
105
106
 
106
107
  const activeItemName = formData?.[extensionType];
107
108
  let activeItem = variations?.find((item) => item.id === activeItemName);
@@ -122,7 +123,7 @@ const FacetSchema = ({ intl }) => ({
122
123
  {
123
124
  id: 'default',
124
125
  title: 'Default',
125
- fields: ['title', 'field', 'type', 'multiple', 'hidden'],
126
+ fields: ['title', 'field', 'type', 'hidden'],
126
127
  },
127
128
  ],
128
129
  properties: {
@@ -141,7 +142,9 @@ const FacetSchema = ({ intl }) => ({
141
142
  return Object.assign(
142
143
  {},
143
144
  ...Object.keys(options).map((k) =>
144
- Object.keys(options[k].values || {}).length
145
+ Object.keys(options[k].values || {}).length ||
146
+ hasNonValueOperation(options[k].operations) ||
147
+ hasDateOperation(options[k].operations)
145
148
  ? { [k]: options[k] }
146
149
  : {},
147
150
  ),
@@ -0,0 +1,16 @@
1
+ export const NONVALUE_OPERATIONS = new Set([
2
+ 'plone.app.querystring.operation.boolean.isFalse',
3
+ 'plone.app.querystring.operation.boolean.isTrue',
4
+ ]);
5
+
6
+ export const DATE_OPERATIONS = new Set([
7
+ 'plone.app.querystring.operation.date.between',
8
+ ]);
9
+
10
+ export const hasNonValueOperation = (ops) => {
11
+ return ops.filter((x) => NONVALUE_OPERATIONS.has(x)).length > 0;
12
+ };
13
+
14
+ export const hasDateOperation = (ops) => {
15
+ return ops.filter((x) => DATE_OPERATIONS.has(x)).length > 0;
16
+ };
@@ -178,7 +178,9 @@ export class DatetimeWidgetComponent extends Component {
178
178
  onFocusChange = ({ focused }) => this.setState({ focused });
179
179
 
180
180
  render() {
181
- const { id, noPastDates, resettable, intl, reactDates } = this.props;
181
+ const { id, resettable, intl, reactDates, widgetOptions } = this.props;
182
+ const noPastDates =
183
+ this.props.noPastDates || widgetOptions?.pattern_options?.noPastDates;
182
184
  const moment = this.props.moment.default;
183
185
  const datetime = this.getInternalValue();
184
186
  const dateOnly = this.getDateOnly();