@eeacms/volto-cca-policy 1.0.22 → 1.0.24

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 (45) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/locales/bg/LC_MESSAGES/volto.po +10 -0
  3. package/locales/cs/LC_MESSAGES/volto.po +10 -0
  4. package/locales/da/LC_MESSAGES/volto.po +10 -0
  5. package/locales/de/LC_MESSAGES/volto.po +10 -0
  6. package/locales/el/LC_MESSAGES/volto.po +10 -0
  7. package/locales/en/LC_MESSAGES/volto.po +10 -0
  8. package/locales/es/LC_MESSAGES/volto.po +10 -0
  9. package/locales/et/LC_MESSAGES/volto.po +10 -0
  10. package/locales/fi/LC_MESSAGES/volto.po +10 -0
  11. package/locales/fr/LC_MESSAGES/volto.po +10 -0
  12. package/locales/ga/LC_MESSAGES/volto.po +10 -0
  13. package/locales/hr/LC_MESSAGES/volto.po +10 -0
  14. package/locales/hu/LC_MESSAGES/volto.po +10 -0
  15. package/locales/is/LC_MESSAGES/volto.po +10 -0
  16. package/locales/it/LC_MESSAGES/volto.po +10 -0
  17. package/locales/lt/LC_MESSAGES/volto.po +10 -0
  18. package/locales/lv/LC_MESSAGES/volto.po +10 -0
  19. package/locales/mt/LC_MESSAGES/volto.po +10 -0
  20. package/locales/nl/LC_MESSAGES/volto.po +10 -0
  21. package/locales/nn/LC_MESSAGES/volto.po +10 -0
  22. package/locales/pl/LC_MESSAGES/volto.po +10 -0
  23. package/locales/pt/LC_MESSAGES/volto.po +10 -0
  24. package/locales/ro/LC_MESSAGES/volto.po +10 -0
  25. package/locales/sk/LC_MESSAGES/volto.po +10 -0
  26. package/locales/sl/LC_MESSAGES/volto.po +10 -0
  27. package/locales/sv/LC_MESSAGES/volto.po +10 -0
  28. package/locales/tr/LC_MESSAGES/volto.po +10 -0
  29. package/locales/volto.pot +11 -1
  30. package/package.json +1 -1
  31. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +23 -5
  32. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +44 -0
  33. package/src/components/manage/Blocks/ChatbotCatalogue/DocumentCard.jsx +37 -122
  34. package/src/components/manage/Blocks/ChatbotCatalogue/DocumentCard.test.jsx +162 -73
  35. package/src/components/manage/Blocks/ChatbotCatalogue/astUtils.js +249 -0
  36. package/src/components/manage/Blocks/ChatbotCatalogue/astUtils.test.js +207 -0
  37. package/src/components/manage/Blocks/ChatbotCatalogue/catalogue-chat.less +0 -56
  38. package/src/components/manage/Blocks/ChatbotCatalogue/docCards.js +92 -29
  39. package/src/components/manage/Blocks/ChatbotCatalogue/docCards.test.js +107 -0
  40. package/src/components/manage/Blocks/ChatbotCatalogue/nextSteps.js +22 -20
  41. package/src/components/manage/Blocks/ChatbotCatalogue/nextSteps.test.js +69 -0
  42. package/src/components/manage/Blocks/ChatbotCatalogue/useCatalogueDoc.js +6 -2
  43. package/src/components/manage/Blocks/ChatbotCatalogue/useCatalogueDoc.test.js +12 -0
  44. package/src/components/theme/Views/ExtendedToolView.jsx +36 -0
  45. package/src/components/theme/Views/ExtendedToolView.test.jsx +31 -4
@@ -393,6 +393,11 @@ msgstr ""
393
393
  msgid "Cycle"
394
394
  msgstr ""
395
395
 
396
+ #. Default: "Data sources"
397
+ #: components/theme/Views/ExtendedToolView
398
+ msgid "Data sources"
399
+ msgstr ""
400
+
396
401
  #. Default: "Date of creation:"
397
402
  #: components/theme/Content/Sections/ContentMetadata
398
403
  msgid "Date of creation:"
@@ -1462,6 +1467,11 @@ msgstr ""
1462
1467
  msgid "Tool"
