@eeacms/volto-cca-policy 0.3.65 → 0.3.67

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 (42) hide show
  1. package/CHANGELOG.md +60 -5
  2. package/locales/bg/LC_MESSAGES/volto.po +87 -22
  3. package/locales/cs/LC_MESSAGES/volto.po +87 -22
  4. package/locales/da/LC_MESSAGES/volto.po +87 -22
  5. package/locales/de/LC_MESSAGES/volto.po +87 -22
  6. package/locales/eea.cca.pot +45 -0
  7. package/locales/el/LC_MESSAGES/volto.po +87 -22
  8. package/locales/en/LC_MESSAGES/volto.po +65 -0
  9. package/locales/es/LC_MESSAGES/volto.po +87 -22
  10. package/locales/et/LC_MESSAGES/volto.po +87 -22
  11. package/locales/fi/LC_MESSAGES/volto.po +87 -22
  12. package/locales/fr/LC_MESSAGES/volto.po +87 -22
  13. package/locales/ga/LC_MESSAGES/volto.po +87 -22
  14. package/locales/hr/LC_MESSAGES/volto.po +87 -22
  15. package/locales/hu/LC_MESSAGES/volto.po +87 -22
  16. package/locales/is/LC_MESSAGES/volto.po +87 -22
  17. package/locales/it/LC_MESSAGES/volto.po +87 -22
  18. package/locales/lt/LC_MESSAGES/volto.po +87 -22
  19. package/locales/lv/LC_MESSAGES/volto.po +65 -0
  20. package/locales/mt/LC_MESSAGES/volto.po +87 -22
  21. package/locales/nl/LC_MESSAGES/volto.po +87 -22
  22. package/locales/nn/LC_MESSAGES/volto.po +117 -19
  23. package/locales/pl/LC_MESSAGES/volto.po +87 -22
  24. package/locales/pt/LC_MESSAGES/volto.po +87 -22
  25. package/locales/ro/LC_MESSAGES/volto.po +87 -22
  26. package/locales/sk/LC_MESSAGES/volto.po +87 -22
  27. package/locales/sl/LC_MESSAGES/volto.po +87 -22
  28. package/locales/sv/LC_MESSAGES/volto.po +87 -22
  29. package/locales/tr/LC_MESSAGES/volto.po +103 -22
  30. package/locales/volto.pot +2 -1
  31. package/package.json +1 -1
  32. package/src/components/manage/Blocks/ASTNavigation/ASTNavigationView.jsx +8 -2
  33. package/src/components/manage/Blocks/RASTBlock/ContextNavigation.jsx +3 -1
  34. package/src/components/theme/ASTNavigation/ASTAccordion.jsx +4 -2
  35. package/src/components/theme/Header/Header.jsx +4 -1
  36. package/src/components/theme/Header/LanguageSwitch.jsx +41 -68
  37. package/src/components/theme/ImageGallery/ImageGallery.jsx +2 -2
  38. package/src/components/theme/ImageGallery/ImageGallery.test.jsx +38 -0
  39. package/src/components/theme/PortalMessage/PortalMessage.test.jsx +24 -0
  40. package/src/components/theme/Widgets/PromotionalImageWidget.jsx +35 -13
  41. package/src/index.js +18 -3
  42. package/src/utils.js +11 -6
@@ -20,8 +20,8 @@ export default function LanguageSwitch({ history }) {
20
20
  );
21
21
  const [, setSelectedLanguage] = useAtom(selectedLanguageAtom);
22
22
  const width = useSelector((state) => state.screen?.width);
23
-
24
23
  const currentLang = useSelector((state) => state.intl.locale);
24
+
25
25
  const [language, setLanguage] = React.useState(
26
26
  currentLang || eea.defaultLanguage,
27
27
  );
@@ -45,7 +45,43 @@ export default function LanguageSwitch({ history }) {
45
45
  });
46
46
  };
47
47
 
