@plone/volto 17.0.0-alpha.17 → 17.0.0-alpha.19

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 (74) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +57 -0
  3. package/cypress/support/commands.js +17 -0
  4. package/locales/ca/LC_MESSAGES/volto.po +39 -0
  5. package/locales/ca.json +1 -1
  6. package/locales/de/LC_MESSAGES/volto.po +39 -0
  7. package/locales/de.json +1 -1
  8. package/locales/en/LC_MESSAGES/volto.po +39 -0
  9. package/locales/en.json +1 -1
  10. package/locales/es/LC_MESSAGES/volto.po +39 -0
  11. package/locales/es.json +1 -1
  12. package/locales/eu/LC_MESSAGES/volto.po +39 -0
  13. package/locales/eu.json +1 -1
  14. package/locales/fi/LC_MESSAGES/volto.po +39 -0
  15. package/locales/fi.json +1 -1
  16. package/locales/fr/LC_MESSAGES/volto.po +39 -0
  17. package/locales/fr.json +1 -1
  18. package/locales/it/LC_MESSAGES/volto.po +40 -1
  19. package/locales/it.json +1 -1
  20. package/locales/ja/LC_MESSAGES/volto.po +39 -0
  21. package/locales/ja.json +1 -1
  22. package/locales/nl/LC_MESSAGES/volto.po +39 -0
  23. package/locales/nl.json +1 -1
  24. package/locales/pt/LC_MESSAGES/volto.po +39 -0
  25. package/locales/pt.json +1 -1
  26. package/locales/pt_BR/LC_MESSAGES/volto.po +39 -0
  27. package/locales/pt_BR.json +1 -1
  28. package/locales/ro/LC_MESSAGES/volto.po +39 -0
  29. package/locales/ro.json +1 -1
  30. package/locales/volto.pot +39 -0
  31. package/locales/zh_CN/LC_MESSAGES/volto.po +39 -0
  32. package/locales/zh_CN.json +1 -1
  33. package/package.json +2 -2
  34. package/packages/volto-slate/package.json +1 -1
  35. package/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +21 -212
  36. package/packages/volto-slate/src/blocks/Table/schema.js +122 -0
  37. package/packages/volto-slate/src/editor/plugins/StyleMenu/utils.js +14 -5
  38. package/packages/volto-slate/src/utils/blocks.js +7 -0
  39. package/packages/volto-slate/src/widgets/RichTextWidget.jsx +15 -8
  40. package/src/components/index.js +1 -0
  41. package/src/components/manage/Blocks/Search/components/Facets.jsx +6 -2
  42. package/src/components/manage/Blocks/Search/components/SearchInput.jsx +9 -2
  43. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +12 -1
  44. package/src/components/manage/Blocks/ToC/Schema.jsx +5 -1
  45. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +142 -8
  46. package/src/components/manage/LinksToItem/LinksToItem.jsx +209 -0
  47. package/src/components/manage/LinksToItem/LinksToItem.test.jsx +97 -0
  48. package/src/components/manage/Toolbar/More.jsx +15 -0
  49. package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +1 -1
  50. package/src/components/theme/Breadcrumbs/Breadcrumbs.jsx +52 -99
  51. package/src/components/theme/Breadcrumbs/Breadcrumbs.stories.jsx +14 -13
  52. package/src/components/theme/Comments/CommentEditModal.jsx +63 -115
  53. package/src/components/theme/ContactForm/ContactForm.jsx +108 -192
  54. package/src/components/theme/ContactForm/ContactForm.stories.jsx +1 -1
  55. package/src/components/theme/ContactForm/ContactForm.test.jsx +2 -3
  56. package/src/components/theme/Login/Login.jsx +1 -1
  57. package/src/components/theme/SearchWidget/SearchWidget.jsx +38 -98
  58. package/src/components/theme/View/LinkView.jsx +51 -79
  59. package/src/config/NonContentRoutes.jsx +1 -0
  60. package/src/config/index.js +2 -0
  61. package/src/config/server.js +2 -0
  62. package/src/express-middleware/ok.js +16 -0
  63. package/src/hooks/client/useClient.js +11 -0
  64. package/src/hooks/index.js +1 -0
  65. package/src/routes.js +9 -0
  66. package/theme/themes/pastanaga/extras/main.less +2 -1
  67. package/theme/themes/pastanaga/extras/toc.less +29 -0
  68. package/news/4351.bugfix +0 -1
  69. package/news/4725.bugfix +0 -1
  70. package/news/4932.bugfix +0 -1
  71. package/news/4941.documentation +0 -1
  72. package/news/4951.breaking +0 -1
  73. package/news/4962.feature +0 -1
  74. package/news/4964.bugfix +0 -1
