@plone/volto 17.19.0 → 17.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/CHANGELOG.md CHANGED
@@ -17,6 +17,17 @@ myst:
17
17
 
18
18
  <!-- towncrier release notes start -->
19
19
 
20
+ ## 17.20.0 (2024-10-09)
21
+
22
+ ### Feature
23
+
24
+ - Pass the `user`, `navRoot`, and `contentType` objects to the `restricted` function of the block settings. @wesleybl [#6264](https://github.com/plone/volto/issues/6264)
25
+ - improve DiffField.jsx with better support for displaying HTML elements such as images @dobri1408 [#6384](https://github.com/plone/volto/issues/6384)
26
+
27
+ ### Documentation
28
+
29
+ - Install Vale via pip, and pin to <3.0.0, allowing it to run locally and in CI without reconfiguration. @stevepiercy [#6377](https://github.com/plone/volto/issues/6377)
30
+
20
31
  ## 17.19.0 (2024-10-03)
21
32
 
22
33
  ### Feature
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "17.19.0",
12
+ "version": "17.20.0",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "17.19.0",
3
+ "version": "17.20.0",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -4,6 +4,7 @@ import { filter, isEmpty } from 'lodash';
4
4
  import { Menu } from 'semantic-ui-react';
5
5
  import { useIntl, FormattedMessage } from 'react-intl';
6
6
  import { Icon } from '@plone/volto/components';
7
+ import { useUser } from '@plone/volto/hooks';
7
8
 
8
9
  const emptySlateBlock = () => ({
9
10
  value: [
@@ -105,9 +106,13 @@ const PersistentSlashMenu = ({ editor }) => {
105
106
  selected,
106
107
  allowedBlocks,
107
108
  detached,
109
+ navRoot,
110
+ contentType,
108
111
  } = props;
109
112
  const disableNewBlocks = data?.disableNewBlocks || detached;
110
113
 
114
+ const user = useUser();
115
+
111
116
  const [slashMenuSelected, setSlashMenuSelected] = React.useState(0);
112
117
 
113
118
  const hasAllowedBlocks = !isEmpty(allowedBlocks);
@@ -122,7 +127,13 @@ const PersistentSlashMenu = ({ editor }) => {
122
127
  hasAllowedBlocks
123
128
  ? allowedBlocks.includes(item.id)
124
129
  : typeof item.restricted === 'function'
125
- ? !item.restricted({ properties, block: item })
130
+ ? !item.restricted({
131
+ properties,
132
+ block: item,
133
+ navRoot,
134
+ contentType,
135
+ user,
136
+ })
126
137
  : !item.restricted,
127
138
  )
128
139
  .filter((block) => Boolean(block.title && block.id))
@@ -152,6 +163,9 @@ const PersistentSlashMenu = ({ editor }) => {
152
163
  properties,
153
164
  slashCommand,
154
165
  hasAllowedBlocks,
166
+ navRoot,
167
+ contentType,
168
+ user,
155
169
  ],
156
170
  );
157
171
 
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { useUser } from '@plone/volto/hooks';
2
3
  import PropTypes from 'prop-types';
3
4
  import { filter, map, groupBy, isEmpty } from 'lodash';
4
5
  import { Accordion, Button } from 'semantic-ui-react';
@@ -35,6 +36,7 @@ const BlockChooser = ({
35
36
  contentType,
36
37
  }) => {
37
38
  const intl = useIntl();
39
+ const user = useUser();
38
40
  const hasAllowedBlocks = !isEmpty(allowedBlocks);
39
41
 
40
42
  const filteredBlocksConfig = filter(blocksConfig, (item) => {
@@ -57,7 +59,13 @@ const BlockChooser = ({
57
59
  // depending on this function, given properties (current present blocks) and the
58
60
  // block being evaluated
59
61
  return typeof item.restricted === 'function'
60
- ? !item.restricted({ properties, block: item, navRoot, contentType })
62
+ ? !item.restricted({
63
+ properties,
64
+ block: item,
65
+ navRoot,
66
+ contentType,
67
+ user,
68
+ })
61
69
  : !item.restricted;
62
70
  }
63
71
  }
@@ -5,6 +5,7 @@ import { Provider } from 'react-intl-redux';
5
5
  import configureStore from 'redux-mock-store';
6
6
  import BlockChooser from './BlockChooser';
7
7
  import config from '@plone/volto/registry';
8
+ import jwt from 'jsonwebtoken';
8
9
 
9
10
  const blockSVG = {};
10
11
 
@@ -122,6 +123,9 @@ const store = mockStore({
122
123
  locale: 'en',
123
124
  messages: {},
124
125
  },
126
+ userSession: {
127
+ token: jwt.sign({ fullname: 'John Doe' }, 'secret'),
128
+ },
125
129
  });
126
130
 
127
131
  describe('BlocksChooser', () => {
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import React from 'react';
7
- // import { diffWords as dWords } from 'diff';
8
7
  import { join, map } from 'lodash';
9
8
  import PropTypes from 'prop-types';
10
9
  import { Grid } from 'semantic-ui-react';
@@ -13,20 +12,128 @@ import { Provider } from 'react-intl-redux';
13
12
  import { createBrowserHistory } from 'history';
14
13
  import { ConnectedRouter } from 'connected-react-router';
15
14
  import { useSelector } from 'react-redux';
16
-
15
+ import config from '@plone/volto/registry';
17
16
  import { Api } from '@plone/volto/helpers';
18
17
  import configureStore from '@plone/volto/store';
19
- import { DefaultView } from '@plone/volto/components/';
18
+ import { RenderBlocks } from '@plone/volto/components';
20
19
  import { serializeNodes } from '@plone/volto-slate/editor/render';
21
-
22
20
  import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
23
21
 
24
- /**
25
- * Enhanced diff words utility
26
- * @function diffWords
27
- * @param oneStr Field one
28
- * @param twoStr Field two
29
- */
22
+ const isHtmlTag = (str) => {
23
+ // Match complete HTML tags, including:
24
+ // 1. Opening tags like <div>, <img src="example" />, <svg>...</svg>
25
+ // 2. Self-closing tags like <img />, <br />
26
+ // 3. Closing tags like </div>
27
+ return /^<([a-zA-Z]+[0-9]*)\b[^>]*>|^<\/([a-zA-Z]+[0-9]*)\b[^>]*>$|^<([a-zA-Z]+[0-9]*)\b[^>]*\/>$/.test(
28
+ str,
29
+ );
30
+ };
31
+
32
+ const splitWords = (str) => {
33
+ if (typeof str !== 'string') return str;
34
+ if (!str) return [];
35
+
36
+ const result = [];
37
+ let currentWord = '';
38
+ let insideTag = false;
39
+ let insideSpecialTag = false;
40
+ let tagBuffer = '';
41
+
42
+ // Special tags that should not be split (e.g., <img />, <svg> ... </svg>)
43
+ const specialTags = ['img', 'svg'];
44
+
45
+ for (let i = 0; i < str.length; i++) {
46
+ const char = str[i];
47
+
48
+ // Start of an HTML tag
49
+ if (char === '<') {
50
+ if (currentWord) {
51
+ result.push(currentWord); // Push text before the tag
52
+ currentWord = '';
53
+ }
54
+ insideTag = true;
55
+ tagBuffer += char;
56
+ }
57
+ // End of an HTML tag
58
+ else if (char === '>') {
59
+ tagBuffer += char;
60
+ insideTag = false;
61
+
62
+ // Check if the tagBuffer contains a special tag
63
+ const tagNameMatch = tagBuffer.match(/^<\/?([a-zA-Z]+[0-9]*)\b/);
64
+ if (tagNameMatch && specialTags.includes(tagNameMatch[1])) {
65
+ insideSpecialTag =
66
+ tagNameMatch[0].startsWith('<') && !tagNameMatch[0].startsWith('</');
67
+ result.push(tagBuffer); // Push the complete special tag as one unit
68
+ tagBuffer = '';
69
+ continue;
70
+ }
71
+
72
+ result.push(tagBuffer); // Push the complete tag
73
+ tagBuffer = '';
74
+ }
75
+ // Inside the tag or special tag
76
+ else if (insideTag || insideSpecialTag) {
77
+ tagBuffer += char;
78
+ }
79
+ // Space outside of tags - push current word
80
+ else if (char === ' ' && !insideTag && !insideSpecialTag) {
81
+ if (currentWord) {
82
+ result.push(currentWord);
83
+ currentWord = '';
84
+ }
85
+ result.push(' ');
86
+ } else if (
87
+ char === ',' &&
88
+ i < str.length - 1 &&
89
+ str[i + 1] !== ' ' &&
90
+ !insideTag &&
91
+ !insideSpecialTag
92
+ ) {
93
+ if (currentWord) {
94
+ result.push(currentWord + char);
95
+ currentWord = '';
96
+ }
97
+ result.push(' ');
98
+ }
99
+ // Accumulate characters outside of tags
100
+ else {
101
+ currentWord += char;
102
+ }
103
+ }
104
+
105
+ // Push any remaining text
106
+ if (currentWord) {
107
+ result.push(currentWord);
108
+ }
109
+ if (tagBuffer) {
110
+ result.push(tagBuffer); // Push remaining tagBuffer
111
+ }
112
+
113
+ return result;
114
+ };
115
+
116
+ const formatDiffPart = (part, value, side) => {
117
+ if (!isHtmlTag(value)) {
118
+ if (part.removed && (side === 'left' || side === 'unified')) {
119
+ return `<span class="deletion">${value}</span>`;
120
+ } else if (part.removed) return '';
121
+ else if (part.added && (side === 'right' || side === 'unified')) {
122
+ return `<span class="addition">${value}</span>`;
123
+ } else if (part.added) return '';
124
+ return value;
125
+ } else {
126
+ if (side === 'unified' && part.added) return value;
127
+ else if (side === 'unified' && part.removed) return '';
128
+ if (part.removed && side === 'left') {
129
+ return value;
130
+ } else if (part.removed) return '';
131
+ else if (part.added && side === 'right') {
132
+ return value;
133
+ } else if (part.added) return '';
134
+ return value;
135
+ }
136
+ };
30
137
 
31
138
  /**
32
139
  * Diff field component.
@@ -36,6 +143,7 @@ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
36
143
  * @param {Object} schema Field schema
37
144
  * @returns {string} Markup of the component.
38
145
  */
146
+
39
147
  const DiffField = ({
40
148
  one,
41
149
  two,
@@ -51,7 +159,10 @@ const DiffField = ({
51
159
  timeStyle: 'short',
52
160
  };
53
161
  const diffWords = (oneStr, twoStr) => {
54
- return diffLib.diffWords(String(oneStr), String(twoStr));
162
+ return diffLib.diffArrays(
163
+ splitWords(String(oneStr)),
164
+ splitWords(String(twoStr)),
165
+ );
55
166
  };
56
167
 
57
168
  let parts, oneArray, twoArray;
@@ -78,14 +189,14 @@ const DiffField = ({
78
189
  ReactDOMServer.renderToStaticMarkup(
79
190
  <Provider store={store}>
80
191
  <ConnectedRouter history={history}>
81
- <DefaultView content={contentOne} />
192
+ <RenderBlocks content={contentOne} />
82
193
  </ConnectedRouter>
83
194
  </Provider>,
84
195
  ),
85
196
  ReactDOMServer.renderToStaticMarkup(
86
197
  <Provider store={store}>
87
198
  <ConnectedRouter history={history}>
88
- <DefaultView content={contentTwo} />
199
+ <RenderBlocks content={contentTwo} />
89
200
  </ConnectedRouter>
90
201
  </Provider>,
91
202
  ),
@@ -116,7 +227,30 @@ const DiffField = ({
116
227
  }
117
228
  case 'textarea':
118
229
  default:
119
- parts = diffWords(one, two);
230
+ const Widget = config.widgets?.views?.widget?.[schema.widget];
231
+
232
+ if (Widget) {
233
+ const api = new Api();
234
+ const history = createBrowserHistory();
235
+ const store = configureStore(window.__data, history, api);
236
+ parts = diffWords(
237
+ ReactDOMServer.renderToStaticMarkup(
238
+ <Provider store={store}>
239
+ <ConnectedRouter history={history}>
240
+ <Widget value={one} />
241
+ </ConnectedRouter>
242
+ </Provider>,
243
+ ),
244
+ ReactDOMServer.renderToStaticMarkup(
245
+ <Provider store={store}>
246
+ <ConnectedRouter history={history}>
247
+ <Widget value={two} />
248
+ </ConnectedRouter>
249
+ </Provider>,
250
+ ),
251
+ );
252
+ } else parts = diffWords(one, two);
253
+
120
254
  break;
121
255
  }
122
256
  } else if (schema.type === 'object') {
@@ -128,6 +262,7 @@ const DiffField = ({
128
262
  } else {
129
263
  parts = diffWords(one?.title || one, two?.title || two);
130
264
  }
265
+
131
266
  return (
132
267
  <Grid data-testid="DiffField">
133
268
  <Grid.Row>
@@ -140,14 +275,12 @@ const DiffField = ({
140
275
  <span
141
276
  dangerouslySetInnerHTML={{
142
277
  __html: join(
143
- map(
144
- parts,
145
- (part) =>
146
- (part.removed &&
147
- `<span class="deletion">${part.value}</span>`) ||
148
- (!part.added && `<span>${part.value}</span>`) ||
149
- '',
150
- ),
278
+ map(parts, (part) => {
279
+ let combined = (part.value || []).reduce((acc, value) => {
280
+ return acc + formatDiffPart(part, value, 'left');
281
+ }, '');
282
+ return combined;
283
+ }),
151
284
  '',
152
285
  ),
153
286
  }}
@@ -157,14 +290,12 @@ const DiffField = ({
157
290
  <span
158
291
  dangerouslySetInnerHTML={{
159
292
  __html: join(
160
- map(
161
- parts,
162
- (part) =>
163
- (part.added &&
164
- `<span class="addition">${part.value}</span>`) ||
165
- (!part.removed && `<span>${part.value}</span>`) ||
166
- '',
167
- ),
293
+ map(parts, (part) => {
294
+ let combined = (part.value || []).reduce((acc, value) => {
295
+ return acc + formatDiffPart(part, value, 'right');
296
+ }, '');
297
+ return combined;
298
+ }),
168
299
  '',
169
300
  ),
170
301
  }}
@@ -178,15 +309,12 @@ const DiffField = ({
178
309
  <span
179
310
  dangerouslySetInnerHTML={{
180
311
  __html: join(
181
- map(
182
- parts,
183
- (part) =>
184
- (part.removed &&
185
- `<span class="deletion">${part.value}</span>`) ||
186
- (part.added &&
187
- `<span class="addition">${part.value}</span>`) ||
188
- (!part.added && `<span>${part.value}</span>`),
189
- ),
312
+ map(parts, (part) => {
313
+ let combined = (part.value || []).reduce((acc, value) => {
314
+ return acc + formatDiffPart(part, value, 'unified');
315
+ }, '');
316
+ return combined;
317
+ }),
190
318
  '',
191
319
  ),
192
320
  }}
@@ -1,2 +1,3 @@
1
1
  export { default as useClipboard } from '@plone/volto/hooks/clipboard/useClipboard';
2
2
  export { useClient } from '@plone/volto/hooks/client/useClient';
3
+ export { default as useUser } from '@plone/volto/hooks/user/useUser';
@@ -0,0 +1,23 @@
1
+ import { useEffect } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import jwtDecode from 'jwt-decode';
4
+ import { getUser } from '@plone/volto/actions';
5
+
6
+ const useUser = () => {
7
+ const users = useSelector((state) => state.users);
8
+ const user = users?.user;
9
+ const userId = useSelector((state) =>
10
+ state.userSession.token ? jwtDecode(state.userSession.token).sub : '',
11
+ );
12
+ const dispatch = useDispatch();
13
+
14
+ useEffect(() => {
15
+ if (!user?.id && users?.get.loading === false) {
16
+ dispatch(getUser(userId));
17
+ }
18
+ }, [dispatch, userId, user, users?.get.loading]);
19
+
20
+ return user;
21
+ };
22
+
23
+ export default useUser;
@@ -1,2 +1,3 @@
1
1
  export { default as useClipboard } from "@plone/volto/hooks/clipboard/useClipboard";
2
2
  export { useClient } from "@plone/volto/hooks/client/useClient";
3
+ export { default as useUser } from "@plone/volto/hooks/user/useUser";
@@ -0,0 +1,2 @@
1
+ export default useUser;
2
+ declare function useUser(): any;