@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.
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/packages/volto-slate/src/blocks/Text/SlashMenu.jsx +15 -1
- package/src/components/manage/BlockChooser/BlockChooser.jsx +9 -1
- package/src/components/manage/BlockChooser/BlockChooser.test.jsx +4 -0
- package/src/components/manage/Diff/DiffField.jsx +167 -39
- package/src/hooks/index.js +1 -0
- package/src/hooks/user/useUser.js +23 -0
- package/types/hooks/index.d.ts +1 -0
- package/types/hooks/user/useUser.d.ts +2 -0
package/.yarn/install-state.gz
CHANGED
|
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
|
@@ -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({
|
|
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({
|
|
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 {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
}}
|
package/src/hooks/index.js
CHANGED
|
@@ -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;
|
package/types/hooks/index.d.ts
CHANGED