@plone/volto 18.6.0 → 18.8.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/cypress/support/commands.js +22 -0
  3. package/locales/nl/LC_MESSAGES/volto.po +9 -9
  4. package/locales/nl.json +1 -1
  5. package/package.json +4 -4
  6. package/src/components/manage/Blocks/Block/BlocksForm.jsx +6 -4
  7. package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +1 -0
  8. package/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx +18 -14
  9. package/src/components/manage/Controlpanels/Relations/Relations.jsx +48 -46
  10. package/src/components/manage/Toolbar/More.jsx +113 -117
  11. package/src/components/manage/Toolbar/More.test.jsx +0 -24
  12. package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +15 -2
  13. package/src/components/manage/Widgets/RegistryImageWidget.jsx +15 -16
  14. package/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx +53 -56
  15. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +23 -0
  16. package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +135 -0
  17. package/src/components/theme/Image/Image.jsx +8 -1
  18. package/src/components/theme/View/View.jsx +2 -0
  19. package/src/config/ControlPanels.js +0 -1
  20. package/src/config/index.js +0 -1
  21. package/src/express-middleware/robotstxt.js +4 -5
  22. package/src/helpers/Api/Api.js +1 -1
  23. package/src/helpers/Blocks/Blocks.js +26 -4
  24. package/src/helpers/Blocks/Blocks.test.js +46 -0
  25. package/src/helpers/FormValidation/validators.ts +3 -1
  26. package/src/helpers/Robots/Robots.js +12 -31
  27. package/src/hooks/clipboard/useClipboard.js +7 -3
  28. package/types/components/manage/Widgets/ObjectBrowserWidget.d.ts +1 -0
  29. package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.d.ts +1 -0
  30. package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.d.ts +1 -0
  31. package/types/helpers/Api/Api.d.ts +7 -0
  32. package/types/helpers/Blocks/Blocks.d.ts +15 -5
  33. package/public/robots.txt +0 -2
@@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl';
7
7
  import Toast from '@plone/volto/components/manage/Toast/Toast';
8
8
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
9
9
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
10
- import config from '@plone/volto/registry';
11
10
  import useDeepCompareEffect from 'use-deep-compare-effect';
12
11
 
