@plone/volto 18.6.0 → 18.7.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.
@@ -0,0 +1,36 @@
1
+ /*
2
+ This file is autogenerated. Don't change it directly.
3
+ Instead, change the "addons" setting in your package.json file.
4
+ */
5
+
6
+ const addonsInfo = [];
7
+ export { addonsInfo };
8
+
9
+ const safeWrapper = (func) => (config) => {
10
+ const res = func(config);
11
+ if (typeof res === 'undefined') {
12
+ throw new Error("Configuration function doesn't return config");
13
+ }
14
+ return res;
15
+ };
16
+
17
+ const projectConfigLoader = false;
18
+ const projectConfig = (config) => {
19
+ return projectConfigLoader &&
20
+ typeof projectConfigLoader.default === 'function'
21
+ ? projectConfigLoader.default(config)
22
+ : config;
23
+ };
24
+
25
+ const load = (config) => {
26
+ const addonLoaders = [];
27
+ if (!addonLoaders.every((el) => typeof el === 'function')) {
28
+ throw new TypeError(
29
+ 'Each addon has to provide a function applying its configuration to the projects configuration.',
30
+ );
31
+ }
32
+ return projectConfig(
33
+ addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config),
34
+ );
35
+ };
36
+ export default load;
package/CHANGELOG.md CHANGED
@@ -17,6 +17,29 @@ myst:
17
17
 
18
18
  <!-- towncrier release notes start -->
19
19
 
