@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +0 -2
  3. package/cypress/support/commands.js +13 -6
  4. package/locales/af.json +1 -1
  5. package/locales/ar.json +1 -1
  6. package/locales/bg.json +1 -1
  7. package/locales/bn.json +1 -1
  8. package/locales/ca/LC_MESSAGES/volto.po +16 -2
  9. package/locales/ca.json +1 -1
  10. package/locales/cs.json +1 -1
  11. package/locales/cy.json +1 -1
  12. package/locales/da.json +1 -1
  13. package/locales/de/LC_MESSAGES/volto.po +19 -5
  14. package/locales/de.json +1 -1
  15. package/locales/el.json +1 -1
  16. package/locales/en/LC_MESSAGES/volto.po +16 -2
  17. package/locales/en.json +1 -1
  18. package/locales/en_AU.json +1 -1
  19. package/locales/en_GB.json +1 -1
  20. package/locales/eo.json +1 -1
  21. package/locales/es/LC_MESSAGES/volto.po +16 -2
  22. package/locales/es.json +1 -1
  23. package/locales/et.json +1 -1
  24. package/locales/eu/LC_MESSAGES/volto.po +16 -2
  25. package/locales/eu.json +1 -1
  26. package/locales/fa.json +1 -1
  27. package/locales/fi/LC_MESSAGES/volto.po +16 -2
  28. package/locales/fi.json +1 -1
  29. package/locales/fr/LC_MESSAGES/volto.po +16 -2
  30. package/locales/fr.json +1 -1
  31. package/locales/fu.json +1 -1
  32. package/locales/gl.json +1 -1
  33. package/locales/he.json +1 -1
  34. package/locales/hi/LC_MESSAGES/volto.po +16 -2
  35. package/locales/hi.json +1 -1
  36. package/locales/hr.json +1 -1
  37. package/locales/hu.json +1 -1
  38. package/locales/hy.json +1 -1
  39. package/locales/id.json +1 -1
  40. package/locales/it/LC_MESSAGES/volto.po +16 -2
  41. package/locales/it.json +1 -1
  42. package/locales/ja/LC_MESSAGES/volto.po +16 -2
  43. package/locales/ja.json +1 -1
  44. package/locales/ka.json +1 -1
  45. package/locales/kn.json +1 -1
  46. package/locales/ko.json +1 -1
  47. package/locales/lt.json +1 -1
  48. package/locales/lv.json +1 -1
  49. package/locales/mi.json +1 -1
  50. package/locales/mk.json +1 -1
  51. package/locales/my.json +1 -1
  52. package/locales/nb_NO.json +1 -1
  53. package/locales/nl/LC_MESSAGES/volto.po +16 -2
  54. package/locales/nl.json +1 -1
  55. package/locales/nn.json +1 -1
  56. package/locales/pl.json +1 -1
  57. package/locales/pt/LC_MESSAGES/volto.po +16 -2
  58. package/locales/pt.json +1 -1
  59. package/locales/pt_BR/LC_MESSAGES/volto.po +16 -2
  60. package/locales/pt_BR.json +1 -1
  61. package/locales/rm.json +1 -1
  62. package/locales/ro/LC_MESSAGES/volto.po +16 -2
  63. package/locales/ro.json +1 -1
  64. package/locales/ru/LC_MESSAGES/volto.po +16 -2
  65. package/locales/ru.json +1 -1
  66. package/locales/sk.json +1 -1
  67. package/locales/sl.json +1 -1
  68. package/locales/sm.json +1 -1
  69. package/locales/sq.json +1 -1
  70. package/locales/sr.json +1 -1
  71. package/locales/sr@cyrl.json +1 -1
  72. package/locales/sr@latn.json +1 -1
  73. package/locales/sv.json +1 -1
  74. package/locales/ta.json +1 -1
  75. package/locales/te.json +1 -1
  76. package/locales/th.json +1 -1
  77. package/locales/to.json +1 -1
  78. package/locales/tr.json +1 -1
  79. package/locales/uk.json +1 -1
  80. package/locales/vi.json +1 -1
  81. package/locales/volto.pot +17 -3
  82. package/locales/zh_CN/LC_MESSAGES/volto.po +16 -2
  83. package/locales/zh_CN.json +1 -1
  84. package/locales/zh_Hant.json +1 -1
  85. package/locales/zh_Hant_HK.json +1 -1
  86. package/package.json +9 -9
  87. package/src/components/manage/Blocks/Image/Edit.jsx +5 -1
  88. package/src/components/manage/Blocks/Search/SearchBlockView.jsx +21 -4
  89. package/src/components/manage/Blocks/Teaser/Data.jsx +1 -1
  90. package/src/components/manage/Contents/ContentsItem.jsx +1 -1
  91. package/src/components/manage/Form/InlineForm.jsx +2 -2
  92. package/src/components/manage/UniversalLink/UniversalLink.tsx +1 -0
  93. package/src/components/manage/Widgets/ImageWidget.jsx +159 -33
  94. package/src/components/manage/Widgets/RegistryImageWidget.test.jsx +3 -2
  95. package/src/components/manage/Widgets/SelectAutoComplete.jsx +29 -12
  96. package/src/config/ControlPanels.js +1 -0
  97. package/src/config/index.js +1 -1
  98. package/src/express-middleware/devproxy.js +7 -2
  99. package/src/helpers/Html/Html.jsx +9 -1
  100. package/src/helpers/Url/bulkFlattenToAppURL.test.ts +122 -0
  101. package/src/helpers/Url/bulkFlattenToAppURL.ts +24 -0
  102. package/src/middleware/storeProtectLoadUtils.test.js +3 -3
  103. package/theme/themes/pastanaga/extras/blocks.less +19 -0
  104. package/types/components/manage/Widgets/ImageWidget.d.ts +41 -1
  105. package/types/components/manage/Widgets/RecurrenceWidget/Utils.d.ts +12 -18
  106. package/types/helpers/Helmet/Helmet.d.ts +1 -1
  107. package/types/helpers/Url/bulkFlattenToAppURL.d.ts +5 -0
  108. 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
