@plone/volto 19.0.0-alpha.3 → 19.0.0-alpha.5
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/CHANGELOG.md +55 -0
- package/README.md +0 -2
- package/cypress/support/commands.js +13 -6
- package/locales/af.json +1 -1
- package/locales/ar.json +1 -1
- package/locales/bg.json +1 -1
- package/locales/bn.json +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +16 -2
- package/locales/ca.json +1 -1
- package/locales/cs.json +1 -1
- package/locales/cy.json +1 -1
- package/locales/da.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +19 -5
- package/locales/de.json +1 -1
- package/locales/el.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +16 -2
- package/locales/en.json +1 -1
- package/locales/en_AU.json +1 -1
- package/locales/en_GB.json +1 -1
- package/locales/eo.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +16 -2
- package/locales/es.json +1 -1
- package/locales/et.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +16 -2
- package/locales/eu.json +1 -1
- package/locales/fa.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +16 -2
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +16 -2
- package/locales/fr.json +1 -1
- package/locales/fu.json +1 -1
- package/locales/gl.json +1 -1
- package/locales/he.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +16 -2
- package/locales/hi.json +1 -1
- package/locales/hr.json +1 -1
- package/locales/hu.json +1 -1
- package/locales/hy.json +1 -1
- package/locales/id.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +16 -2
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +16 -2
- package/locales/ja.json +1 -1
- package/locales/ka.json +1 -1
- package/locales/kn.json +1 -1
- package/locales/ko.json +1 -1
- package/locales/lt.json +1 -1
- package/locales/lv.json +1 -1
- package/locales/mi.json +1 -1
- package/locales/mk.json +1 -1
- package/locales/my.json +1 -1
- package/locales/nb_NO.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +16 -2
- package/locales/nl.json +1 -1
- package/locales/nn.json +1 -1
- package/locales/pl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +16 -2
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +16 -2
- package/locales/pt_BR.json +1 -1
- package/locales/rm.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +16 -2
- package/locales/ro.json +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +16 -2
- package/locales/ru.json +1 -1
- package/locales/sk.json +1 -1
- package/locales/sl.json +1 -1
- package/locales/sm.json +1 -1
- package/locales/sq.json +1 -1
- package/locales/sr.json +1 -1
- package/locales/sr@cyrl.json +1 -1
- package/locales/sr@latn.json +1 -1
- package/locales/sv.json +1 -1
- package/locales/ta.json +1 -1
- package/locales/te.json +1 -1
- package/locales/th.json +1 -1
- package/locales/to.json +1 -1
- package/locales/tr.json +1 -1
- package/locales/uk.json +1 -1
- package/locales/vi.json +1 -1
- package/locales/volto.pot +17 -3
- package/locales/zh_CN/LC_MESSAGES/volto.po +16 -2
- package/locales/zh_CN.json +1 -1
- package/locales/zh_Hant.json +1 -1
- package/locales/zh_Hant_HK.json +1 -1
- package/package.json +9 -9
- package/src/components/manage/Blocks/Image/Edit.jsx +5 -1
- package/src/components/manage/Blocks/Search/SearchBlockView.jsx +21 -4
- package/src/components/manage/Blocks/Teaser/Data.jsx +1 -1
- package/src/components/manage/Contents/ContentsItem.jsx +1 -1
- package/src/components/manage/Form/InlineForm.jsx +2 -2
- package/src/components/manage/UniversalLink/UniversalLink.tsx +1 -0
- package/src/components/manage/Widgets/ImageWidget.jsx +159 -33
- package/src/components/manage/Widgets/RegistryImageWidget.test.jsx +3 -2
- package/src/components/manage/Widgets/SelectAutoComplete.jsx +29 -12
- package/src/config/ControlPanels.js +1 -0
- package/src/config/index.js +1 -1
- package/src/express-middleware/devproxy.js +7 -2
- package/src/helpers/Html/Html.jsx +9 -1
- package/src/helpers/Url/bulkFlattenToAppURL.test.ts +122 -0
- package/src/helpers/Url/bulkFlattenToAppURL.ts +24 -0
- package/src/middleware/storeProtectLoadUtils.test.js +3 -3
- package/theme/themes/pastanaga/extras/blocks.less +19 -0
- package/types/components/manage/Widgets/ImageWidget.d.ts +41 -1
- package/types/components/manage/Widgets/RecurrenceWidget/Utils.d.ts +12 -18
- package/types/helpers/Helmet/Helmet.d.ts +1 -1
- package/types/helpers/Url/bulkFlattenToAppURL.d.ts +5 -0
- package/cypress/downloads/downloads.html +0 -0
|
@@ -9,11 +9,14 @@ import { compose } from 'redux';
|
|
|
9
9
|
import { toast } from 'react-toastify';
|
|
10
10
|
import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
|
|
11
11
|
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
|
|
12
|
+
import config from '@plone/volto/registry';
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
flattenToAppURL,
|
|
15
16
|
getBaseUrl,
|
|
16
17
|
isInternalURL,
|
|
18
|
+
normalizeUrl,
|
|
19
|
+
removeProtocol,
|
|
17
20
|
} from '@plone/volto/helpers/Url/Url';
|
|
18
21
|
import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
|
|
19
22
|
import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
|
|
@@ -28,6 +31,9 @@ import clearSVG from '@plone/volto/icons/clear.svg';
|
|
|
28
31
|
import navTreeSVG from '@plone/volto/icons/nav.svg';
|
|
29
32
|
import linkSVG from '@plone/volto/icons/link.svg';
|
|
30
33
|
import uploadSVG from '@plone/volto/icons/upload.svg';
|
|
34
|
+
import Image from '../../theme/Image/Image';
|
|
35
|
+
import { urlValidator } from '@plone/volto/helpers/FormValidation/validators';
|
|
36
|
+
import { searchContent } from '@plone/volto/actions/search/search';
|
|
31
37
|
|
|
32
38
|
const Dropzone = loadable(() => import('react-dropzone'));
|
|
33
39
|
|
|
@@ -70,6 +76,14 @@ const messages = defineMessages({
|
|
|
70
76
|
id: 'imageUploadErrorMessage',
|
|
71
77
|
defaultMessage: 'Please upload an image instead.',
|
|
72
78
|
},
|
|
79
|
+
externalURLsNotAllowed: {
|
|
80
|
+
id: 'externalURLsNotAllowed',
|
|
81
|
+
defaultMessage: 'External URLs are not allowed in this field.',
|
|
82
|
+
},
|
|
83
|
+
internalImageNotFoundErrorMessage: {
|
|
84
|
+
id: 'internalImageNotFoundErrorMessage',
|
|
85
|
+
defaultMessage: 'No image was found in the internal path you provided.',
|
|
86
|
+
},
|
|
73
87
|
});
|
|
74
88
|
|
|
75
89
|
const UnconnectedImageInput = (props) => {
|
|
@@ -89,7 +103,7 @@ const UnconnectedImageInput = (props) => {
|
|
|
89
103
|
placeholderLinkInput = '',
|
|
90
104
|
onSelectItem,
|
|
91
105
|
} = props;
|
|
92
|
-
const imageValue = value?.[0]?.['@id'] || value;
|
|
106
|
+
const imageValue = value?.[0]?.['@id'] || value?.['@id'] || value;
|
|
93
107
|
|
|
94
108
|
const intl = useIntl();
|
|
95
109
|
const linkEditor = useLinkEditor();
|
|
@@ -109,16 +123,34 @@ const UnconnectedImageInput = (props) => {
|
|
|
109
123
|
const imageId = content?.['@id'];
|
|
110
124
|
const image = content?.image;
|
|
111
125
|
let loading = false;
|
|
126
|
+
const isRelationChoice = props.factory === 'Relation Choice';
|
|
112
127
|
|
|
113
128
|
useEffect(() => {
|
|
114
129
|
if (uploading && loading && loaded) {
|
|
115
130
|
setUploading(false);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
if (isRelationChoice) {
|
|
132
|
+
onChange(id, content, {
|
|
133
|
+
image_field: 'image',
|
|
134
|
+
image_scales: { image: [image] },
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
onChange(id, imageId, {
|
|
138
|
+
image_field: 'image',
|
|
139
|
+
image_scales: { image: [image] },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
120
142
|
}
|
|
121
|
-
}, [
|
|
143
|
+
}, [
|
|
144
|
+
loading,
|
|
145
|
+
loaded,
|
|
146
|
+
uploading,
|
|
147
|
+
imageId,
|
|
148
|
+
image,
|
|
149
|
+
id,
|
|
150
|
+
content,
|
|
151
|
+
isRelationChoice,
|
|
152
|
+
onChange,
|
|
153
|
+
]);
|
|
122
154
|
|
|
123
155
|
loading = usePrevious(props.request?.loading);
|
|
124
156
|
|
|
@@ -170,6 +202,83 @@ const UnconnectedImageInput = (props) => {
|
|
|
170
202
|
}, [restrictFileUpload]);
|
|
171
203
|
const onDragLeave = React.useCallback(() => setDragging(false), []);
|
|
172
204
|
|
|
205
|
+
const validateManualLink = React.useCallback(
|
|
206
|
+
(url) => {
|
|
207
|
+
if (!url.startsWith('/')) {
|
|
208
|
+
const error = urlValidator({
|
|
209
|
+
value: url,
|
|
210
|
+
formatMessage: intl.formatMessage,
|
|
211
|
+
});
|
|
212
|
+
// if (error && url !== '') {
|
|
213
|
+
// this.setState({ errors: [error] });
|
|
214
|
+
// } else {
|
|
215
|
+
// this.setState({ errors: [] });
|
|
216
|
+
// }
|
|
217
|
+
return !Boolean(error);
|
|
218
|
+
} else {
|
|
219
|
+
return isInternalURL(url);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
[intl.formatMessage],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const onSubmitURL = React.useCallback(
|
|
226
|
+
(url) => {
|
|
227
|
+
if (validateManualLink(url)) {
|
|
228
|
+
if (isInternalURL(url)) {
|
|
229
|
+
// convert it into an internal on if possible
|
|
230
|
+
props
|
|
231
|
+
.searchContent(
|
|
232
|
+
'/',
|
|
233
|
+
{
|
|
234
|
+
portal_type: config.settings.imageObjects,
|
|
235
|
+
'path.query': flattenToAppURL(url),
|
|
236
|
+
'path.depth': '0',
|
|
237
|
+
sort_on: 'getObjPositionInParent',
|
|
238
|
+
metadata_fields: '_all',
|
|
239
|
+
b_size: 1000,
|
|
240
|
+
},
|
|
241
|
+
`${props.block}-${props.mode}`,
|
|
242
|
+
)
|
|
243
|
+
.then((resp) => {
|
|
244
|
+
if (resp.items?.length > 0) {
|
|
245
|
+
onChange(props.id, resp.items[0], {});
|
|
246
|
+
} else {
|
|
247
|
+
toast.error(
|
|
248
|
+
<Toast
|
|
249
|
+
error
|
|
250
|
+
title={intl.formatMessage(messages.Error)}
|
|
251
|
+
content={intl.formatMessage(
|
|
252
|
+
messages.internalImageNotFoundErrorMessage,
|
|
253
|
+
)}
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
if (isRelationChoice) {
|
|
260
|
+
toast.error(
|
|
261
|
+
<Toast
|
|
262
|
+
error
|
|
263
|
+
title={intl.formatMessage(messages.Error)}
|
|
264
|
+
content={intl.formatMessage(messages.imageUploadErrorMessage)}
|
|
265
|
+
/>,
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
// if it's an external link, we save it as is
|
|
269
|
+
onChange(props.id, [
|
|
270
|
+
{
|
|
271
|
+
'@id': normalizeUrl(url),
|
|
272
|
+
title: removeProtocol(url),
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[validateManualLink, props, intl, isRelationChoice, onChange],
|
|
280
|
+
);
|
|
281
|
+
|
|
173
282
|
return imageValue ? (
|
|
174
283
|
<div
|
|
175
284
|
className="image-upload-widget-image"
|
|
@@ -178,15 +287,20 @@ const UnconnectedImageInput = (props) => {
|
|
|
178
287
|
role="toolbar"
|
|
179
288
|
>
|
|
180
289
|
{selected && <ImageToolbar {...props} />}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
290
|
+
{/* If it's relation choice (preview_image_link) */}
|
|
291
|
+
{isRelationChoice ? (
|
|
292
|
+
<Image item={value} width="fit-content" height="auto" loading="lazy" />
|
|
293
|
+
) : (
|
|
294
|
+
<img
|
|
295
|
+
className={props.className}
|
|
296
|
+
src={
|
|
297
|
+
isInternalURL(imageValue)
|
|
298
|
+
? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
|
|
299
|
+
: imageValue
|
|
300
|
+
}
|
|
301
|
+
alt=""
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
190
304
|
</div>
|
|
191
305
|
) : (
|
|
192
306
|
<div
|
|
@@ -242,15 +356,24 @@ const UnconnectedImageInput = (props) => {
|
|
|
242
356
|
e.preventDefault();
|
|
243
357
|
openObjectBrowser({
|
|
244
358
|
mode: objectBrowserPickerType,
|
|
245
|
-
onSelectItem:
|
|
246
|
-
?
|
|
247
|
-
|
|
248
|
-
onChange(props.id,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
359
|
+
onSelectItem: isRelationChoice
|
|
360
|
+
? (url, item) => {
|
|
361
|
+
// we save the whole item if it's a relation choice
|
|
362
|
+
onChange(props.id, item);
|
|
363
|
+
}
|
|
364
|
+
: onSelectItem
|
|
365
|
+
? onSelectItem
|
|
366
|
+
: // else we save the url along with the image field and scales
|
|
367
|
+
(
|
|
368
|
+
url,
|
|
369
|
+
{ title, image_field, image_scales },
|
|
370
|
+
) => {
|
|
371
|
+
onChange(props.id, flattenToAppURL(url), {
|
|
372
|
+
title,
|
|
373
|
+
image_field,
|
|
374
|
+
image_scales,
|
|
375
|
+
});
|
|
376
|
+
},
|
|
254
377
|
currentPath: contextUrl,
|
|
255
378
|
});
|
|
256
379
|
}}
|
|
@@ -311,13 +434,14 @@ const UnconnectedImageInput = (props) => {
|
|
|
311
434
|
intl.formatMessage(messages.linkAnImage)
|
|
312
435
|
}
|
|
313
436
|
objectBrowserPickerType={objectBrowserPickerType}
|
|
314
|
-
onChange={(_, e) =>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
437
|
+
onChange={(_, e) => {
|
|
438
|
+
onSubmitURL(e);
|
|
439
|
+
// onChange(
|
|
440
|
+
// props.id,
|
|
441
|
+
// isInternalURL(e) ? flattenToAppURL(e) : e,
|
|
442
|
+
// {},
|
|
443
|
+
// );
|
|
444
|
+
}}
|
|
321
445
|
id={id}
|
|
322
446
|
/>
|
|
323
447
|
)}
|
|
@@ -331,6 +455,8 @@ const UnconnectedImageInput = (props) => {
|
|
|
331
455
|
};
|
|
332
456
|
|
|
333
457
|
export const ImageInput = compose(
|
|
458
|
+
// This HOC goes first because it injects block in case that it's not present (not a block, but a DX field)
|
|
459
|
+
withObjectBrowser,
|
|
334
460
|
connect(
|
|
335
461
|
(state, ownProps) => {
|
|
336
462
|
const requestId = `image-upload-${ownProps.id}`;
|
|
@@ -339,9 +465,9 @@ export const ImageInput = compose(
|
|
|
339
465
|
content: state.content.subrequests[ownProps.block || requestId]?.data,
|
|
340
466
|
};
|
|
341
467
|
},
|
|
342
|
-
{ createContent },
|
|
468
|
+
{ createContent, searchContent },
|
|
343
469
|
),
|
|
344
|
-
)(
|
|
470
|
+
)(UnconnectedImageInput);
|
|
345
471
|
|
|
346
472
|
const ImageUploadWidget = (props) => {
|
|
347
473
|
const { fieldSet, id, title } = props;
|
|
@@ -82,10 +82,11 @@ describe('RegistryImageWidget', () => {
|
|
|
82
82
|
const dropzone = container.querySelector('.file-widget-dropzone');
|
|
83
83
|
const preview = container.querySelector('.image-preview');
|
|
84
84
|
const filename = container.querySelector('.field-file-name');
|
|
85
|
+
const img = container.querySelector('img[src*="logo"]');
|
|
85
86
|
|
|
86
|
-
return dropzone && preview && filename;
|
|
87
|
+
return dropzone && preview && filename && img && img.complete;
|
|
87
88
|
},
|
|
88
|
-
{ timeout:
|
|
89
|
+
{ timeout: 2000 },
|
|
89
90
|
);
|
|
90
91
|
|
|
91
92
|
expect(container).toMatchSnapshot();
|
|
@@ -105,6 +105,7 @@ class SelectAutoComplete extends Component {
|
|
|
105
105
|
error: [],
|
|
106
106
|
choices: [],
|
|
107
107
|
value: null,
|
|
108
|
+
isMulti: true,
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
/**
|
|
@@ -127,10 +128,16 @@ class SelectAutoComplete extends Component {
|
|
|
127
128
|
componentDidMount() {
|
|
128
129
|
const { id, lang, value, choices } = this.props;
|
|
129
130
|
if (value && value?.length > 0) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
let tokensQuery;
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
tokensQuery = convertValueToVocabQuery(
|
|
134
|
+
normalizeValue(choices, [value], this.props.intl),
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
tokensQuery = convertValueToVocabQuery(
|
|
138
|
+
normalizeValue(choices, value, this.props.intl),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
134
141
|
this.props.getVocabularyTokenTitle({
|
|
135
142
|
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
136
143
|
subrequest: `widget-${id}-${lang}`,
|
|
@@ -160,13 +167,22 @@ class SelectAutoComplete extends Component {
|
|
|
160
167
|
* @returns {undefined}
|
|
161
168
|
*/
|
|
162
169
|
handleChange(selectedOption) {
|
|
163
|
-
|
|
164
|
-
this.props.id,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
if (!Array.isArray(selectedOption)) {
|
|
171
|
+
this.props.onChange(this.props.id, selectedOption?.value || null);
|
|
172
|
+
if (selectedOption) {
|
|
173
|
+
this.setState((state) => ({
|
|
174
|
+
termsPairsCache: [...state.termsPairsCache, selectedOption],
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
this.props.onChange(
|
|
179
|
+
this.props.id,
|
|
180
|
+
selectedOption ? selectedOption.map((item) => item.value) : null,
|
|
181
|
+
);
|
|
182
|
+
this.setState((state) => ({
|
|
183
|
+
termsPairsCache: [...state.termsPairsCache, ...selectedOption],
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
timeoutRef = React.createRef();
|
|
@@ -251,7 +267,8 @@ class SelectAutoComplete extends Component {
|
|
|
251
267
|
this.props.intl.formatMessage(messages.select)
|
|
252
268
|
}
|
|
253
269
|
onChange={this.handleChange}
|
|
254
|
-
isMulti
|
|
270
|
+
isMulti={this.props.isMulti}
|
|
271
|
+
isClearable={!this.props.isMulti}
|
|
255
272
|
/>
|
|
256
273
|
</FormFieldWrapper>
|
|
257
274
|
);
|
package/src/config/index.js
CHANGED
|
@@ -10,8 +10,13 @@ import querystring from 'querystring';
|
|
|
10
10
|
import { parse as parseUrl } from 'url';
|
|
11
11
|
|
|
12
12
|
const filter = function (pathname, req) {
|
|
13
|
-
//
|
|
14
|
-
|
|
13
|
+
// Check if pathname is defined, there are some corner cases that pathname is null
|
|
14
|
+
if (pathname) {
|
|
15
|
+
// This is the proxy to the API in case the accept header is 'application/json'
|
|
16
|
+
return config.settings.devProxyToApiPath && pathname.startsWith('/++api++');
|
|
17
|
+
} else {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
15
20
|
};
|
|
16
21
|
|
|
17
22
|
let _env = null;
|
|
@@ -11,6 +11,7 @@ import join from 'lodash/join';
|
|
|
11
11
|
import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
|
|
12
12
|
import { runtimeConfig } from '@plone/volto/runtime_config';
|
|
13
13
|
import config from '@plone/volto/registry';
|
|
14
|
+
import { bulkFlattenToAppURL } from '../Url/bulkFlattenToAppURL';
|
|
14
15
|
|
|
15
16
|
const CRITICAL_CSS_TEMPLATE = `function alter() {
|
|
16
17
|
document.querySelectorAll("head link[rel='prefetch']").forEach(function(el) { el.rel = 'stylesheet'});
|
|
@@ -189,7 +190,14 @@ class Html extends Component {
|
|
|
189
190
|
<script
|
|
190
191
|
dangerouslySetInnerHTML={{
|
|
191
192
|
__html: `window.__data=${serialize(
|
|
192
|
-
loadReducers(
|
|
193
|
+
loadReducers({
|
|
194
|
+
...store.getState(),
|
|
195
|
+
// Flatten the content URLs in initial request in SSR
|
|
196
|
+
// it normalizes the URLs in case the INTERNAL_API_PATH is set
|
|
197
|
+
// and prevents unwanted leaks of INTERNAL_API_PATH in the client
|
|
198
|
+
// (only in the first request)
|
|
199
|
+
content: bulkFlattenToAppURL(store.getState().content),
|
|
200
|
+
}),
|
|
193
201
|
)};`,
|
|
194
202
|
}}
|
|
195
203
|
charSet="UTF-8"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import config from '@plone/registry';
|
|
3
|
+
import { bulkFlattenToAppURL } from './bulkFlattenToAppURL';
|
|
4
|
+
|
|
5
|
+
const API_PATH = 'http://example.com/api';
|
|
6
|
+
const INTERNAL_API_PATH = 'http://backend/api';
|
|
7
|
+
|
|
8
|
+
describe('bulkFlattenToAppURL', () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
config.settings = {};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should flatten URLs correctly with API_PATH', () => {
|
|
14
|
+
config.settings.apiPath = API_PATH;
|
|
15
|
+
|
|
16
|
+
const input = {
|
|
17
|
+
'@id': `${API_PATH}/path/to/resource`,
|
|
18
|
+
anotherField: `${API_PATH}/another/path`,
|
|
19
|
+
nested: {
|
|
20
|
+
'@id': `${API_PATH}/nested/resource`,
|
|
21
|
+
anotherNestedField: `${API_PATH}/nested/another/path`,
|
|
22
|
+
},
|
|
23
|
+
arrayField: [
|
|
24
|
+
{
|
|
25
|
+
'@id': `${API_PATH}/array/resource/1`,
|
|
26
|
+
anotherArrayField: `${API_PATH}/array/another/path/1`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
'@id': `${API_PATH}/array/resource/2`,
|
|
30
|
+
anotherArrayField: `${API_PATH}/array/another/path/2`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
stringField: 'This is a string',
|
|
34
|
+
numberField: 42,
|
|
35
|
+
booleanField: true,
|
|
36
|
+
nullField: null,
|
|
37
|
+
undefinedField: undefined,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const expectedOutput = {
|
|
41
|
+
'@id': '/path/to/resource',
|
|
42
|
+
anotherField: '/another/path',
|
|
43
|
+
nested: {
|
|
44
|
+
'@id': '/nested/resource',
|
|
45
|
+
anotherNestedField: '/nested/another/path',
|
|
46
|
+
},
|
|
47
|
+
arrayField: [
|
|
48
|
+
{
|
|
49
|
+
'@id': '/array/resource/1',
|
|
50
|
+
anotherArrayField: '/array/another/path/1',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
'@id': '/array/resource/2',
|
|
54
|
+
anotherArrayField: '/array/another/path/2',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
stringField: 'This is a string',
|
|
58
|
+
numberField: 42,
|
|
59
|
+
booleanField: true,
|
|
60
|
+
nullField: null,
|
|
61
|
+
undefinedField: undefined,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = bulkFlattenToAppURL(input as any);
|
|
65
|
+
expect(result).toEqual(expectedOutput);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should flatten URLs correctly with INTERNAL_API_PATH', () => {
|
|
69
|
+
config.settings.internalApiPath = INTERNAL_API_PATH;
|
|
70
|
+
|
|
71
|
+
const input = {
|
|
72
|
+
'@id': `${INTERNAL_API_PATH}/path/to/resource`,
|
|
73
|
+
anotherField: `${INTERNAL_API_PATH}/another/path`,
|
|
74
|
+
nested: {
|
|
75
|
+
'@id': `${INTERNAL_API_PATH}/nested/resource`,
|
|
76
|
+
anotherNestedField: `${INTERNAL_API_PATH}/nested/another/path`,
|
|
77
|
+
},
|
|
78
|
+
arrayField: [
|
|
79
|
+
{
|
|
80
|
+
'@id': `${INTERNAL_API_PATH}/array/resource/1`,
|
|
81
|
+
anotherArrayField: `${INTERNAL_API_PATH}/array/another/path/1`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
'@id': `${INTERNAL_API_PATH}/array/resource/2`,
|
|
85
|
+
anotherArrayField: `${INTERNAL_API_PATH}/array/another/path/2`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
stringField: 'This is a string',
|
|
89
|
+
numberField: 42,
|
|
90
|
+
booleanField: true,
|
|
91
|
+
nullField: null,
|
|
92
|
+
undefinedField: undefined,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const expectedOutput = {
|
|
96
|
+
'@id': '/path/to/resource',
|
|
97
|
+
anotherField: '/another/path',
|
|
98
|
+
nested: {
|
|
99
|
+
'@id': '/nested/resource',
|
|
100
|
+
anotherNestedField: '/nested/another/path',
|
|
101
|
+
},
|
|
102
|
+
arrayField: [
|
|
103
|
+
{
|
|
104
|
+
'@id': '/array/resource/1',
|
|
105
|
+
anotherArrayField: '/array/another/path/1',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
'@id': '/array/resource/2',
|
|
109
|
+
anotherArrayField: '/array/another/path/2',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
stringField: 'This is a string',
|
|
113
|
+
numberField: 42,
|
|
114
|
+
booleanField: true,
|
|
115
|
+
nullField: null,
|
|
116
|
+
undefinedField: undefined,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = bulkFlattenToAppURL(input as any);
|
|
120
|
+
expect(result).toEqual(expectedOutput);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import config from '@plone/registry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The definitive flattenToAppURL function
|
|
5
|
+
* Flattens all the URLs in the response to the current app URL (or internalApiPath if set).
|
|
6
|
+
*/
|
|
7
|
+
export function bulkFlattenToAppURL<T>(data: T) {
|
|
8
|
+
// Convert data to string to perform replacements
|
|
9
|
+
let stringData = JSON.stringify(data);
|
|
10
|
+
|
|
11
|
+
// Replace all occurrences of backend URLs
|
|
12
|
+
stringData = stringData.replaceAll(`${config.settings.apiPath}/`, '/');
|
|
13
|
+
stringData = stringData.replaceAll(config.settings.apiPath, '/');
|
|
14
|
+
if (config.settings.internalApiPath) {
|
|
15
|
+
stringData = stringData.replaceAll(
|
|
16
|
+
`${config.settings.internalApiPath}/`,
|
|
17
|
+
'/',
|
|
18
|
+
);
|
|
19
|
+
stringData = stringData.replaceAll(config.settings.internalApiPath, '/');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Parse back to object
|
|
23
|
+
return JSON.parse(stringData) as T;
|
|
24
|
+
}
|
|
@@ -445,9 +445,9 @@ describe('storeProtectLoadUtils', () => {
|
|
|
445
445
|
isCounting: false,
|
|
446
446
|
});
|
|
447
447
|
};
|
|
448
|
-
test('pending', expectPass({ type: 'ANY_PENDING' }
|
|
449
|
-
test('success', expectPass({ type: 'ANY_SUCCESS' }
|
|
450
|
-
test('failure', expectPass({ type: 'ANY_FAIL' }
|
|
448
|
+
test('pending', expectPass({ type: 'ANY_PENDING' }, 2));
|
|
449
|
+
test('success', expectPass({ type: 'ANY_SUCCESS' }, 2));
|
|
450
|
+
test('failure', expectPass({ type: 'ANY_FAIL' }, 2));
|
|
451
451
|
});
|
|
452
452
|
describe('counting', () => {
|
|
453
453
|
const expectCount = (action, from, to) => () => {
|
|
@@ -434,10 +434,18 @@ body.has-toolbar.has-sidebar-collapsed .ui.wrapper > .ui.inner.block.full {
|
|
|
434
434
|
top: 1em;
|
|
435
435
|
right: 1em;
|
|
436
436
|
|
|
437
|
+
.sidebar-container & .ui.icon.button:hover {
|
|
438
|
+
background: transparent !important;
|
|
439
|
+
}
|
|
440
|
+
|
|
437
441
|
.icon.circled {
|
|
438
442
|
border: 1px solid #e4e8ec;
|
|
439
443
|
background: #f3f5f7;
|
|
440
444
|
box-shadow: inset 0 0 0 0.14285714rem transparent !important;
|
|
445
|
+
|
|
446
|
+
&:hover {
|
|
447
|
+
background-color: #e4e8ec;
|
|
448
|
+
}
|
|
441
449
|
}
|
|
442
450
|
}
|
|
443
451
|
|
|
@@ -990,6 +998,17 @@ div.image-upload-widget-image {
|
|
|
990
998
|
}
|
|
991
999
|
}
|
|
992
1000
|
|
|
1001
|
+
// In the sidebar, the height is forced on all .ui.input T_T
|
|
1002
|
+
.ui.form .ui.input.input-anchorlink-theme,
|
|
1003
|
+
.ui.form
|
|
1004
|
+
.inline.field
|
|
1005
|
+
.link-form-container
|
|
1006
|
+
.wrapper
|
|
1007
|
+
.ui.input.input-anchorlink-theme
|
|
1008
|
+
input {
|
|
1009
|
+
height: 34px;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
993
1012
|
.block.image.align.left,
|
|
994
1013
|
.block.image.align.right {
|
|
995
1014
|
img {
|
|
@@ -5,6 +5,46 @@ export function ImageToolbar({ className, data, id, onChange, selected }: {
|
|
|
5
5
|
onChange: any;
|
|
6
6
|
selected: any;
|
|
7
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
export const ImageInput:
|
|
8
|
+
export const ImageInput: {
|
|
9
|
+
new (): {
|
|
10
|
+
state: {
|
|
11
|
+
isObjectBrowserOpen: boolean;
|
|
12
|
+
};
|
|
13
|
+
openObjectBrowser: ({ mode, onSelectItem, dataName, overlay, propDataName, searchableTypes, selectableTypes, maximumSelectionSize, currentPath, }?: {
|
|
14
|
+
mode: string;
|
|
15
|
+
dataName: string;
|
|
16
|
+
onSelectItem: string;
|
|
17
|
+
overlay: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
closeObjectBrowser: () => void;
|
|
20
|
+
render(): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
context: unknown;
|
|
22
|
+
setState<K extends string | number | symbol>(state: any, callback?: () => void): void;
|
|
23
|
+
forceUpdate(callback?: () => void): void;
|
|
24
|
+
readonly props: Readonly<any>;
|
|
25
|
+
refs: {
|
|
26
|
+
[key: string]: React.ReactInstance;
|
|
27
|
+
};
|
|
28
|
+
componentDidMount?(): void;
|
|
29
|
+
shouldComponentUpdate?(nextProps: Readonly<any>, nextState: Readonly<any>, nextContext: any): boolean;
|
|
30
|
+
componentWillUnmount?(): void;
|
|
31
|
+
componentDidCatch?(error: Error, errorInfo: React.ErrorInfo): void;
|
|
32
|
+
getSnapshotBeforeUpdate?(prevProps: Readonly<any>, prevState: Readonly<any>): any;
|
|
33
|
+
componentDidUpdate?(prevProps: Readonly<any>, prevState: Readonly<any>, snapshot?: any): void;
|
|
34
|
+
componentWillMount?(): void;
|
|
35
|
+
UNSAFE_componentWillMount?(): void;
|
|
36
|
+
componentWillReceiveProps?(nextProps: Readonly<any>, nextContext: any): void;
|
|
37
|
+
UNSAFE_componentWillReceiveProps?(nextProps: Readonly<any>, nextContext: any): void;
|
|
38
|
+
componentWillUpdate?(nextProps: Readonly<any>, nextState: Readonly<any>, nextContext: any): void;
|
|
39
|
+
UNSAFE_componentWillUpdate?(nextProps: Readonly<any>, nextState: Readonly<any>, nextContext: any): void;
|
|
40
|
+
};
|
|
41
|
+
defaultProps: {
|
|
42
|
+
onChangeBlock: () => void;
|
|
43
|
+
data: {};
|
|
44
|
+
block: string;
|
|
45
|
+
};
|
|
46
|
+
contextType?: React.Context<any> | undefined;
|
|
47
|
+
};
|
|
9
48
|
export default ImageUploadWidget;
|
|
49
|
+
import React from 'react';
|
|
10
50
|
declare function ImageUploadWidget(props: any): import("react/jsx-runtime").JSX.Element;
|