20
+ ## 18.7.0 (2025-01-24)
21
+
22
+ ### Feature
23
+
24
+ - - Fixed handling of the site logo preview to appear after upload. @Shyam-Raghuwanshi [#6591](https://github.com/plone/volto/issues/6591)
25
+ - Provide language alternate links @erral [#6602](https://github.com/plone/volto/issues/6602)
26
+ - feat(cypress):Add custom check Accessibility command @Tishasoumya-02 [#6606](https://github.com/plone/volto/issues/6606)
27
+
28
+ ### Bugfix
29
+
30
+ - Improve the usability of the `ObjectBrowser` when inputting a manual value, checking it on blur, and adding a local validator. @sneridagh [#6576](https://github.com/plone/volto/issues/6576)
31
+ - fix(useClipboard): Do not have a pending promise in a boolean state @nileshgulia1 [#6585](https://github.com/plone/volto/issues/6585)
32
+
33
+ ### Internal
34
+
35
+ - Add Seven convenience Makefile commands. @sneridagh [#6599](https://github.com/plone/volto/issues/6599)
36
+ - Restore pull request previews on Read the Docs. @stevepiercy [#6612](https://github.com/plone/volto/issues/6612)
37
+ - Fix lint-staged throwing warnings when a file is checked-in and ignored. @sneridagh [#6614](https://github.com/plone/volto/issues/6614)
38
+
39
+ ### Documentation
40
+
41
+ - Enhancements of the upgrade guide for Volto 18, since we detected some inconsistencies. @sneridagh [#6609](https://github.com/plone/volto/issues/6609)
42
+
20
43
  ## 18.6.0 (2025-01-11)
21
44
 
22
45
  ### Feature
@@ -967,3 +967,25 @@ Cypress.Commands.add('queryCounter', (path, steps, number = 1) => {
967
967
 
968
968
  cy.get('@counterName').its('callCount').should('equal', number);
969
969
  });
970
+
971
+ // Print cypress-axe violations to the terminal
972
+ function printAccessibilityViolations(violations) {
973
+ cy.task(
974
+ 'table',
975
+ violations.map(({ id, impact, description, nodes }) => ({
976
+ impact,
977
+ description: `${description} (${id})`,
978
+ nodes: nodes.length,
979
+ })),
980
+ );
981
+ }
982
+
983
+ Cypress.Commands.add(
984
+ 'checkAccessibility',
985
+ (subject, { skipFailures = false } = {}) => {
986
+ cy.checkA11y(subject, null, printAccessibilityViolations, skipFailures);
987
+ },
988
+ {
989
+ prevSubject: 'optional',
990
+ },
991
+ );
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "18.6.0",
12
+ "version": "18.7.0",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -236,8 +236,8 @@
236
236
  "url": "^0.11.3",
237
237
  "use-deep-compare-effect": "1.8.1",
238
238
  "uuid": "^8.3.2",
239
- "@plone/registry": "2.2.0",
240
- "@plone/volto-slate": "18.1.0",
239
+ "@plone/volto-slate": "18.2.0",
240
+ "@plone/registry": "2.3.0",
241
241
  "@plone/scripts": "3.8.1"
242
242
  },
243
243
  "devDependencies": {
@@ -358,7 +358,7 @@
358
358
  "webpack-dev-server": "4.11.1",
359
359
  "webpack-node-externals": "3.0.0",
360
360
  "why": "0.6.2",
361
- "@plone/types": "1.2.0",
361
+ "@plone/types": "1.3.0",
362
362
  "@plone/volto-coresandbox": "1.0.0"
363
363
  },
364
364
  "volta": {
@@ -16,10 +16,10 @@ import { Image, Label, Popup, Button } from 'semantic-ui-react';
16
16
  import {
17
17
  flattenToAppURL,
18
18
  isInternalURL,
19
- isUrl,
20
19
  normalizeUrl,
21
20
  removeProtocol,
22
21
  } from '@plone/volto/helpers/Url/Url';
22
+ import { urlValidator } from '@plone/volto/helpers/FormValidation/validators';
23
23
  import { searchContent } from '@plone/volto/actions/search/search';
24
24
  import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
25
25
  import { defineMessages, injectIntl } from 'react-intl';
@@ -102,6 +102,7 @@ export class ObjectBrowserWidgetComponent extends Component {
102
102
  state = {
103
103
  manualLinkInput: '',
104
104
  validURL: false,
105
+ errors: [],
105
106
  };
106
107
 
107
108
  constructor(props) {
@@ -230,7 +231,16 @@ export class ObjectBrowserWidgetComponent extends Component {
230
231
 
231
232
  validateManualLink = (url) => {
232
233
  if (this.props.allowExternals) {
233
- return isUrl(url);
234
+ const error = urlValidator({
235
+ value: url,
236
+ formatMessage: this.props.intl.formatMessage,
237
+ });
238
+ if (error && url !== '') {
239
+ this.setState({ errors: [error] });
240
+ } else {
241
+ this.setState({ errors: [] });
242
+ }
243
+ return !Boolean(error);
234
244
  } else {
235
245
  return isInternalURL(url);
236
246
  }
@@ -344,6 +354,8 @@ export class ObjectBrowserWidgetComponent extends Component {
344
354
  return (
345
355
  <FormFieldWrapper
346
356
  {...this.props}
357
+ // At the moment, OBW handles its own errors and validation
358
+ error={this.state.errors}
347
359
  className={description ? 'help text' : 'text'}
348
360
  >
349
361
  <div
@@ -372,6 +384,7 @@ export class ObjectBrowserWidgetComponent extends Component {
372
384
  items.length === 0 &&
373
385
  this.props.mode !== 'multiple' && (
374
386
  <input
387
+ onBlur={this.onSubmitManualLink}
375
388
  onKeyDown={this.onKeyDownManualLink}
376
389
  onChange={this.onManualLinkInput}
377
390
  value={this.state.manualLinkInput}
@@ -3,7 +3,7 @@
3
3
  * @module components/manage/Widgets/RegistryImageWidget
4
4
  */
5
5
 
6
- import React from 'react';
6
+ import React, { useState } from 'react';
7
7
  import PropTypes from 'prop-types';
8
8
  import { Button, Image, Dimmer } from 'semantic-ui-react';
9
9
  import { readAsDataURL } from 'promise-file-reader';
@@ -76,12 +76,15 @@ const RegistryImageWidget = (props) => {
76
76
  const { id, value, onChange, isDisabled } = props;
77
77
  const intl = useIntl();
78
78
 
79
- const fileName = value?.split(';')[0];
80
- const imgsrc = fileName
81
- ? `${toPublicURL('/')}@@site-logo/${atob(
82
- fileName.replace('filenameb64:', ''),
83
- )}`
84
- : '';
79
+ // State to manage the preview image source
80
+ const [previewSrc, setPreviewSrc] = useState(() => {
81
+ const fileName = value?.split(';')[0];
82
+ return fileName
83
+ ? `${toPublicURL('/')}@@site-logo/${atob(
84
+ fileName.replace('filenameb64:', ''),
85
+ )}`
86
+ : '';
87
+ });
85
88
 
86
89
  /**
87
90
  * Drop handler
@@ -102,8 +105,7 @@ const RegistryImageWidget = (props) => {
102
105
  reader.onload = function () {
103
106
  const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
104
107
  if (imageMimetypes.includes(fields[1])) {
105
- let imagePreview = document.getElementById(`field-${id}-image`);
106
- imagePreview.src = reader.result;
108
+ setPreviewSrc(reader.result);
107
109
  }
108
110
  };
109
111
  reader.readAsDataURL(files[0]);
@@ -115,12 +117,12 @@ const RegistryImageWidget = (props) => {
115
117
  {({ getRootProps, getInputProps, isDragActive }) => (
116
118
  <div className="file-widget-dropzone" {...getRootProps()}>
117
119
  {isDragActive && <Dimmer active></Dimmer>}
118
- {imgsrc ? (
120
+ {previewSrc ? (
119
121
  <Image
120
122
  className="image-preview"
121
123
  id={`field-${id}-image`}
122
124
  size="small"
123
- src={imgsrc}
125
+ src={previewSrc}
124
126
  />
125
127
  ) : (
126
128
  <div className="dropzone-placeholder">
@@ -139,7 +141,6 @@ const RegistryImageWidget = (props) => {
139
141
  )}
140
142
  </div>
141
143
  )}
142
-
143
144
  <label className="label-file-widget-input">
144
145
  {value
145
146
  ? intl.formatMessage(messages.replaceFile)
@@ -168,6 +169,7 @@ const RegistryImageWidget = (props) => {
168
169
  disabled={isDisabled}
169
170
  onClick={() => {
170
171
  onChange(id, '');
172
+ setPreviewSrc(''); // Clear the preview image
171
173
  }}
172
174
  >
173
175
  <Icon name={deleteSVG} size="20px" />
@@ -189,10 +191,7 @@ RegistryImageWidget.propTypes = {
189
191
  description: PropTypes.string,
190
192
  required: PropTypes.bool,
191
193
  error: PropTypes.arrayOf(PropTypes.string),
192
- value: PropTypes.shape({
193
- '@type': PropTypes.string,
194
- title: PropTypes.string,
195
- }),
194
+ value: PropTypes.string,
196
195
  onChange: PropTypes.func.isRequired,
197
196
  wrapped: PropTypes.bool,
198
197
  };
@@ -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
+ });
@@ -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={
@@ -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;