13
12
  const messages = defineMessages({
@@ -39,48 +38,28 @@ const WorkingCopyToastsFactory = (props) => {
39
38
  };
40
39
 
41
40
  useDeepCompareEffect(() => {
42
- if (content && config.settings.hasWorkingCopySupport) {
43
- if (working_copy) {
44
- let toastMessage, toastTitle;
45
- if (content.working_copy_of) {
46
- // I'm a working copy
47
- toastMessage = messages.thisIsAWorkingCopyOf;
48
- toastTitle = (
49
- <Link to={flattenToAppURL(content.working_copy_of['@id'])}>
50
- {content.working_copy_of?.title}
51
- </Link>
52
- );
53
- } else {
54
- // I'm a baseline
55
- toastMessage = messages.workingCopyIs;
56
- toastTitle = (
57
- <Link to={flattenToAppURL(working_copy['@id'])}>
58
- {working_copy?.title}
59
- </Link>
60
- );
61
- }
62
- if (toast.isActive('workingcopyinfo')) {
63
- toast.update('workingcopyinfo', {
64
- render: (
65
- <Toast
66
- info
67
- title={intl.formatMessage(toastMessage, {
68
- title: toastTitle,
69
- })}
70
- content={intl.formatMessage(messages.workingCopyCreatedBy, {
71
- creator: working_copy?.creator_name,
72
- date: (
73
- <FormattedDate
74
- date={working_copy?.created}
75
- format={dateOptions}
76
- />
77
- ),
78
- })}
79
- />
80
- ),
81
- });
82
- } else {
83
- toast.info(
41
+ if (working_copy) {
42
+ let toastMessage, toastTitle;
43
+ if (content.working_copy_of) {
44
+ // I'm a working copy
45
+ toastMessage = messages.thisIsAWorkingCopyOf;
46
+ toastTitle = (
47
+ <Link to={flattenToAppURL(content.working_copy_of['@id'])}>
48
+ {content.working_copy_of?.title}
49
+ </Link>
50
+ );
51
+ } else {
52
+ // I'm a baseline
53
+ toastMessage = messages.workingCopyIs;
54
+ toastTitle = (
55
+ <Link to={flattenToAppURL(working_copy['@id'])}>
56
+ {working_copy?.title}
57
+ </Link>
58
+ );
59
+ }
60
+ if (toast.isActive('workingcopyinfo')) {
61
+ toast.update('workingcopyinfo', {
62
+ render: (
84
63
  <Toast
85
64
  info
86
65
  title={intl.formatMessage(toastMessage, {
@@ -95,20 +74,38 @@ const WorkingCopyToastsFactory = (props) => {
95
74
  />
96
75
  ),
97
76
  })}
98
- />,
99
- {
100
- toastId: 'workingcopyinfo',
101
- autoClose: false,
102
- closeButton: false,
103
- transition: null,
104
- },
105
- );
106
- }
77
+ />
78
+ ),
79
+ });
80
+ } else {
81
+ toast.info(
82
+ <Toast
83
+ info
84
+ title={intl.formatMessage(toastMessage, {
85
+ title: toastTitle,
86
+ })}
87
+ content={intl.formatMessage(messages.workingCopyCreatedBy, {
88
+ creator: working_copy?.creator_name,
89
+ date: (
90
+ <FormattedDate
91
+ date={working_copy?.created}
92
+ format={dateOptions}
93
+ />
94
+ ),
95
+ })}
96
+ />,
97
+ {
98
+ toastId: 'workingcopyinfo',
99
+ autoClose: false,
100
+ closeButton: false,
101
+ transition: null,
102
+ },
103
+ );
107
104
  }
108
- if (!working_copy) {
109
- if (toast.isActive('workingcopyinfo')) {
110
- toast.dismiss('workingcopyinfo');
111
- }
105
+ }
106
+ if (!working_copy) {
107
+ if (toast.isActive('workingcopyinfo')) {
108
+ toast.dismiss('workingcopyinfo');
112
109
  }
113
110
  }
114
111
  }, [pathname, content, title, working_copy, intl, lang, dateOptions]);
@@ -0,0 +1,23 @@
1
+ import config from '@plone/volto/registry';
2
+ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
3
+
4
+ const AlternateHrefLangs = (props) => {
5
+ const { content } = props;
6
+ return (
7
+ <Helmet>
8
+ {config.settings.isMultilingual &&
9
+ content['@components']?.translations?.items?.map((item, key) => {
10
+ return (
11
+ <link
12
+ key={key}
13
+ rel="alternate"
14
+ hrefLang={item.language}
15
+ href={item['@id']}
16
+ />
17
+ );
18
+ })}
19
+ </Helmet>
20
+ );
21
+ };
22
+
23
+ export { AlternateHrefLangs };
@@ -0,0 +1,135 @@
1
+ import React from 'react';
2
+ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
3
+
4
+ import renderer from 'react-test-renderer';
5
+ import configureStore from 'redux-mock-store';
6
+ import { Provider } from 'react-intl-redux';
7
+ import config from '@plone/volto/registry';
8
+
9
+ import { AlternateHrefLangs } from './AlternateHrefLangs';
10
+
11
+ const mockStore = configureStore();
12
+
13
+ describe('AlternateHrefLangs', () => {
14
+ beforeEach(() => {});
15
+ it('non multilingual site, renders nothing', () => {
16
+ config.settings.isMultilingual = false;
17
+ const content = {
18
+ '@id': '/',
19
+ '@components': {},
20
+ };
21
+ const store = mockStore({
22
+ intl: {
23
+ locale: 'en',
24
+ messages: {},
25
+ },
26
+ });
27
+ // We need to force the component rendering
28
+ // to fill the Helmet
29
+ renderer.create(
30
+ <Provider store={store}>
31
+ <AlternateHrefLangs content={content} />
32
+ </Provider>,
33
+ );
34
+
35
+ const helmetLinks = Helmet.peek().linkTags;
36
+ expect(helmetLinks.length).toBe(0);
37
+ });
38
+ it('multilingual site, with some translations', () => {
39
+ config.settings.isMultilingual = true;
40
+ config.settings.supportedLanguages = ['en', 'es', 'eu'];
41
+
42
+ const content = {
43
+ '@components': {
44
+ translations: {
45
+ items: [
46
+ { '@id': '/en', language: 'en' },
47
+ { '@id': '/es', language: 'es' },
48
+ ],
49
+ },
50
+ },
51
+ };
52
+
53
+ const store = mockStore({
54
+ intl: {
55
+ locale: 'en',
56
+ messages: {},
57
+ },
58
+ });
59
+
60
+ // We need to force the component rendering
61
+ // to fill the Helmet
62
+ renderer.create(
63
+ <Provider store={store}>
64
+ <>
65
+ <AlternateHrefLangs content={content} />
66
+ </>
67
+ </Provider>,
68
+ );
69
+ const helmetLinks = Helmet.peek().linkTags;
70
+
71
+ expect(helmetLinks.length).toBe(2);
72
+
73
+ expect(helmetLinks).toContainEqual({
74
+ rel: 'alternate',
75
+ href: '/es',
76
+ hrefLang: 'es',
77
+ });
78
+ expect(helmetLinks).toContainEqual({
79
+ rel: 'alternate',
80
+ href: '/en',
81
+ hrefLang: 'en',
82
+ });
83
+ });
84
+ it('multilingual site, with all available translations', () => {
85
+ config.settings.isMultilingual = true;
86
+ config.settings.supportedLanguages = ['en', 'es', 'eu'];
87
+ const store = mockStore({
88
+ intl: {
89
+ locale: 'en',
90
+ messages: {},
91
+ },
92
+ });
93
+
94
+ const content = {
95
+ '@components': {
96
+ translations: {
97
+ items: [
98
+ { '@id': '/en', language: 'en' },
99
+ { '@id': '/eu', language: 'eu' },
100
+ { '@id': '/es', language: 'es' },
101
+ ],
102
+ },
103
+ },
104
+ };
105
+
106
+ // We need to force the component rendering
107
+ // to fill the Helmet
108
+ renderer.create(
109
+ <Provider store={store}>
110
+ <AlternateHrefLangs content={content} />
111
+ </Provider>,
112
+ );
113
+
114
+ const helmetLinks = Helmet.peek().linkTags;
115
+
116
+ // We expect having 3 links
117
+ expect(helmetLinks.length).toBe(3);
118
+
119
+ expect(helmetLinks).toContainEqual({
120
+ rel: 'alternate',
121
+ href: '/eu',
122
+ hrefLang: 'eu',
123
+ });
124
+ expect(helmetLinks).toContainEqual({
125
+ rel: 'alternate',
126
+ href: '/es',
127
+ hrefLang: 'es',
128
+ });
129
+ expect(helmetLinks).toContainEqual({
130
+ rel: 'alternate',
131
+ href: '/en',
132
+ hrefLang: 'en',
133
+ });
134
+ });
135
+ });
@@ -54,7 +54,14 @@ export default function Image({
54
54
  attrs.className = cx(className, { responsive });
55
55
 
56
56
  if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
57
- const sortedScales = Object.values(image.scales).sort((a, b) => {
57
+ const sortedScales = Object.values({
58
+ ...image.scales,
59
+ original: {
60
+ download: `${image.download}`,
61
+ width: image.width,
62
+ height: image.height,
63
+ },
64
+ }).sort((a, b) => {
58
65
  if (a.width > b.width) return 1;
59
66
  else if (a.width < b.width) return -1;
60
67
  else return 0;
@@ -21,6 +21,7 @@ import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
21
21
  import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url';
22
22
  import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content';
23
23
  import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
24
+ import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs';
24
25
 
25
26
  import config from '@plone/volto/registry';
26
27
  import SlotRenderer from '../SlotRenderer/SlotRenderer';
@@ -234,6 +235,7 @@ class View extends Component {
234
235
  return (
235
236
  <div id="view" tabIndex="-1">
236
237
  <ContentMetadataTags content={this.props.content} />
238
+ <AlternateHrefLangs content={this.props.content} />
237
239
  {/* Body class if displayName in component is set */}
238
240
  <BodyClass
239
241
  className={
@@ -76,7 +76,6 @@ export const unwantedControlPanelsFields = {
76
76
  'site_favicon_mimetype',
77
77
  'exposeDCMetaTags',
78
78
  'enable_sitemap',
79
- 'robots_txt',
80
79
  'webstats_js',
81
80
  ],
82
81
  editing: ['available_editors', 'default_editor', 'ext_editor'],
@@ -163,7 +163,6 @@ let config = {
163
163
  showSelfRegistration: false,
164
164
  contentMetadataTagsImageField: 'image',
165
165
  contentPropertiesSchemaEnhancer: null,
166
- hasWorkingCopySupport: false,
167
166
  maxUndoLevels: 200, // undo history size for the main form
168
167
  addonsInfo: addonsInfo,
169
168
  workflowMapping,
@@ -4,13 +4,12 @@ import { generateRobots } from '@plone/volto/helpers/Robots/Robots';
4
4
  /*
5
5
  robots.txt - priority order:
6
6
 
7
- 1) robots.txt in /public folder
8
- 2) VOLTO_ROBOTSTXT var in .env
9
- 3) default: plone robots.txt
7
+ 1) VOLTO_ROBOTSTXT var in .env
8
+ 2) robots.txt setting in the site control panel
10
9
 
11
10
  */
12
11
 
13
- const ploneRobots = function (req, res, next) {
12
+ const siteRobots = function (req, res, next) {
14
13
  generateRobots(req).then((robots) => {
15
14
  res.set('Content-Type', 'text/plain');
16
15
  res.send(robots);
@@ -27,7 +26,7 @@ export default function robotstxtMiddleware() {
27
26
  if (process.env.VOLTO_ROBOTSTXT) {
28
27
  middleware.all('**/robots.txt', envRobots);
29
28
  } else {
30
- middleware.all('**/robots.txt', ploneRobots);
29
+ middleware.all('**/robots.txt', siteRobots);
31
30
  }
32
31
  middleware.id = 'robots.txt';
33
32
  return middleware;
@@ -17,7 +17,7 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
17
17
  * @param {string} path Path (or URL) to be formatted.
18
18
  * @returns {string} Formatted path.
19
19
  */
20
- function formatUrl(path) {
20
+ export function formatUrl(path) {
21
21
  const { settings } = config;
22
22
  const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
23
23
 
@@ -120,9 +120,10 @@ export function moveBlock(formData, source, destination) {
120
120
  * @function deleteBlock
121
121
  * @param {Object} formData Form data
122
122
  * @param {string} blockId Block uid
123
+ * @param {Object} intl intl object.
123
124
  * @return {Object} New form data
124
125
  */
125
- export function deleteBlock(formData, blockId) {
126
+ export function deleteBlock(formData, blockId, intl) {
126
127
  const blocksFieldname = getBlocksFieldname(formData);
127
128
  const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
128
129
 
@@ -135,7 +136,13 @@ export function deleteBlock(formData, blockId) {
135
136
  };
136
137
 
137
138
  if (newFormData[blocksLayoutFieldname].items.length === 0) {
138
- newFormData = addBlock(newFormData, config.settings.defaultBlockType, 0);
139
+ newFormData = addBlock(
140
+ newFormData,
141
+ config.settings.defaultBlockType,
142
+ 0,
143
+ {},
144
+ intl,
145
+ );
139
146
  }
140
147
 
141
148
  return newFormData;
@@ -147,9 +154,11 @@ export function deleteBlock(formData, blockId) {
147
154
  * @param {Object} formData Form data
148
155
  * @param {string} type Block type
149
156
  * @param {number} index Destination index
157
+ * @param {Object} blocksConfig Blocks configuration.
158
+ * @param {Object} intl intl object.
150
159
  * @return {Array} New block id, New form data
151
160
  */
152
- export function addBlock(formData, type, index, blocksConfig) {
161
+ export function addBlock(formData, type, index, blocksConfig, intl) {
153
162
  const { settings } = config;
154
163
  const id = uuid();
155
164
  const idTrailingBlock = uuid();
@@ -192,6 +201,7 @@ export function addBlock(formData, type, index, blocksConfig) {
192
201
  },
193
202
  selected: id,
194
203
  },
204
+ intl,
195
205
  }),
196
206
  ];
197
207
  }
@@ -208,6 +218,7 @@ export const applyBlockInitialValue = ({
208
218
  value,
209
219
  blocksConfig,
210
220
  formData,
221
+ intl,
211
222
  }) => {
212
223
  const type = value['@type'];
213
224
  blocksConfig = blocksConfig || config.blocks.blocksConfig;
@@ -217,6 +228,7 @@ export const applyBlockInitialValue = ({
217
228
  id,
218
229
  value,
219
230
  formData,
231
+ intl,
220
232
  });
221
233
  const blocksFieldname = getBlocksFieldname(formData);
222
234
  formData[blocksFieldname][id] = value;
@@ -231,9 +243,11 @@ export const applyBlockInitialValue = ({
231
243
  * @param {Object} formData Form data
232
244
  * @param {string} id Block uid to mutate
233
245
  * @param {number} value Block's new value
246
+ * @param {Object} blocksConfig Blocks configuration.
247
+ * @param {Object} intl intl object.
234
248
  * @return {Object} New form data
235
249
  */
236
- export function mutateBlock(formData, id, value, blocksConfig) {
250
+ export function mutateBlock(formData, id, value, blocksConfig, intl) {
237
251
  const { settings } = config;
238
252
  const blocksFieldname = getBlocksFieldname(formData);
239
253
  const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
@@ -260,6 +274,7 @@ export function mutateBlock(formData, id, value, blocksConfig) {
260
274
  [id]: value || null,
261
275
  },
262
276
  },
277
+ intl,
263
278
  });
264
279
  if (!blockHasValue(block)) {
265
280
  return newFormData;
@@ -288,6 +303,7 @@ export function mutateBlock(formData, id, value, blocksConfig) {
288
303
  ],
289
304
  },
290
305
  },
306
+ intl,
291
307
  });
292
308
  return newFormData;
293
309
  }
@@ -298,6 +314,10 @@ export function mutateBlock(formData, id, value, blocksConfig) {
298
314
  * @param {Object} formData Form data
299
315
  * @param {string} id Insert new block before the block with this id
300
316
  * @param {number} value New block's value
317
+ * @param {Object} current Current block
318
+ * @param {number} offset offset position
319
+ * @param {Object} blocksConfig Blocks configuration.
320
+ * @param {Object} intl intl object.
301
321
  * @return {Array} New block id, New form data
302
322
  */
303
323
  export function insertBlock(
@@ -307,6 +327,7 @@ export function insertBlock(
307
327
  current = {},
308
328
  offset = 0,
309
329
  blocksConfig,
330
+ intl,
310
331
  ) {
311
332
  const blocksFieldname = getBlocksFieldname(formData);
312
333
  const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
@@ -340,6 +361,7 @@ export function insertBlock(
340
361
  ],
341
362
  },
342
363
  },
364
+ intl,
343
365
  });
344
366
 
345
367
  return [newBlockId, newFormData];
@@ -568,6 +568,52 @@ describe('Blocks', () => {
568
568
  marker: true,
569
569
  });
570
570
  });
571
+
572
+ it('initialValue with intl', () => {
573
+ // Mock intl with formatMessage function
574
+ const intl = {
575
+ formatMessage: jest.fn(({ id }) => id),
576
+ };
577
+
578
+ const messages = {
579
+ intl: {
580
+ id: 'intl',
581
+ defaultMessage: 'intl',
582
+ },
583
+ };
584
+
585
+ config.blocks.blocksConfig.text.initialValue = ({
586
+ id,
587
+ value,
588
+ formData,
589
+ intl,
590
+ }) => {
591
+ return {
592
+ ...formData.blocks[id],
593
+ intl: intl.formatMessage(messages.intl),
594
+ };
595
+ };
596
+ const [newId, form] = addBlock(
597
+ {
598
+ blocks: { a: { value: 1 }, b: { value: 2 } },
599
+ blocks_layout: { items: ['a', 'b'] },
600
+ },
601
+ 'text',
602
+ 1,
603
+ config.blocks.blocksConfig,
604
+ intl,
605
+ );
606
+
607
+ delete config.blocks.blocksConfig.text.initialValue;
608
+
609
+ expect(form.blocks[newId]).toStrictEqual({
610
+ '@type': 'text',
611
+ booleanField: false,
612
+ description: 'Default description',
613
+ title: 'Default title',
614
+ intl: 'intl',
615
+ });
616
+ });
571
617
  });
572
618
 
573
619
  describe('moveBlock', () => {
@@ -171,7 +171,9 @@ export const patternValidator = ({
171
171
  }
172
172
  const regex = new RegExp(field.pattern);
173
173
  const isValid = regex.test(value);
174
- return !isValid ? formatMessage(messages.pattern) : null;
174
+ return !isValid
175
+ ? formatMessage(messages.pattern, { pattern: field.pattern })
176
+ : null;
175
177
  };
176
178
 
177
179
  export const maxItemsValidator = ({
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Sitemap helper.
3
- * @module helpers/Sitemap
2
+ * Robots helper.
3
+ * @module helpers/Robots
4
4
  */
5
5
 
6
6
  import superagent from 'superagent';
7
+
7
8
  import config from '@plone/volto/registry';
9
+ import { formatUrl } from '@plone/volto/helpers/Api/Api';
8
10
  import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
9
11
 
10
12
  /**
@@ -15,43 +17,22 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
15
17
  */
16
18
  export const generateRobots = (req) =>
17
19
  new Promise((resolve) => {
18
- const internalUrl =
19
- config.settings.internalApiPath ??
20
- config.settings.devProxyToApiPath ??
21
- config.settings.apiPath;
22
- const request = superagent.get(`${internalUrl}/robots.txt`);
23
- request.set('Accept', 'text/plain');
20
+ const request = superagent.get(formatUrl('@site'));
21
+ request.set('Accept', 'application/json');
24
22
  const authToken = req.universalCookies.get('auth_token');
25
23
  if (authToken) {
26
24
  request.set('Authorization', `Bearer ${authToken}`);
27
25
  }
28
26
  request.use(addHeadersFactory(req));
29
- request.end((error, { text }) => {
27
+ request.end((error, { text, body }) => {
30
28
  if (error) {
31
29
  resolve(text || error);
32
30
  } else {
33
- // It appears that express does not take the x-forwarded headers into
34
- // consideration, so we do it ourselves.
35
- const {
36
- 'x-forwarded-proto': forwardedProto,
37
- 'x-forwarded-host': forwardedHost,
38
- 'x-forwarded-port': forwardedPort,
39
- } = req.headers;
40
- const proto = forwardedProto ?? req.protocol;
41
- const host = forwardedHost ?? req.get('Host');
42
- const portNum = forwardedPort ?? req.get('Port');
43
- const port =
44
- (proto === 'https' && '' + portNum === '443') ||
45
- (proto === 'http' && '' + portNum === '80')
46
- ? ''
47
- : `:${portNum}`;
48
- // Plone has probably returned the sitemap link with the internal url.
49
- // If so, let's replace it with the current one.
50
- const url = `${proto}://${host}${port}`;
51
- text = text.replace(internalUrl, url);
52
- // Replace the sitemap with the sitemap index.
53
- text = text.replace('sitemap.xml.gz', 'sitemap-index.xml');
54
- resolve(text);
31
+ resolve(
32
+ body['plone.robots_txt']
33
+ .replace('{portal_url}', config.settings.publicURL)
34
+ .replace('sitemap.xml.gz', 'sitemap-index.xml'),
35
+ );
55
36
  }
56
37
  });
57
38
  });
@@ -13,9 +13,13 @@ export default function useClipboard(clipboardText = '') {
13
13
  }
14
14
  };
15
15
 
16
- const copyAction = useCallback(() => {
17
- const copiedString = copyToClipboard(stringToCopy.current);
18
- setCopied(copiedString);
16
+ const copyAction = useCallback(async () => {
17
+ try {
18
+ await copyToClipboard(stringToCopy.current);
19
+ setCopied(true);
20
+ } catch (error) {
21
+ setCopied(false);
22
+ }
19
23
  }, [stringToCopy]);
20
24
 
21
25
  useEffect(() => {
@@ -43,6 +43,7 @@ export class ObjectBrowserWidgetComponent extends React.Component<any, any, any>
43
43
  state: {
44
44
  manualLinkInput: string;
45
45
  validURL: boolean;
46
+ errors: any[];
46
47
  };
47
48
  selectedItemsRef: React.RefObject<any>;
48
49
  placeholderRef: React.RefObject<any>;
@@ -0,0 +1 @@
1
+ export function AlternateHrefLangs(props: any): import("react/jsx-runtime").JSX.Element;
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Format the url.
3
+ * @function formatUrl
4
+ * @param {string} path Path (or URL) to be formatted.
5
+ * @returns {string} Formatted path.
6
+ */
7
+ export function formatUrl(path: string): string;
1
8
  export default Api;
2
9
  /**
3
10
  * Api class.