- onChange(id, imageId, {
117
- image_field: 'image',
118
- image_scales: { image: [image] },
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
- }, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
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
- <img
182
- className={props.className}
183
- src={
184
- isInternalURL(imageValue)
185
- ? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
186
- : imageValue
187
- }
188
- alt=""
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: onSelectItem
246
- ? onSelectItem
247
- : (url, { title, image_field, image_scales }) => {
248
- onChange(props.id, flattenToAppURL(url), {
249
- title,
250
- image_field,
251
- image_scales,
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
- onChange(
316
- props.id,
317
- isInternalURL(e) ? flattenToAppURL(e) : e,
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
- )(withObjectBrowser(UnconnectedImageInput));
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: 1000 },
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
- const tokensQuery = convertValueToVocabQuery(
131
- normalizeValue(choices, value, this.props.intl),
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
- this.props.onChange(
164
- this.props.id,
165
- selectedOption ? selectedOption.map((item) => item.value) : null,
166
- );
167
- this.setState((state) => ({
168
- termsPairsCache: [...state.termsPairsCache, ...selectedOption],
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
  );
@@ -77,6 +77,7 @@ export const unwantedControlPanelsFields = {
77
77
  'exposeDCMetaTags',
78
78
  'enable_sitemap',
79
79
  'webstats_js',
80
+ 'webstats_head_js',
80
81
  ],
81
82
  editing: ['available_editors', 'default_editor', 'ext_editor'],
82
83
  imaging: [
@@ -141,7 +141,7 @@ let config = {
141
141
  serverConfig,
142
142
  storeExtenders: [],
143
143
  showTags: true,
144
- showRelatedItems: false,
144
+ showRelatedItems: true,
145
145
  controlpanels: [],
146
146
  controlPanelsIcons,
147
147
  filterControlPanels,
@@ -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
- // This is the proxy to the API in case the accept header is 'application/json'
14
- return config.settings.devProxyToApiPath && pathname.startsWith('/++api++');
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(store.getState()),
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' }), 2);
449
- test('success', expectPass({ type: 'ANY_SUCCESS' }), 2);
450
- test('failure', expectPass({ type: 'ANY_FAIL' }), 2);
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: any;
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;