@@ -0,0 +1,122 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ const messages = defineMessages({
4
+ hideHeaders: {
5
+ id: 'Hide headers',
6
+ defaultMessage: 'Hide headers',
7
+ },
8
+ sortable: {
9
+ id: 'Make the table sortable',
10
+ defaultMessage: 'Make the table sortable',
11
+ },
12
+ sortableDescription: {
13
+ id: 'Visible only in view mode',
14
+ defaultMessage: 'Visible only in view mode',
15
+ },
16
+ fixed: {
17
+ id: 'Fixed width table cells',
18
+ defaultMessage: 'Fixed width table cells',
19
+ },
20
+ compact: {
21
+ id: 'Make the table compact',
22
+ defaultMessage: 'Make the table compact',
23
+ },
24
+ basic: {
25
+ id: 'Reduce complexity',
26
+ defaultMessage: 'Reduce complexity',
27
+ },
28
+ celled: {
29
+ id: 'Divide each row into separate cells',
30
+ defaultMessage: 'Divide each row into separate cells',
31
+ },
32
+ inverted: {
33
+ id: 'Table color inverted',
34
+ defaultMessage: 'Table color inverted',
35
+ },
36
+ striped: {
37
+ id: 'Stripe alternate rows with color',
38
+ defaultMessage: 'Stripe alternate rows with color',
39
+ },
40
+ });
41
+
42
+ function TableSchema(props) {
43
+ const { intl } = props;
44
+ return {
45
+ title: 'Table block',
46
+ fieldsets: [
47
+ {
48
+ id: 'default',
49
+ title: 'Default',
50
+ fields: [
51
+ 'hideHeaders',
52
+ 'sortable',
53
+ 'fixed',
54
+ 'celled',
55
+ 'striped',
56
+ 'compact',
57
+ 'basic',
58
+ 'inverted',
59
+ ],
60
+ },
61
+ ],
62
+ properties: {
63
+ hideHeaders: {
64
+ title: intl.formatMessage(messages.hideHeaders),
65
+ type: 'boolean',
66
+ },
67
+ sortable: {
68
+ title: intl.formatMessage(messages.sortable),
69
+ type: 'boolean',
70
+ },
71
+ fixed: {
72
+ title: intl.formatMessage(messages.fixed),
73
+ type: 'boolean',
74
+ },
75
+ celled: {
76
+ title: intl.formatMessage(messages.celled),
77
+ type: 'boolean',
78
+ },
79
+ striped: {
80
+ title: intl.formatMessage(messages.striped),
81
+ type: 'boolean',
82
+ },
83
+ compact: {
84
+ title: intl.formatMessage(messages.compact),
85
+ type: 'boolean',
86
+ },
87
+ basic: {
88
+ title: intl.formatMessage(messages.basic),
89
+ type: 'boolean',
90
+ },
91
+ inverted: {
92
+ title: intl.formatMessage(messages.inverted),
93
+ type: 'boolean',
94
+ },
95
+ },
96
+ required: [],
97
+ };
98
+ }
99
+
100
+ export function TableBlockSchema(props) {
101
+ return {
102
+ title: 'Table block',
103
+ fieldsets: [
104
+ {
105
+ id: 'default',
106
+ title: 'Default',
107
+ fields: ['table'],
108
+ },
109
+ ],
110
+ properties: {
111
+ table: {
112
+ title: 'Table block',
113
+ widget: 'object',
114
+ schema: TableSchema(props),
115
+ },
116
+ },
117
+
118
+ required: [],
119
+ };
120
+ }
121
+
122
+ export default TableBlockSchema;
@@ -72,19 +72,28 @@ export const toggleInlineStyle = (editor, style) => {
72
72
  };