48
- // .filter((item) => eea.non_eu_langs.indexOf(item.code) !== -1)
48
+ const renderLanguageItems = (languages) =>
49
+ languages.map((item, index) => {
50
+ const translated = (translations || []).some(
51
+ (obj) => obj.language === item.code,
52
+ );
53
+ const active = item.code === currentLang;
54
+ const disabled = !translated && !active;
55
+
56
+ return (
57
+ <Dropdown.Item
58
+ className={cx({
59
+ disabled: disabled,
60
+ active: active,
61
+ })}
62
+ as="li"
63
+ key={index}
64
+ text={
65
+ <span>
66
+ <span className="country-code">{item.code.toUpperCase()}</span>{' '}
67
+ {item.name}
68
+ </span>
69
+ }
70
+ onClick={(e) =>
71
+ disabled || active ? e.preventDefault() : handlePageRedirect(item)
72
+ }
73
+ />
74
+ );
75
+ });
76
+
77
+ const euLanguages = eea.languages.filter(
78
+ (item) => eea.non_eu_langs.indexOf(item.code) === -1,
79
+ );
80
+
81
+ const nonEuLanguages = eea.languages.filter(
82
+ (item) => eea.non_eu_langs.indexOf(item.code) !== -1,
83
+ );
84
+
49
85
  return (
50
86
  <Header.TopDropdownMenu
51
87
  id="language-switcher"
@@ -56,7 +92,7 @@ export default function LanguageSwitch({ history }) {
56
92
  }
57
93
  text={`${language.toUpperCase()}`}
58
94
  mobileText={`${language.toUpperCase()}`}
59
- icon={<Image src={globeIcon} alt="language dropdown globe icon"></Image>}
95
+ icon={<Image src={globeIcon} alt="language dropdown globe icon" />}
60
96
  viewportWidth={width}
61
97
  >
62
98
  <ul
@@ -64,39 +100,8 @@ export default function LanguageSwitch({ history }) {
64
100
  role="listbox"
65
101
  aria-label="language switcher"
66
102
  >
67
- {eea.languages
68
- .filter((item) => eea.non_eu_langs.indexOf(item.code) === -1)
69
- .map((item, index) => {
70
- const translated = (translations || []).some(
71
- (obj) => obj.language === item.code,
72
- );
73
- const active = item.code === currentLang;
74
- const disabled = !translated && !active;
103
+ {renderLanguageItems(euLanguages)}
75
104
 
76
- return (
77
- <Dropdown.Item
78
- className={cx({
79
- disabled: disabled,
80
- active: active,
81
- })}
82
- as="li"
83
- key={index}
84
- text={
85
- <span>
86
- <span className="country-code">
87
- {item.code.toUpperCase()}
88
- </span>{' '}
89
- {item.name}
90
- </span>
91
- }
92
- onClick={(e) =>
93
- disabled || active
94
- ? e.preventDefault()
95
- : handlePageRedirect(item)
96
- }
97
- ></Dropdown.Item>
98
- );
99
- })}
100
105
  <strong className="noneu-langs-label">
101
106
  <FormattedMessage
102
107
  id="Non-EU Languages"
@@ -104,39 +109,7 @@ export default function LanguageSwitch({ history }) {
104
109
  />
105
110
  </strong>
106
111
 
107
- {eea.languages
108
- .filter((item) => eea.non_eu_langs.indexOf(item.code) !== -1)
109
- .map((item, index) => {
110
- const translated = (translations || []).some(
111
- (obj) => obj.language === item.code,
112
- );
113
- const active = item.code === currentLang;
114
- const disabled = !translated && !active;
115
-
116
- return (
117
- <Dropdown.Item
118
- className={cx({
119
- disabled: disabled,
120
- active: active,
121
- })}
122
- as="li"
123
- key={index}
124
- text={
125
- <span>
126
- <span className="country-code">
127
- {item.code.toUpperCase()}
128
- </span>{' '}
129
- {item.name}
130
- </span>
131
- }
132
- onClick={(e) =>
133
- disabled || active
134
- ? e.preventDefault()
135
- : handlePageRedirect(item)
136
- }
137
- ></Dropdown.Item>
138
- );
139
- })}
112
+ {renderLanguageItems(nonEuLanguages)}
140
113
  </ul>
141
114
  </Header.TopDropdownMenu>
142
115
  );
@@ -89,10 +89,10 @@ const ImageGallery = (props) => {
89
89
  <Slider {...carouselSettings} ref={sliderRef}>
90
90
  {items.map((item, i) => {
91
91
  return image.rights ? (
92
- <div>
92
+ <div key={i}>
93
93
  <div className="image-slide">
94
94
  <div className="image-rights">@ {image.rights}</div>
95
- <Image key={i} src={item.url} alt={item?.title} />
95
+ <Image src={item.url} alt={item?.title} />
96
96
  </div>
97
97
  </div>
98
98
  ) : (
@@ -0,0 +1,38 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import ImageGallery from './ImageGallery';
4
+
5
+ jest.mock('react-slick', () => (props) => <div>React Slick Gallery</div>);
6
+
7
+ const mockItems = [
8
+ {
9
+ url: 'https://example.com/image1.jpg',
10
+ title: 'Image One',
11
+ description: 'First image description',
12
+ rights: 'Author One',
13
+ },
14
+ {
15
+ url: 'https://example.com/image2.jpg',
16
+ title: 'Image Two',
17
+ description: 'Second image description',
18
+ rights: 'Author Two',
19
+ },
20
+ ];
21
+
22
+ describe('ImageGallery', () => {
23
+ it('renders preview image and opens modal on click', () => {
24
+ render(<ImageGallery items={mockItems} />);
25
+
26
+ const previewImage = screen.getByRole('img', { name: /Image One/i });
27
+ expect(previewImage).toBeInTheDocument();
28
+
29
+ expect(
30
+ screen.queryByText('First image description'),
31
+ ).not.toBeInTheDocument();
32
+
33
+ fireEvent.click(previewImage);
34
+
35
+ expect(screen.getByText('Image One')).toBeInTheDocument();
36
+ expect(screen.getByText('First image description')).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,24 @@
1
+ import { render } from '@testing-library/react';
2
+ import PortalMessage from './PortalMessage';
3
+ import { IntlProvider } from 'react-intl';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const renderWithIntl = (ui) =>
7
+ render(<IntlProvider locale="en">{ui}</IntlProvider>);
8
+
9
+ describe('PortalMessage', () => {
10
+ it('renders the message component when content is archived', () => {
11
+ const content = { review_state: 'archived' };
12
+ const { container } = renderWithIntl(<PortalMessage content={content} />);
13
+
14
+ const messageEl = container.querySelector('.ui.message');
15
+ expect(messageEl).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders nothing when content is not archived', () => {
19
+ const content = { review_state: 'published' };
20
+ const { container } = renderWithIntl(<PortalMessage content={content} />);
21
+
22
+ expect(container).toBeEmptyDOMElement();
23
+ });
24
+ });
@@ -72,6 +72,24 @@ const messages = defineMessages({
72
72
  * ```
73
73
  *
74
74
  */
75
+ function parseDataURL(dataUrl) {
76
+ if (!dataUrl.startsWith('data:')) return null;
77
+
78
+ const commaIndex = dataUrl.indexOf(',');
79
+ if (commaIndex === -1) return null;
80
+
81
+ const meta = dataUrl.slice(5, commaIndex);
82
+ const data = dataUrl.slice(commaIndex + 1);
83
+
84
+ const [contentType, encoding] = meta.split(';');
85
+
86
+ return {
87
+ 'content-type': contentType || '',
88
+ encoding: encoding || '',
89
+ data: data || '',
90
+ };
91
+ }
92
+
75
93
  const FileWidget = (props) => {
76
94
  const { id, value, onChange, isDisabled } = props;
77
95
  const [fileType, setFileType] = React.useState(false);
@@ -99,27 +117,31 @@ const FileWidget = (props) => {
99
117
  const file = files[0];
100
118
  if (!validateFileUploadSize(file, intl.formatMessage)) return;
101
119
  readAsDataURL(file).then((data) => {
102
- const fields = data.match(/^data:(.*);(.*),(.*)$/);
103
- onChange(id, {
104
- data: fields[3],
105
- encoding: fields[2],
106
- 'content-type': fields[1],
107
- filename: file.name,
108
- });
120
+ const fields = parseDataURL(data);
121
+ if (fields) {
122
+ onChange(id, {
123
+ data: fields.data,
124
+ encoding: fields.encoding,
125
+ 'content-type': fields['content-type'],
126
+ filename: file.name,
127
+ });
128
+ }
109
129
  });
110
130
 
111
- let reader = new FileReader();
131
+ const reader = new FileReader();
112
132
  reader.onload = function () {
113
- const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
114
- if (imageMimetypes.includes(fields[1])) {
133
+ const parsed = parseDataURL(reader.result);
134
+ if (parsed && imageMimetypes.includes(parsed['content-type'])) {
115
135
  setFileType(true);
116
- let imagePreview = document.getElementById(`field-${id}-image`);
117
- imagePreview.src = reader.result;
136
+ const imagePreview = document.getElementById(`field-${id}-image`);
137
+ if (imagePreview) {
138
+ imagePreview.src = reader.result;
139
+ }
118
140
  } else {
119
141
  setFileType(false);
120
142
  }
121
143
  };
122
- reader.readAsDataURL(files[0]);
144
+ reader.readAsDataURL(file);
123
145
  };
124
146
 
125
147
  return (
package/src/index.js CHANGED
@@ -522,7 +522,7 @@ const applyConfig = (config) => {
522
522
  },
523
523
  {
524
524
  match: {
525
- path: /(.*)\/add$/,
525
+ path: /^.*\/add$/,
526
526
  },
527
527
  component: RedirectToLogin,
528
528
  },
@@ -532,13 +532,13 @@ const applyConfig = (config) => {
532
532
  ...config.settings.apiExpanders,
533
533
  {
534
534
  match: {
535
- path: /(.*)\/policy-context\/country-profiles\/(.*)/,
535
+ path: /\/policy-context\/country-profiles\/.+/,
536
536
  },
537
537
  GET_CONTENT: ['siblings'],
538
538
  },
539
539
  {
540
540
  match: {
541
- path: /(.*)\/countries-regions\/countries\/(.*)/,
541
+ path: /\/countries-regions\/countries\/.+/,
542
542
  },
543
543
  GET_CONTENT: ['siblings'],
544
544
  },
@@ -567,6 +567,21 @@ const applyConfig = (config) => {
567
567
  'plone.app.vocabularies.Users'
568
568
  ] = SelectAutoCompleteWidget;
569
569
 
570
+ config.settings.matomoTrackerIdFn = (pathname) => {
571
+ return pathname.split('/')[2] === 'mission'
572
+ ? {
573
+ matomoSiteId:
574
+ typeof window !== 'undefined'
575
+ ? window.env?.RAZZLE_MATOMO_SITE_ID || '321'
576
+ : '321',
577
+ matomoSecondSiteId:
578
+ typeof window !== 'undefined'
579
+ ? window.env?.RAZZLE_MATOMO_MISSION_SITE_ID || '321'
580
+ : '321',
581
+ }
582
+ : null;
583
+ };
584
+
570
585
  return compose(installBlocks, installSearchEngine, installStore)(config);
571
586
  };
572
587
 
package/src/utils.js CHANGED
@@ -101,6 +101,15 @@ export const formatTextToHTML = (text) => {
101
101
  : `<p>${formattedText}</p>`;
102
102
  };
103
103
 
104
+ const trimTrailingChars = (str) => {
105
+ const charsToTrim = ['-', '–', ';', ',', ':', ' '];
106
+ let end = str.length;
107
+ while (end > 0 && charsToTrim.includes(str[end - 1])) {
108
+ end--;
109
+ }
110
+ return str.substring(0, end);
111
+ };
112
+
104
113
  export const extractPlanNameAndURL = (text) => {
105
114
  if (!text) return { name: '', url: '' };
106
115
 
@@ -114,12 +123,8 @@ export const extractPlanNameAndURL = (text) => {
114
123
 
115
124
  if (url) {
116
125
  // Remove URL and any punctuation before it
117
- name = name
118
- .replace(`(${url})`, '')
119
- .replace(url, '')
120
- .replace(/[-–;,:\s]+$/, '')
121
- .replace(/[-–;,:\s]+$/, '')
122
- .trim();
126
+ name = name.replace(`(${url})`, '').replace(url, '');
127
+ name = trimTrailingChars(name).trim();
123
128
  }
124
129
 
125
130
  return {