1463
1468
  msgstr "Nástroj"
1464
1469
 
1470
+ #. Default: "Tool validation use"
1471
+ #: components/theme/Views/ExtendedToolView
1472
+ msgid "Tool validation use"
1473
+ msgstr ""
1474
+
1465
1475
  #. Default: "Tools per country"
1466
1476
  #: components/Search/NavigatorCatalogue/NavigatorCatalogueMapView
1467
1477
  msgid "Tools per country"
@@ -393,6 +393,11 @@ msgstr ""
393
393
  msgid "Cycle"
394
394
  msgstr ""
395
395
 
396
+ #. Default: "Data sources"
397
+ #: components/theme/Views/ExtendedToolView
398
+ msgid "Data sources"
399
+ msgstr ""
400
+
396
401
  #. Default: "Date of creation:"
397
402
  #: components/theme/Content/Sections/ContentMetadata
398
403
  msgid "Date of creation:"
@@ -1462,6 +1467,11 @@ msgstr ""
1462
1467
  msgid "Tool"
1463
1468
  msgstr "Orodje"
1464
1469
 
1470
+ #. Default: "Tool validation use"
1471
+ #: components/theme/Views/ExtendedToolView
1472
+ msgid "Tool validation use"
1473
+ msgstr ""
1474
+
1465
1475
  #. Default: "Tools per country"
1466
1476
  #: components/Search/NavigatorCatalogue/NavigatorCatalogueMapView
1467
1477
  msgid "Tools per country"
@@ -393,6 +393,11 @@ msgstr ""
393
393
  msgid "Cycle"
394
394
  msgstr ""
395
395
 
396
+ #. Default: "Data sources"
397
+ #: components/theme/Views/ExtendedToolView
398
+ msgid "Data sources"
399
+ msgstr ""
400
+
396
401
  #. Default: "Date of creation:"
397
402
  #: components/theme/Content/Sections/ContentMetadata
398
403
  msgid "Date of creation:"
@@ -1462,6 +1467,11 @@ msgstr ""
1462
1467
  msgid "Tool"
1463
1468
  msgstr "Verktyg"
1464
1469
 
1470
+ #. Default: "Tool validation use"
1471
+ #: components/theme/Views/ExtendedToolView
1472
+ msgid "Tool validation use"
1473
+ msgstr ""
1474
+
1465
1475
  #. Default: "Tools per country"
1466
1476
  #: components/Search/NavigatorCatalogue/NavigatorCatalogueMapView
1467
1477
  msgid "Tools per country"
@@ -393,6 +393,11 @@ msgstr ""
393
393
  msgid "Cycle"
394
394
  msgstr ""
395
395
 
396
+ #. Default: "Data sources"
397
+ #: components/theme/Views/ExtendedToolView
398
+ msgid "Data sources"
399
+ msgstr ""
400
+
396
401
  #. Default: "Date of creation:"
397
402
  #: components/theme/Content/Sections/ContentMetadata
398
403
  msgid "Date of creation:"
@@ -1462,6 +1467,11 @@ msgstr ""
1462
1467
  msgid "Tool"
1463
1468
  msgstr "Araç"
1464
1469
 
1470
+ #. Default: "Tool validation use"
1471
+ #: components/theme/Views/ExtendedToolView
1472
+ msgid "Tool validation use"
1473
+ msgstr ""
1474
+
1465
1475
  #. Default: "Tools per country"
1466
1476
  #: components/Search/NavigatorCatalogue/NavigatorCatalogueMapView
1467
1477
  msgid "Tools per country"
package/locales/volto.pot CHANGED
@@ -1,7 +1,7 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Project-Id-Version: Plone\n"
4
- "POT-Creation-Date: 2026-09-14T12:10:53.707Z\n"
4
+ "POT-Creation-Date: 2026-09-16T07:51:35.558Z\n"
5
5
  "Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
6
6
  "Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
7
7
  "Content-Type: text/plain; charset=utf-8\n"
@@ -395,6 +395,11 @@ msgstr ""
395
395
  msgid "Cycle"
396
396
  msgstr ""
397
397
 
398
+ #. Default: "Data sources"
399
+ #: components/theme/Views/ExtendedToolView
400
+ msgid "Data sources"
401
+ msgstr ""
402
+
398
403
  #. Default: "Date of creation:"