73
73
 
74
74
  export const isBlockStyleActive = (editor, style) => {
75
+ const keyName = `style-${style}`;
75
76
  const sn = Array.from(
76
77
  Editor.nodes(editor, {
77
- match: (n) => !Editor.isEditor(n) && typeof n.styleName === 'string',
78
- mode: 'highest',
78
+ match: (n) => {
79
+ const isStyle = typeof n.styleName === 'string' || n[keyName];
80
+ return !Editor.isEditor(n) && isStyle;
81
+ },
82
+ mode: 'all',
79
83
  }),
80
84
  );
81
85
 
82
86
  for (const [n] of sn) {
83
- if (n.styleName.split(' ').filter((x) => x === style).length > 0) {
87
+ if (typeof n.styleName === 'string') {
88
+ if (n.styleName.split(' ').filter((x) => x === style).length > 0) {
89
+ return true;
90
+ }
91
+ } else if (
92
+ n[keyName] &&
93
+ keyName.split('-').filter((x) => x === style).length > 0
94
+ )
84
95
  return true;
85
- }
86
96
  }
87
-
88
97
  return false;
89
98
  };
90
99
 
@@ -125,6 +125,13 @@ export function createEmptyParagraph() {
125
125
  };
126
126
  }
127
127
 
128
+ export function createParagraph(text) {
129
+ return {
130
+ type: config.settings.slate.defaultBlockType,
131
+ children: [{ text }],
132
+ };
133
+ }
134
+
128
135
  export const isSingleBlockTypeActive = (editor, format) => {
129
136
  const [match] = Editor.nodes(editor, {
130
137
  match: (n) => n.type === format,
@@ -4,13 +4,26 @@
4
4
  */
5
5
 
6
6
  import React from 'react';
7
+ import isUndefined from 'lodash/isUndefined';
8
+ import isString from 'lodash/isString';
7
9
  import { FormFieldWrapper } from '@plone/volto/components';
8
10
  import SlateEditor from '@plone/volto-slate/editor/SlateEditor';
9
11
 
10
- import { createEmptyParagraph } from '../utils/blocks';
12
+ import { createEmptyParagraph, createParagraph } from '../utils/blocks';
11
13
 
12
14
  import './style.css';
13
15
 
16
+ const getValue = (value) => {
17
+ if (isUndefined(value) || !isUndefined(value?.data)) {
18
+ return [createEmptyParagraph()];
19
+ }
20
+ // Previously this was a text field
21
+ if (isString(value)) {
22
+ return [createParagraph(value)];
23
+ }
24
+ return value;
25
+ };
26
+
14
27
  const SlateRichTextWidget = (props) => {
15
28
  const {
16
29
  id,
@@ -42,13 +55,7 @@ const SlateRichTextWidget = (props) => {
42
55
  readOnly={readOnly}
43
56
  id={id}
44
57
  name={id}
45
- value={
46
- typeof value === 'undefined' ||
47
- typeof value?.data !==
48
- 'undefined' /* previously this was a Draft block */
49
- ? [createEmptyParagraph()]
50
- : value
51
- }
58
+ value={getValue(value)}
52
59
  onChange={(newValue) => {
53
60
  onChange(id, newValue);
54
61
  }}
@@ -108,6 +108,7 @@ export History from '@plone/volto/components/manage/History/History';
108
108
  export Sharing from '@plone/volto/components/manage/Sharing/Sharing';
109
109
  export Rules from '@plone/volto/components/manage/Rules/Rules';
110
110
  export Aliases from '@plone/volto/components/manage/Aliases/Aliases';
111
+ export LinksToItem from '@plone/volto/components/manage/LinksToItem/LinksToItem';
111
112
  export Workflow from '@plone/volto/components/manage/Workflow/Workflow';
112
113
  export Messages from '@plone/volto/components/manage/Messages/Messages';
113
114
  export BlockChooser from '@plone/volto/components/manage/BlockChooser/BlockChooser';
@@ -12,7 +12,7 @@ const messages = defineMessages({
12
12
  hideFilters: { id: 'Hide filters', defaultMessage: 'Hide filters' },
13
13
  });
14
14
 
15
- const showFacet = (index) => {
15
+ const defaultShowFacet = (index) => {
16
16
  const { values } = index;
17
17
  return index
18
18
  ? hasNonValueOperation(index.operations || []) ||
@@ -91,7 +91,11 @@ const Facets = (props) => {
91
91
 
92
92
  // TODO :handle changing the type of facet (multi/nonmulti)
93
93
 
94
- const { view: FacetWidget, stateToValue } = resolveExtension(
94
+ const {
95
+ view: FacetWidget,
96
+ stateToValue,
97
+ showFacet = defaultShowFacet,
98
+ } = resolveExtension(
95
99
  'type',
96
100
  search.extensions.facetWidgets.types,
97
101
  facetSettings,
@@ -13,7 +13,14 @@ const messages = defineMessages({
13
13
  });
14
14
 
15
15
  const SearchInput = (props) => {
16
- const { data, searchText, setSearchText, isLive, onTriggerSearch } = props;
16
+ const {
17
+ data,
18
+ searchText,
19
+ setSearchText,
20
+ isLive,
21
+ onTriggerSearch,
22
+ removeSearchQuery,
23
+ } = props;
17
24
  const intl = useIntl();
18
25
 
19
26
  return (
@@ -44,7 +51,7 @@ const SearchInput = (props) => {
44
51
  className="search-input-clear-icon-button"
45
52
  onClick={() => {
46
53
  setSearchText('');
47
- onTriggerSearch('');
54
+ removeSearchQuery();
48
55
  }}
49
56
  >
50
57
  <Icon name={clearSVG} />
@@ -301,7 +301,7 @@ const withSearch = (options) => (WrappedComponent) => {
301
301
  id,
302
302
  query: data.query || {},
303
303
  facets: toSearchFacets || facets,
304
- searchText: toSearchText || searchText,
304
+ searchText: toSearchText ? toSearchText.trim() : '',
305
305
  sortOn: toSortOn || sortOn,
306
306
  sortOrder: toSortOrder || sortOrder,
307
307
  facetSettings,
@@ -329,6 +329,16 @@ const withSearch = (options) => (WrappedComponent) => {
329
329
  ],
330
330
  );
331
331
 
332
+ const removeSearchQuery = () => {
333
+ searchData.query = searchData.query.reduce(
334
+ // Remove SearchableText from query
335
+ (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
336
+ [],
337
+ );
338
+ setSearchData(searchData);
339
+ setLocationSearchData(getSearchFields(searchData));
340
+ };
341
+
332
342
  const querystringResults = useSelector(
333
343
  (state) => state.querystringsearch.subrequests,
334
344
  );
@@ -347,6 +357,7 @@ const withSearch = (options) => (WrappedComponent) => {
347
357
  sortOrder={sortOrder}
348
358
  searchedText={urlSearchText}
349
359
  searchText={searchText}
360
+ removeSearchQuery={removeSearchQuery}
350
361
  setSearchText={setSearchText}
351
362
  onTriggerSearch={onTriggerSearch}
352
363
  totalItems={totalItems}
@@ -10,7 +10,7 @@ const TableOfContentsSchema = ({ data }) => {
10
10
  fields: [
11
11
  'title',
12
12
  'hide_title',
13
- ...(variation === 'default' ? ['ordered'] : []),
13
+ ...(variation === 'default' ? ['ordered'] : ['sticky']),
14
14
  'levels',
15
15
  ],
16
16
  },
@@ -39,6 +39,10 @@ const TableOfContentsSchema = ({ data }) => {
39
39
  title: 'Ordered',
40
40
  type: 'boolean',
41
41
  },
42
+ sticky: {
43
+ title: 'Sticky',
44
+ type: 'boolean',
45
+ },
42
46
  },
43
47
  required: [],
44
48
  };
@@ -1,12 +1,7 @@
1
- /**
2
- * View toc block.
3
- * @module components/manage/Blocks/ToC/View
4
- */
5
-
6
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
7
2
  import PropTypes from 'prop-types';
8
3
  import { map } from 'lodash';
9
- import { Menu } from 'semantic-ui-react';
4
+ import { Menu, Dropdown } from 'semantic-ui-react';
10
5
  import { FormattedMessage, injectIntl } from 'react-intl';
11
6
  import AnchorLink from 'react-anchor-link-smooth-scroll';
12
7
  import Slugger from 'github-slugger';
@@ -36,6 +31,131 @@ const RenderMenuItems = ({ items }) => {
36
31
  * @extends Component
37
32
  */
38
33
  const View = ({ data, tocEntries }) => {
34
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
35
+ // When the page is resized to prevent items from the TOC from going out of the viewport,
36
+ // a dropdown menu is added containing all the items that don't fit.
37
+ const handleResize = () => {
38
+ const menuElement = document.querySelector('.responsive-menu');
39
+ const containerWidth = menuElement.offsetWidth;
40
+
41
+ // Get all divs that contain the items from the TOC, except the dropdown button
42
+ const nested = document.querySelectorAll(
43
+ '.responsive-menu .item:not(.dropdown)',
44
+ );
45
+ const nestedArray = Object.values(nested);
46
+ const middle = Math.ceil(nestedArray.length / 2);
47
+ const firstHalfNested = nestedArray.slice(0, middle);
48
+ const secondHalfNested = nestedArray.slice(middle);
49
+
50
+ const dropdown = document.querySelector('.dropdown');
51
+ const dropdownWidth = dropdown.offsetWidth;
52
+
53
+ const firstHalfNestedHiddenItems = [];
54
+
55
+ // Add a 'hidden' class for the items that should be in the dropdown
56
+ firstHalfNested.forEach((item) => {
57
+ const itemOffsetLeft = item.offsetLeft;
58
+ const itemOffsetWidth = item.offsetWidth;
59
+ if (itemOffsetLeft + itemOffsetWidth > containerWidth - dropdownWidth) {
60
+ item.classList.add('hidden');
61
+ firstHalfNestedHiddenItems.push(item);
62
+ } else {
63
+ item.classList.remove('hidden');
64
+ }
65
+ });
66
+
67
+ secondHalfNested.forEach((item) => item.classList.add('hidden-dropdown'));
68
+
69
+ const diff = firstHalfNested.length - firstHalfNestedHiddenItems.length;
70
+ const secondHalfNestedShownItems = secondHalfNested.slice(diff);
71
+ secondHalfNestedShownItems.forEach((item) =>
72
+ item.classList.remove('hidden-dropdown'),
73
+ );
74
+
75
+ // If there are elements that should be displayed in the dropdown, show the dropdown button
76
+ if (secondHalfNestedShownItems.length > 0)
77
+ dropdown.classList.remove('hidden-dropdown');
78
+ else {
79
+ dropdown.classList.add('hidden-dropdown');
80
+ }
81
+ };
82
+
83
+ const handleDropdownKeyDown = (event) => {
84
+ const dropdownMenu = document.querySelector('.menu.transition');
85
+ if (event.key === 'ArrowDown' && isDropdownOpen) {
86
+ event.preventDefault();
87
+ const menuItems = dropdownMenu.querySelectorAll(
88
+ '.item:not(.hidden-dropdown)',
89
+ );
90
+ const focusedItem = dropdownMenu.querySelector('.item.focused');
91
+ const focusedIndex = Array.from(menuItems).indexOf(focusedItem);
92
+
93
+ if (focusedIndex === -1) {
94
+ // No item is currently focused, so focus the first item
95
+ menuItems[0].classList.add('focused');
96
+ } else if (focusedIndex === menuItems.length - 1) {
97
+ // Remove focus from the currently focused item and close the dropdown
98
+ focusedItem.classList.remove('focused');
99
+ setIsDropdownOpen(false);
100
+
101
+ // Focus the next element on the page
102
+ const nextElement = dropdownMenu.nextElementSibling;
103
+ if (nextElement) {
104
+ nextElement.focus();
105
+ }
106
+ } else {
107
+ // Remove focus from the currently focused item
108
+ focusedItem.classList.remove('focused');
109
+
110
+ // Focus the next item or wrap around to the first item
111
+ const nextIndex = (focusedIndex + 1) % menuItems.length;
112
+ menuItems[nextIndex].classList.add('focused');
113
+ }
114
+ } else if (event.key === 'Enter' && isDropdownOpen) {
115
+ const focusedItem = dropdownMenu.querySelector('.item.focused');
116
+ if (focusedItem) {
117
+ focusedItem.querySelector('a').click();
118
+ focusedItem.classList.remove('focused');
119
+ }
120
+ } else if (event.key === 'Tab') {
121
+ const focusedItem = dropdownMenu.querySelector('.item.focused');
122
+ if (focusedItem) {
123
+ focusedItem.classList.remove('focused');
124
+ }
125
+ }
126
+ };
127
+
128
+ useEffect(() => {
129
+ if (data.sticky) {
130
+ const toc = document.querySelector('.horizontalMenu');
131
+ const tocPos = toc ? toc.offsetTop : 0;
132
+
133
+ const handleScroll = () => {
134
+ let scrollPos = window.scrollY;
135
+ if (scrollPos > tocPos && toc) {
136
+ toc.classList.add('sticky-toc');
137
+ } else if (scrollPos <= tocPos && toc) {
138
+ toc.classList.remove('sticky-toc');
139
+ }
140
+ };
141
+
142
+ window.addEventListener('scroll', handleScroll);
143
+
144
+ return () => {
145
+ window.removeEventListener('scroll', handleScroll);
146
+ };
147
+ }
148
+ }, [data.sticky]);
149
+
150
+ useEffect(() => {
151
+ handleResize();
152
+ window.addEventListener('resize', handleResize);
153
+
154
+ return () => {
155
+ window.removeEventListener('resize', handleResize);
156
+ };
157
+ });
158
+
39
159
  return (
40
160
  <>
41
161
  {data.title && !data.hide_title ? (
@@ -50,8 +170,22 @@ const View = ({ data, tocEntries }) => {
50
170
  ) : (
51
171
  ''
52
172
  )}
53
- <Menu>
173
+ <Menu className="responsive-menu">
54
174
  <RenderMenuItems items={tocEntries} />
175
+ <Dropdown
176
+ item
177
+ text="More"
178
+ className="hidden-dropdown"
179
+ open={isDropdownOpen}
180
+ onOpen={() => setIsDropdownOpen(true)}
181
+ onClose={() => setIsDropdownOpen(false)}
182
+ tabIndex={0}
183
+ onKeyDown={handleDropdownKeyDown}
184
+ >
185
+ <Dropdown.Menu>
186
+ <RenderMenuItems items={tocEntries} />
187
+ </Dropdown.Menu>
188
+ </Dropdown>
55
189
  </Menu>
56
190
  </>
57
191
  );