399
404
  #: components/theme/Content/Sections/ContentMetadata
400
405
  msgid "Date of creation:"
@@ -1464,6 +1469,11 @@ msgstr ""
1464
1469
  msgid "Tool"
1465
1470
  msgstr ""
1466
1471
 
1472
+ #. Default: "Tool validation use"
1473
+ #: components/theme/Views/ExtendedToolView
1474
+ msgid "Tool validation use"
1475
+ msgstr ""
1476
+
1467
1477
  #. Default: "Tools per country"
1468
1478
  #: components/Search/NavigatorCatalogue/NavigatorCatalogueMapView
1469
1479
  msgid "Tools per country"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -54,6 +54,23 @@ const publicationDateFormatter = new Intl.DateTimeFormat('en-GB', {
54
54
  timeZone: 'UTC',
55
55
  });
56
56
 
57
+ const formatPublicationDate = (value) => {
58
+ if (!value) return '';
59
+ const raw =
60
+ typeof value === 'object' && value !== null && 'raw' in value
61
+ ? value.raw
62
+ : value;
63
+ const val = Array.isArray(raw) ? raw[0] : raw;
64
+ if (!val || typeof val === 'object') return '';
65
+ try {
66
+ const d = new Date(val);
67
+ if (Number.isNaN(d.getTime())) return '';
68
+ return publicationDateFormatter.format(d);
69
+ } catch {
70
+ return '';
71
+ }
72
+ };
73
+
57
74
  const TagGroup = ({ typeLabel, values, type }) => {
58
75
  const visible = values.slice(0, 3);
59
76
  const hidden = values.slice(3);
@@ -117,7 +134,7 @@ const CycleElements = ({ intl, values }) => {
117
134
  };
118
135
 
119
136
  const NavigatorCatalogueCardItem = (props) => {
120
- const { result } = props;
137
+ const { result = {} } = props;
121
138
  const intl = useIntl();
122
139
  const sectors = rawValueAsArray(result.cca_adaptation_sectors);
123
140
  const hazards = rawValueAsArray(result.cca_climate_impacts);
@@ -131,13 +148,14 @@ const NavigatorCatalogueCardItem = (props) => {
131
148
  const adaptationSupportCycleSteps = rawValueAsArray(
132
149
  result.adaptation_support_cycle_step,
133
150
  )
134
- .map((value) => value?.title?.split(':')[0])
151
+ .map((value) => {
152
+ const title = value?.title || value;
153
+ return typeof title === 'string' ? title.split(':')[0] : title;
154
+ })
135
155
  .filter(Boolean);
136
156
  const publicationDate =
137
157
  result.publication_date?.raw || result.publication_date;
138
- const formattedPublicationDate = publicationDate
139
- ? publicationDateFormatter.format(new Date(publicationDate))
140
- : '';
158
+ const formattedPublicationDate = formatPublicationDate(publicationDate);
141
159
  const sectorLabel = intl.formatMessage(messages.sector);
142
160
  const hazardLabel = intl.formatMessage(messages.hazard);
143
161
  const compareTool = {
@@ -175,6 +175,50 @@ describe('NavigatorCatalogueCardItem', () => {
175
175
  expect(screen.queryByText('License:')).not.toBeInTheDocument();
176
176
  });
177
177
 
178
+ it('safely handles invalid, array, or malformed publication dates without throwing RangeError', () => {
179
+ // Malformed/invalid date string that would throw RangeError: Invalid time value
180
+ const { rerender } = renderCard({
181
+ title: 'Invalid date tool',
182
+ publication_date: 'N/A',
183
+ });
184
+ expect(screen.getByText('Invalid date tool')).toBeInTheDocument();
185
+ expect(screen.queryByText('N/A')).not.toBeInTheDocument();
186
+
187
+ // Array publication date
188
+ rerender(
189
+ <IntlProvider locale="en">
190
+ <NavigatorCatalogueCardItem
191
+ result={{
192
+ title: 'Array date tool',
193
+ publication_date: { raw: ['2026-07-24'] },
194
+ }}
195
+ />
196
+ </IntlProvider>,
197
+ );
198
+ expect(screen.getByText('24 Jul 26')).toBeInTheDocument();
199
+
200
+ // Malformed object publication date
201
+ rerender(
202
+ <IntlProvider locale="en">
203
+ <NavigatorCatalogueCardItem
204
+ result={{
205
+ title: 'Object date tool',
206
+ publication_date: { raw: { invalid: true } },
207
+ }}
208
+ />
209
+ </IntlProvider>,
210
+ );
211
+ expect(screen.getByText('Object date tool')).toBeInTheDocument();
212
+ });
213
+
214
+ it('safely handles non-string adaptationSupportCycleSteps without throwing', () => {
215
+ renderCard({
216
+ title: 'Numeric cycle tool',
217
+ adaptation_support_cycle_step: { raw: [123, null, { title: 456 }] },
218
+ });
219
+ expect(screen.getByText('Numeric cycle tool')).toBeInTheDocument();
220
+ });
221
+
178
222
  describe('thumbnail rendering and fallback', () => {
179
223
  it('renders thumbnail image and smoothly transitions from placeholder on load', () => {
180
224
  const { container } = renderCard({
@@ -1,6 +1,4 @@
1
1
  import React, { useContext, memo } from 'react';
2
- import { Icon } from 'semantic-ui-react';
3
- import ExternalLink from '@eeacms/search/components/Result/ExternalLink';
4
2
 
5
3
  import NavigatorCatalogueCardItem from '@eeacms/volto-cca-policy/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem';
6
4
  // The `ChatMessageContext` seam only exists in @eeacms/volto-eea-chatbot from
@@ -54,137 +52,58 @@ export function matchesDocumentTitle(docTitle, searchTitle) {
54
52
  return false;
55
53
  }
56
54
 
57
- function formatDate(value) {
58
- if (!value) return null;
59
- const d = new Date(value);
60
- if (Number.isNaN(d.getTime())) return null;
61
- return d.toLocaleDateString('en-GB', {
62
- day: '2-digit',
63
- month: 'short',
64
- year: '2-digit',
65
- });
55
+ function sourcePropsEqual(prev, next) {
56
+ return prev.source?.link === next.source?.link;
66
57
  }
67
58
 
68
- // `InlineDocCard` rebuilds the `source` object on every render (the answer is
69
- // re-rendered on each streaming tick), so a plain `===` prop comparison would
70
- // never memoize. Compare the actual fields instead.
71
- function sourceEqual(prevSource, nextSource) {
72
- return (
73
- prevSource?.semantic_identifier === nextSource?.semantic_identifier &&
74
- prevSource?.blurb === nextSource?.blurb &&
75
- prevSource?.updated_at === nextSource?.updated_at &&
76
- prevSource?.source_type === nextSource?.source_type &&
77
- prevSource?.link === nextSource?.link
78
- );
79
- }
59
+ export class DocCardErrorBoundary extends React.Component {
60
+ constructor(props) {
61
+ super(props);
62
+ this.state = { hasError: false };
63
+ }
80
64
 
81
- function sourcePropsEqual(prev, next) {
82
- return prev.index === next.index && sourceEqual(prev.source, next.source);
65
+ static getDerivedStateFromError() {
66
+ return { hasError: true };
67
+ }
68
+
69
+ componentDidCatch(error, info) {
70
+ // eslint-disable-next-line no-console
71
+ console.warn('DocCard failed to render rich catalogue card:', error, info);
72
+ }
73
+
74
+ render() {
75
+ if (this.state.hasError) {
76
+ return this.props.fallback ?? null;
77
+ }
78
+ return this.props.children;
79
+ }
83
80
  }
84
81
 
85
82
  /**
86
- * Document card styled to match the Navigator catalogue cards
87
- * (volto-cca-policy NavigatorCatalogueCardItem), used when full searchlib
88
- * metadata is not yet loaded or for external/unindexed sources.
83
+ * Document card rendered once the globalsearch ES lookup (by document URL)
84
+ * resolves as the full Navigator catalogue card.
85
+ *
86
+ * If the document is not found in Elasticsearch (or lookup fails), no card
87
+ * is displayed.
89
88
  */
90
- const DocumentCard = memo(function DocumentCard({ source, index }) {
91
- if (!source || typeof source !== 'object' || !source.semantic_identifier) {
89
+ const EnhancedDocCard = memo(function EnhancedDocCard({ source }) {
90
+ const { result } = useCatalogueDoc(source?.link);
91
+
92
+ if (!result) {
92
93
  return null;
93
94
  }
94
- const {
95
- semantic_identifier: rawTitle,
96
- blurb,
97
- updated_at,
98
- source_type,
99
- link,
100
- } = source;
101
- const isWeb = source_type === 'web';
102
- const title = cleanDocumentTitle(rawTitle);
103
- const formattedDate = formatDate(updated_at);
104
- const typeLabel = isWeb ? 'Web' : 'Document';
105
95
 
106
96
  return (
107
- <div className="catalogue-chat-navigator-card">
108
- <div className="navigator-catalogue-item">
109
- <div className="navigator-tool-icon large" aria-hidden="true">
110
- <Icon className="ri-file-line" />
111
- </div>
112
-
113
- <div className="catalogue-item-main">
114
- <div className="catalogue-item-top">
115
- <div className="navigator-tool-provider">
116
- {typeof index === 'number' && (
117
- <span className="chat-citation">{index} </span>
118
- )}
119
- {typeLabel}
120
- </div>
121
- {formattedDate && (
122
- <span className="catalogue-date">{formattedDate}</span>
123
- )}
124
- </div>
125
-
126
- <div className="catalogue-item-heading">
127
- <h4>
128
- {link ? (
129
- <ExternalLink href={link} title={title}>
130
- {title}
131
- </ExternalLink>
132
- ) : (
133
- <span title={title}>{title}</span>
134
- )}
135
- </h4>
136
- </div>
137
-
138
- {blurb && <p className="catalogue-description">{blurb}</p>}
139
-
140
- <div className="catalogue-item-footer">
141
- <div className="catalogue-meta license-type">
142
- <span className="catalogue-type">Type: {typeLabel}</span>
143
- </div>
144
-
145
- <div className="catalogue-actions">
146
- {link && (
147
- <ExternalLink
148
- href={link}
149
- className="ui button primary icon"
150
- labelPosition="left"
151
- >
152
- View
153
- <Icon className="ri-arrow-right-line" />
154
- </ExternalLink>
155
- )}
156
- </div>
157
- </div>
97
+ <DocCardErrorBoundary fallback={null}>
98
+ <div className="catalogue-chat-card-inline">
99
+ <div className="catalogue-chat-navigator-card">
100
+ <NavigatorCatalogueCardItem result={result} />
158
101
  </div>
159
102
  </div>
160
- </div>
103
+ </DocCardErrorBoundary>
161
104
  );
162
105
  }, sourcePropsEqual);
163
106
 
164
- /**
165
- * Document card that upgrades itself: while it has only the Onyx-provided
166
- * fields it renders the basic `DocumentCard`; once the globalsearch ES
167
- * lookup (by document URL) resolves it re-renders as the full Navigator
168
- * catalogue card.
169
- *
170
- * Memoized: the answer markdown is re-rendered on every streaming tick, but
171
- * the (already-shown, potentially heavy) card should only re-render when its
172
- * own data changes, not on every tick. Its own basic→full upgrade is driven
173
- * by internal state, which still re-renders it exactly once.
174
- */
175
- const EnhancedDocCard = memo(function EnhancedDocCard({ source, index }) {
176
- const { result } = useCatalogueDoc(source?.link);
177
-
178
- if (result) {
179
- return (
180
- <div className="catalogue-chat-navigator-card">
181
- <NavigatorCatalogueCardItem result={result} />
182
- </div>
183
- );
184
- }
185
- return <DocumentCard source={source} index={index} />;
186
- }, sourcePropsEqual);
187
-
188
107
  /**
189
108
  * Inline document card, rendered in the message text where the assistant
190
109
  * emitted a `![[doc: Title]]` marker (see docCards.js). Tries to match the
@@ -204,11 +123,7 @@ export function InlineDocCard({ title, documents = [] }) {
204
123
  source_type: doc?.source_type,
205
124
  link: doc?.link,
206
125
  };
207
- return (
208
- <div className="catalogue-chat-card-inline">
209
- <EnhancedDocCard source={source} />
210
- </div>
211
- );
126
+ return <EnhancedDocCard source={source} />;
212
127
  }
213
128
 
214
129
  /**