@eeacms/volto-cca-policy 1.0.18 → 1.0.19

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.0.19](https://github.com/eea/volto-cca-policy/compare/1.0.18...1.0.19) - 10 September 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: enhance comparison table with new output type and adaptation support cycle step features [kreafox - [`40e56ad`](https://github.com/eea/volto-cca-policy/commit/40e56adb144e283ce18bf36e87db5c36f286ab9c)]
12
+ - feat: enhance CompareToolsView with new comparison options and styling [kreafox - [`a912b50`](https://github.com/eea/volto-cca-policy/commit/a912b5006af55aefbd024b494cd9c810bfb22684)]
13
+
14
+ #### :nail_care: Enhancements
15
+
16
+ - change: comparison table export by removing unused fields and improve export functionality [kreafox - [`f9ee0f7`](https://github.com/eea/volto-cca-policy/commit/f9ee0f74d02153b9a233ef13264c026503534038)]
17
+ - change: update comparison options handling and enhance tests for adaptation support cycle steps [kreafox - [`fc2736b`](https://github.com/eea/volto-cca-policy/commit/fc2736b293e938b3e13367d96d3c8158c5435f49)]
18
+
19
+ #### :hammer_and_wrench: Others
20
+
21
+ - test: improve test readability and structure in Compare Tools utilities [kreafox - [`b6fc550`](https://github.com/eea/volto-cca-policy/commit/b6fc550db8f3ddbadf093c56596968caf94900ad)]
7
22
  ### [1.0.18](https://github.com/eea/volto-cca-policy/compare/1.0.17...1.0.18) - 10 September 2026
8
23
 
9
24
  #### :rocket: New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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",
@@ -124,12 +124,6 @@ export const exportComparisonTable = (tools, getToolField) => {
124
124
  formatFunctionalityScore(getToolField(tool, 'functionality')),
125
125
  ),
126
126
  ],
127
- [
128
- 'Spatial scale',
129
- ...tools.map((tool) =>
130
- arrayFieldToString(getToolField(tool, 'cca_geographical_scale')),
131
- ),
132
- ],
133
127
  [
134
128
  'Output type',
135
129
  ...tools.map((tool) =>
@@ -144,12 +138,6 @@ export const exportComparisonTable = (tools, getToolField) => {
144
138
  ),
145
139
  ),
146
140
  ],
147
- [
148
- 'Sector',
149
- ...tools.map((tool) =>
150
- arrayFieldToString(getToolField(tool, 'cca_adaptation_sectors')),
151
- ),
152
- ],
153
141
  ];
154
142
 
155
143
  downloadCsv('navigator-catalogue-comparison.csv', rows);
@@ -104,10 +104,8 @@ describe('Navigator Catalogue utilities', () => {
104
104
  values: {
105
105
  accessibility_and_usability: 'High',
106
106
  functionality: 5,
107
- cca_geographical_scale: ['Local'],
108
107
  cca_type_of_outputs: ['Maps'],
109
108
  cca_adaptation_support_cycle_step: ['Step 1'],
110
- cca_adaptation_sectors: ['Water'],
111
109
  },
112
110
  },
113
111
  ];
@@ -118,9 +116,7 @@ describe('Navigator Catalogue utilities', () => {
118
116
  expect(csv).toContain('"Criteria","Tool A"');
119
117
  expect(csv).toContain('"Usability","High"');
120
118
  expect(csv).toContain('"Functionality","5/6"');
121
- expect(csv).toContain('"Spatial scale","Local"');
122
119
  expect(csv).toContain('"Output type","Maps"');
123
120
  expect(csv).toContain('"Adaptation support cycle step","Step 1"');
124
- expect(csv).toContain('"Sector","Water"');
125
121
  });
126
122
  });
@@ -92,6 +92,14 @@ const messages = defineMessages({
92
92
  id: 'Spatial scale',
93
93
  defaultMessage: 'Spatial scale',
94
94
  },
95
+ available: {
96
+ id: 'Available',
97
+ defaultMessage: 'Available',
98
+ },
99
+ unavailable: {
100
+ id: 'Not available',
101
+ defaultMessage: 'Not available',
102
+ },
95
103
  outputType: {
96
104
  id: 'Output type',
97
105
  defaultMessage: 'Output type',
@@ -108,36 +116,99 @@ const getToolField = (tool, field) =>
108
116
  const getToolFieldDisplay = (tool, field) =>
109
117
  asArray(getToolField(tool, field)).join(', ') || '—';
110
118
 
111
- const MetadataTags = ({ value }) => {
112
- const intl = useIntl();
113
- const items = asArray(value)
114
- .map((item) => item?.title || item?.token || item)
115
- .filter(Boolean);
116
-
117
- return items.length ? (
118
- <div className="metadata-tags">
119
- {items.map((item, index) => (
120
- <span className="metadata-tag" key={`${item}-${index}`}>
121
- {intl.formatMessage({ id: item, defaultMessage: item })}
122
- </span>
123
- ))}
124
- </div>
125
- ) : (
126
- '—'
119
+ const getMetadataItem = (item) => item?.title || item?.token || item;
120
+
121
+ const getComparisonOptions = (tools, field) => {
122
+ const fetchedOptions = tools[0]?.comparisonOptions?.[field];
123
+ const values = fetchedOptions?.length
124
+ ? fetchedOptions
125
+ : tools.flatMap((tool) => asArray(getToolField(tool, field)));
126
+
127
+ return [...new Set(values.map(getMetadataItem).filter(Boolean))].sort(
128
+ (first, second) =>
129
+ first.localeCompare(second, undefined, { numeric: true }),
127
130
  );
128
131
  };
129
132
 
130
- const FieldValueList = ({ value, label }) => {
131
- const values = asArray(value);
133
+ const getApplicableComparisonOptions = (tools, field) =>
134
+ [
135
+ ...new Set(
136
+ tools
137
+ .flatMap((tool) => asArray(getToolField(tool, field)))
138
+ .map(getMetadataItem)
139
+ .filter(Boolean),
140
+ ),
141
+ ].sort((first, second) =>
142
+ first.localeCompare(second, undefined, { numeric: true }),
143
+ );
132
144
 
133
- return values.length ? (
134
- <ul className="compare-field-value-list" aria-label={label}>
135
- {values.map((item, index) => (
136
- <li key={`${item}-${index}`}>{item}</li>
145
+ const getExportTools = (tools) => {
146
+ const comparisonOptions = {
147
+ ...(tools[0]?.comparisonOptions || {}),
148
+ cca_type_of_outputs: getApplicableComparisonOptions(
149
+ tools,
150
+ 'cca_type_of_outputs',
151
+ ),
152
+ cca_adaptation_support_cycle_step: getApplicableComparisonOptions(
153
+ tools,
154
+ 'cca_adaptation_support_cycle_step',
155
+ ),
156
+ };
157
+
158
+ return tools.map((tool) => ({
159
+ ...tool,
160
+ comparisonOptions,
161
+ }));
162
+ };
163
+
164
+ const ComparisonRows = ({ tools, field, label, options }) => {
165
+ const intl = useIntl();
166
+ const selectedItems = tools.map(
167
+ (tool) => new Set(asArray(getToolField(tool, field)).map(getMetadataItem)),
168
+ );
169
+
170
+ return (
171
+ <Table.Body>
172
+ {(options.length ? options : [null]).map((item, index) => (
173
+ <Table.Row key={item || field}>
174
+ {index === 0 && (
175
+ <Table.Cell as="th" scope="rowgroup" rowSpan={options.length || 1}>
176
+ <div className="compare-criteria-title">{label}</div>
177
+ </Table.Cell>
178
+ )}
179
+ {tools.map((tool, toolIndex) =>
180
+ item ? (
181
+ <React.Fragment key={tool.id}>
182
+ <Table.Cell
183
+ className={`compare-option-label${
184
+ selectedItems[toolIndex].has(item) ? ' available' : ''
185
+ }`}
186
+ >
187
+ {intl.formatMessage({ id: item, defaultMessage: item })}
188
+ </Table.Cell>
189
+ <Table.Cell className="compare-option-status">
190
+ {selectedItems[toolIndex].has(item) ? (
191
+ <Icon
192
+ className="ri-check-line"
193
+ aria-label={intl.formatMessage(messages.available)}
194
+ />
195
+ ) : (
196
+ <Icon
197
+ className="ri-subtract-line"
198
+ aria-label={intl.formatMessage(messages.unavailable)}
199
+ />
200
+ )}
201
+ </Table.Cell>
202
+ </React.Fragment>
203
+ ) : (
204
+ <Table.Cell key={tool.id} colSpan={2}>
205
+
206
+ </Table.Cell>
207
+ ),
208
+ )}
209
+ </Table.Row>
137
210
  ))}
138
- </ul>
139
- ) : (
140
- '—'
211
+ </Table.Body>
141
212
  );
142
213
  };
143
214
 
@@ -185,6 +256,7 @@ const getToolHref = (result) => result?.href || result?._result?.id?.raw || '';
185
256
  const getTools = async (uids, registry) => {
186
257
  try {
187
258
  const results = await fetchResultsByUid(uids, registry);
259
+ const comparisonOptions = results.comparisonOptions || {};
188
260
  const resultsByUid = new Map(
189
261
  results.map((result) => [getCompareToolUid(result), result]),
190
262
  );
@@ -198,6 +270,7 @@ const getTools = async (uids, registry) => {
198
270
  href: getToolHref(result),
199
271
  result,
200
272
  error: !result,
273
+ comparisonOptions,
201
274
  };
202
275
  });
203
276
  } catch {
@@ -220,6 +293,7 @@ const CompareToolsView = () => {
220
293
  () => getCompareUids(location.search),
221
294
  [location.search],
222
295
  );
296
+
223
297
  const registry = config.settings.searchlib;
224
298
  const landingPageURL = getNavigatorCataloguePageURL(currentLang);
225
299
  const compareToolsTitle = intl.formatMessage(messages.compareTools);
@@ -291,6 +365,15 @@ const CompareToolsView = () => {
291
365
 
292
366
  const failedTools = tools.filter((tool) => tool.error);
293
367
  const visibleTools = tools.filter((tool) => !tool.error);
368
+ const outputOptions = getComparisonOptions(
369
+ visibleTools,
370
+ 'cca_type_of_outputs',
371
+ );
372
+ const cycleOptions = getComparisonOptions(
373
+ visibleTools,
374
+ 'cca_adaptation_support_cycle_step',
375
+ );
376
+
294
377
  const visibleToolsCount = visibleTools.length;
295
378
  const hasLoadedRequestedTools = tools.length === ids.length;
296
379
  const hasEnoughTools = visibleToolsCount >= 2;
@@ -345,10 +428,16 @@ const CompareToolsView = () => {
345
428
  <Icon className="ri-arrow-left-line" />
346
429
  {intl.formatMessage(messages.backToResults)}
347
430
  </Button>
431
+
348
432
  <Button
349
433
  className="primary inverted"
350
434
  disabled={!hasEnoughTools}
351
- onClick={() => exportComparisonTable(visibleTools, getToolField)}
435
+ onClick={() =>
436
+ exportComparisonTable(
437
+ getExportTools(visibleTools),
438
+ getToolField,
439
+ )
440
+ }
352
441
  >
353
442
  <Icon className="ri-download-2-line" />
354
443
  {intl.formatMessage(messages.exportTable)}
@@ -372,6 +461,16 @@ const CompareToolsView = () => {
372
461
 
373
462
  {!isLoading && hasEnoughTools && (
374
463
  <Table celled aria-label={compareToolsTitle} unstackable>
464
+ <colgroup>
465
+ <col className="compare-criteria-column" />
466
+ {visibleTools.map((tool) => (
467
+ <React.Fragment key={tool.id}>
468
+ <col />
469
+ <col className="compare-status-column" />
470
+ </React.Fragment>
471
+ ))}
472
+ </colgroup>
473
+
375
474
  <Table.Header>
376
475
  <Table.Row>
377
476
  <Table.HeaderCell>
@@ -379,15 +478,18 @@ const CompareToolsView = () => {
379
478
  {intl.formatMessage(messages.criteria)}
380
479
  </span>
381
480
  </Table.HeaderCell>
481
+
382
482
  {visibleTools.map((tool) => (
383
- <Table.HeaderCell key={tool.id}>
483
+ <Table.HeaderCell key={tool.id} colSpan={2}>
384
484
  <div className="compare-tool-header">
385
485
  <div className="compare-tool-title-row">
386
486
  <ToolThumbnail result={tool.result} />
487
+
387
488
  <div className="compare-tool-title" title={tool.title}>
388
489
  {tool.title}
389
490
  </div>
390
491
  </div>
492
+
391
493
  <div className="compare-tool-actions">
392
494
  {tool.href && (
393
495
  <UniversalLink
@@ -398,6 +500,7 @@ const CompareToolsView = () => {
398
500
  <Icon className="ri-external-link-line" />
399
501
  </UniversalLink>
400
502
  )}
503
+
401
504
  <Button
402
505
  className="icon compare-tool-clear"
403
506
  aria-label={intl.formatMessage(messages.removeTool, {
@@ -413,6 +516,7 @@ const CompareToolsView = () => {
413
516
  ))}
414
517
  </Table.Row>
415
518
  </Table.Header>
519
+
416
520
  <Table.Body>
417
521
  <Table.Row>
418
522
  <Table.Cell as="th" scope="row">
@@ -422,14 +526,16 @@ const CompareToolsView = () => {
422
526
  </div>
423
527
  </div>
424
528
  </Table.Cell>
529
+
425
530
  {visibleTools.map((tool) => (
426
- <Table.Cell key={`usability-${tool.id}`}>
531
+ <Table.Cell key={`usability-${tool.id}`} colSpan={2}>
427
532
  <div className="usability-value">
428
533
  {getToolFieldDisplay(tool, 'accessibility_and_usability')}
429
534
  </div>
430
535
  </Table.Cell>
431
536
  ))}
432
537
  </Table.Row>
538
+
433
539
  <Table.Row>
434
540
  <Table.Cell as="th" scope="row">
435
541
  <div className="compare-criteria">
@@ -438,8 +544,9 @@ const CompareToolsView = () => {
438
544
  </div>
439
545
  </div>
440
546
  </Table.Cell>
547
+
441
548
  {visibleTools.map((tool) => (
442
- <Table.Cell key={`functionality-${tool.id}`}>
549
+ <Table.Cell key={`functionality-${tool.id}`} colSpan={2}>
443
550
  <div className="functionality-value">
444
551
  <FunctionalityScore
445
552
  value={getToolField(tool, 'functionality')}
@@ -448,59 +555,21 @@ const CompareToolsView = () => {
448
555
  </Table.Cell>
449
556
  ))}
450
557
  </Table.Row>
451
- {/* <Table.Row>
452
- <Table.Cell as="th" scope="row">
453
- <div className="compare-criteria">
454
- <div className="compare-criteria-title">
455
- {intl.formatMessage(messages.spatialScale)}
456
- </div>
457
- </div>
458
- </Table.Cell>
459
- {visibleTools.map((tool) => (
460
- <Table.Cell key={`spatial-scale-${tool.id}`}>
461
- {getToolFieldDisplay(tool, 'cca_geographical_scale')}
462
- </Table.Cell>
463
- ))}
464
- </Table.Row> */}
465
- <Table.Row>
466
- <Table.Cell as="th" scope="row">
467
- <div className="compare-criteria">
468
- <div className="compare-criteria-title">
469
- {intl.formatMessage(messages.outputType)}
470
- </div>
471
- </div>
472
- </Table.Cell>
473
- {visibleTools.map((tool) => (
474
- <Table.Cell key={`output-type-${tool.id}`}>
475
- <MetadataTags
476
- value={getToolField(tool, 'cca_type_of_outputs')}
477
- />
478
- </Table.Cell>
479
- ))}
480
- </Table.Row>
481
- <Table.Row>
482
- <Table.Cell as="th" scope="row">
483
- <div className="compare-criteria">
484
- <div className="compare-criteria-title">
485
- {intl.formatMessage(messages.adaptationSupportCycleStep)}
486
- </div>
487
- </div>
488
- </Table.Cell>
489
- {visibleTools.map((tool) => (
490
- <Table.Cell key={`adaptation-support-cycle-step-${tool.id}`}>
491
- <FieldValueList
492
- label={intl.formatMessage(
493
- messages.adaptationSupportCycleStep,
494
- )}
495
- value={getToolField(
496
- tool,
497
- 'cca_adaptation_support_cycle_step',
498
- )}
499
- />
500
- </Table.Cell>
501
- ))}
502
- </Table.Row>
503
558
  </Table.Body>
559
+
560
+ <ComparisonRows
561
+ tools={visibleTools}
562
+ field="cca_type_of_outputs"
563
+ label={intl.formatMessage(messages.outputType)}
564
+ options={outputOptions}
565
+ />
566
+
567
+ <ComparisonRows
568
+ tools={visibleTools}
569
+ field="cca_adaptation_support_cycle_step"
570
+ label={intl.formatMessage(messages.adaptationSupportCycleStep)}
571
+ options={cycleOptions}
572
+ />
504
573
  </Table>
505
574
  )}
506
575
  </Container>
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import '@testing-library/jest-dom';
3
- import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { fireEvent, render, screen, within } from '@testing-library/react';
4
4
  import { IntlProvider } from 'react-intl';
5
5
  import { useAtom } from 'jotai';
6
6
  import { useDispatch, useSelector } from 'react-redux';
@@ -69,7 +69,7 @@ describe('CompareToolsView', () => {
69
69
  hash: '',
70
70
  state: {},
71
71
  });
72
- fetchResultsByUid.mockResolvedValue([
72
+ const results = [
73
73
  {
74
74
  cca_uid: { raw: 'one' },
75
75
  title: 'Tool one',
@@ -78,8 +78,14 @@ describe('CompareToolsView', () => {
78
78
  scales: { thumb: { download: '/uploaded-compare-thumb.jpg' } },
79
79
  },
80
80
  functionality: { raw: 4 },
81
+ cca_type_of_outputs: { raw: ['Maps and graphs'] },
81
82
  cca_adaptation_support_cycle_step: {
82
- raw: [{ title: 'Assessing risks' }, { title: 'Monitoring' }],
83
+ raw: [
84
+ {
85
+ title:
86
+ 'Step 2: Assessing Climate Change Risks and Vulnerabilities',
87
+ },
88
+ ],
83
89
  },
84
90
  },
85
91
  {
@@ -88,7 +94,16 @@ describe('CompareToolsView', () => {
88
94
  href: '/tool-two',
89
95
  image: null,
90
96
  },
91
- ]);
97
+ ];
98
+ results.comparisonOptions = {
99
+ cca_type_of_outputs: ['Reports and decision support', 'Maps and graphs'],
100
+ cca_adaptation_support_cycle_step: [
101
+ 'Step 10: Additional step from the catalogue',
102
+ 'Step 2: Assessing Climate Change Risks and Vulnerabilities',
103
+ 'Step 1: Preparing the Ground for Adaptation',
104
+ ],
105
+ };
106
+ fetchResultsByUid.mockResolvedValue(results);
92
107
  });
93
108
 
94
109
  it('labels the table, row headers, and remove actions', async () => {
@@ -117,13 +132,61 @@ describe('CompareToolsView', () => {
117
132
  expect(
118
133
  functionalityScore.querySelectorAll('.functionality-dot.filled'),
119
134
  ).toHaveLength(4);
120
- const adaptationSteps = screen.getByRole('list', {
135
+ const cycleHeader = screen.getByRole('rowheader', {
121
136
  name: 'Adaptation support cycle step',
122
137
  });
123
- expect(adaptationSteps).toHaveClass('compare-field-value-list');
124
- expect(adaptationSteps).toHaveTextContent('Assessing risks');
125
- expect(adaptationSteps).toHaveTextContent('Monitoring');
126
- expect(adaptationSteps.children).toHaveLength(2);
138
+ expect(cycleHeader).toHaveAttribute('scope', 'rowgroup');
139
+ expect(cycleHeader).toHaveAttribute('rowspan', '3');
140
+ const cycleRows = within(cycleHeader.closest('tbody')).getAllByRole('row');
141
+ expect(cycleRows).toHaveLength(3);
142
+ expect(
143
+ within(cycleRows[2]).getAllByText(
144
+ 'Step 10: Additional step from the catalogue',
145
+ ),
146
+ ).toHaveLength(2);
147
+ expect(
148
+ within(cycleRows[1]).getAllByText(
149
+ 'Step 2: Assessing Climate Change Risks and Vulnerabilities',
150
+ ),
151
+ ).toHaveLength(2);
152
+ expect(within(cycleRows[1]).getAllByLabelText('Available')).toHaveLength(1);
153
+ expect(
154
+ within(cycleRows[1]).getAllByLabelText('Not available'),
155
+ ).toHaveLength(1);
156
+ expect(
157
+ within(cycleRows[0]).getAllByLabelText('Not available'),
158
+ ).toHaveLength(2);
159
+ const outputRow = screen
160
+ .getByRole('rowheader', { name: 'Output type' })
161
+ .closest('tr');
162
+ expect(within(outputRow).getAllByText('Maps and graphs')).toHaveLength(2);
163
+ expect(within(outputRow).getAllByLabelText('Available')).toHaveLength(1);
164
+ expect(within(outputRow).getAllByLabelText('Not available')).toHaveLength(
165
+ 1,
166
+ );
167
+ });
168
+
169
+ it('falls back to selected tool values when aggregations are empty', async () => {
170
+ const results = await fetchResultsByUid();
171
+ results.comparisonOptions = {};
172
+ render(
173
+ <IntlProvider locale="en">
174
+ <CompareToolsView />
175
+ </IntlProvider>,
176
+ );
177
+ await screen.findByRole('table', { name: 'Compare tools' });
178
+ const cycleHeader = screen.getByRole('rowheader', {
179
+ name: 'Adaptation support cycle step',
180
+ });
181
+ expect(cycleHeader).toHaveAttribute('rowspan', '1');
182
+ expect(
183
+ within(cycleHeader.closest('tbody')).getAllByText(
184
+ 'Step 2: Assessing Climate Change Risks and Vulnerabilities',
185
+ ),
186
+ ).toHaveLength(2);
187
+ expect(
188
+ screen.queryByText('Step 1: Preparing the Ground for Adaptation'),
189
+ ).not.toBeInTheDocument();
127
190
  });
128
191
 
129
192
  it('shows the uploaded tool result image and keeps the file icon for a missing image', async () => {
@@ -111,11 +111,43 @@ export const fetchResultsByUid = async (uids, registry) => {
111
111
  ],
112
112
  },
113
113
  },
114
+ aggs: {
115
+ allOutputTypes: {
116
+ global: {},
117
+ aggs: {
118
+ values: {
119
+ terms: { field: 'cca_type_of_outputs.keyword', size: 10000 },
120
+ },
121
+ },
122
+ },
123
+ allCycleSteps: {
124
+ global: {},
125
+ aggs: {
126
+ values: {
127
+ terms: {
128
+ field: 'cca_adaptation_support_cycle_step.keyword',
129
+ size: 10000,
130
+ },
131
+ },
132
+ },
133
+ },
134
+ },
114
135
  },
115
136
  appConfig,
116
137
  );
117
138
  const hits = response.body?.hits?.hits || [];
118
139
  const Model = registry.resolve[appConfig.resultItemModel.factory];
140
+ const results = hits.map((hit) => new Model(hit, appConfig));
141
+ results.comparisonOptions = {
142
+ cca_type_of_outputs:
143
+ response.body?.aggregations?.allOutputTypes?.values?.buckets?.map(
144
+ (bucket) => bucket.key,
145
+ ) || [],
146
+ cca_adaptation_support_cycle_step:
147
+ response.body?.aggregations?.allCycleSteps?.values?.buckets?.map(
148
+ (bucket) => bucket.key,
149
+ ) || [],
150
+ };
119
151
 
120
- return hits.map((hit) => new Model(hit, appConfig));
152
+ return results;
121
153
  };
@@ -70,9 +70,11 @@ describe('Compare Tools utilities', () => {
70
70
  it('resolves comparison identifiers and titles from supported shapes', () => {
71
71
  expect(getCompareToolUid({ cca_uid: { raw: 'cca-id' } })).toBe('cca-id');
72
72
  expect(getCompareToolUid({ UID: 'plone-id' })).toBe('plone-id');
73
- expect(getCompareToolUid({ _result: { cca_uid: { raw: 'raw-id' } } })).toBe(
74
- 'raw-id',
75
- );
73
+ expect(
74
+ getCompareToolUid({
75
+ _result: { cca_uid: { raw: 'raw-id' } },
76
+ }),
77
+ ).toBe('raw-id');
76
78
  expect(getCompareToolUid({})).toBe('');
77
79
  expect(getCompareToolTitle({ title: 'Tool title' })).toBe('Tool title');
78
80
  expect(getCompareToolTitle({})).toBe('');
@@ -101,9 +103,13 @@ describe('Compare Tools utilities', () => {
101
103
  ).toEqual({
102
104
  pathname: '/ro/navigator/compare',
103
105
  search: '?uid=one&uid=two&uid=three',
104
- state: { returnURL: '/en/navigator/tool-catalogue?q=test' },
106
+ state: {
107
+ returnURL: '/en/navigator/tool-catalogue?q=test',
108
+ },
105
109
  });
110
+
106
111
  expect(MAX_COMPARE_TOOLS).toBe(4);
112
+
107
113
  expect(getCompareLocation([])).toEqual({
108
114
  pathname: '/en/navigator/compare',
109
115
  search: '',
@@ -118,22 +124,46 @@ describe('Compare Tools utilities', () => {
118
124
  this.config = config;
119
125
  }
120
126
  }
127
+
121
128
  const appConfig = {
122
129
  index_name: 'data_searchui',
123
130
  resultItemModel: { factory: 'resultModel' },
124
131
  };
132
+
125
133
  const registry = {
126
- searchui: { navigatorCatalogueSearch: appConfig },
127
- resolve: { resultModel: ResultModel },
134
+ searchui: {
135
+ navigatorCatalogueSearch: appConfig,
136
+ },
137
+ resolve: {
138
+ resultModel: ResultModel,
139
+ },
128
140
  };
141
+
129
142
  runRequest.mockResolvedValue({
130
- body: { hits: { hits: [{ _id: 'one' }, { _id: 'two' }] } },
143
+ body: {
144
+ hits: {
145
+ hits: [{ _id: 'one' }, { _id: 'two' }],
146
+ },
147
+ aggregations: {
148
+ allOutputTypes: {
149
+ values: {
150
+ buckets: [{ key: 'Map' }, { key: 'Chart' }],
151
+ },
152
+ },
153
+ allCycleSteps: {
154
+ values: {
155
+ buckets: [{ key: 'Assess' }, { key: 'Plan' }],
156
+ },
157
+ },
158
+ },
159
+ },
131
160
  });
132
161
 
133
162
  const results = await fetchResultsByUid(['one', 'two'], registry);
134
163
 
135
164
  expect(rebind).toHaveBeenCalledWith(appConfig);
136
165
  expect(applyConfigurationSchema).toHaveBeenCalledWith(appConfig);
166
+
137
167
  expect(runRequest).toHaveBeenCalledWith(
138
168
  {
139
169
  index: 'data_searchui',
@@ -142,59 +172,127 @@ describe('Compare Tools utilities', () => {
142
172
  bool: {
143
173
  minimum_should_match: 1,
144
174
  should: [
145
- { terms: { 'cca_uid.keyword': ['one', 'two'] } },
146
- { terms: { cca_uid: ['one', 'two'] } },
175
+ {
176
+ terms: {
177
+ 'cca_uid.keyword': ['one', 'two'],
178
+ },
179
+ },
180
+ {
181
+ terms: {
182
+ cca_uid: ['one', 'two'],
183
+ },
184
+ },
147
185
  ],
148
186
  },
149
187
  },
188
+ aggs: {
189
+ allOutputTypes: {
190
+ global: {},
191
+ aggs: {
192
+ values: {
193
+ terms: {
194
+ field: 'cca_type_of_outputs.keyword',
195
+ size: 10000,
196
+ },
197
+ },
198
+ },
199
+ },
200
+ allCycleSteps: {
201
+ global: {},
202
+ aggs: {
203
+ values: {
204
+ terms: {
205
+ field: 'cca_adaptation_support_cycle_step.keyword',
206
+ size: 10000,
207
+ },
208
+ },
209
+ },
210
+ },
211
+ },
150
212
  },
151
213
  appConfig,
152
214
  );
215
+
153
216
  expect(results).toHaveLength(2);
154
217
  expect(results[0]).toBeInstanceOf(ResultModel);
218
+ expect(results[1]).toBeInstanceOf(ResultModel);
219
+
220
+ expect(results.comparisonOptions).toEqual({
221
+ cca_type_of_outputs: ['Map', 'Chart'],
222
+ cca_adaptation_support_cycle_step: ['Assess', 'Plan'],
223
+ });
155
224
  });
156
225
 
157
226
  it('handles missing hits and configurations without an index', async () => {
158
227
  const registry = {
159
228
  searchui: {
160
229
  navigatorCatalogueSearch: {
161
- resultItemModel: { factory: 'resultModel' },
230
+ resultItemModel: {
231
+ factory: 'resultModel',
232
+ },
162
233
  },
163
234
  },
164
- resolve: { resultModel: class ResultModel {} },
235
+ resolve: {
236
+ resultModel: class ResultModel {},
237
+ },
165
238
  };
239
+
166
240
  runRequest.mockResolvedValue({});
167
241
 
168
- await expect(fetchResultsByUid(['one'], registry)).resolves.toEqual([]);
242
+ const results = await fetchResultsByUid(['one'], registry);
243
+
244
+ expect(results).toHaveLength(0);
245
+
246
+ expect(results.comparisonOptions).toEqual({
247
+ cca_type_of_outputs: [],
248
+ cca_adaptation_support_cycle_step: [],
249
+ });
250
+
169
251
  expect(runRequest.mock.calls[0][0]).not.toHaveProperty('index');
170
252
  });
171
253
 
172
254
  it('adds, toggles, and removes a comparison tool', () => {
173
- renderCompareHarness({ uid: 'one', title: 'Tool one' });
255
+ renderCompareHarness({
256
+ uid: 'one',
257
+ title: 'Tool one',
258
+ });
174
259
 
175
260
  expect(screen.getByTestId('selected')).toHaveTextContent('false');
176
261
  expect(screen.getByTestId('limit')).toHaveTextContent('false');
177
262
 
178
263
  fireEvent.click(screen.getByText('Toggle'));
264
+
179
265
  expect(screen.getByTestId('selected')).toHaveTextContent('true');
266
+
180
267
  expect(
181
268
  JSON.parse(window.localStorage.getItem('cca-compare-tools')),
182
- ).toEqual([{ uid: 'one', title: 'Tool one' }]);
269
+ ).toEqual([
270
+ {
271
+ uid: 'one',
272
+ title: 'Tool one',
273
+ },
274
+ ]);
183
275
 
184
276
  fireEvent.click(screen.getByText('Select'));
277
+
185
278
  expect(screen.getByTestId('selected')).toHaveTextContent('true');
186
279
 
187
280
  fireEvent.click(screen.getByText('Remove'));
281
+
188
282
  expect(screen.getByTestId('selected')).toHaveTextContent('false');
283
+
189
284
  expect(
190
285
  JSON.parse(window.localStorage.getItem('cca-compare-tools')),
191
286
  ).toEqual([]);
192
287
  });
193
288
 
194
289
  it('does not add tools without an identifier', () => {
195
- renderCompareHarness({ title: 'Missing UID' });
290
+ renderCompareHarness({
291
+ title: 'Missing UID',
292
+ });
196
293
 
197
294
  fireEvent.click(screen.getByText('Select'));
295
+
198
296
  expect(screen.getByTestId('selected')).toHaveTextContent('false');
199
297
  });
200
298
 
@@ -205,15 +303,26 @@ describe('Compare Tools utilities', () => {
205
303
  uid: `tool-${index}`,
206
304
  }),
207
305
  );
208
- renderCompareHarness({ uid: 'extra' }, initialTools);
306
+
307
+ renderCompareHarness(
308
+ {
309
+ uid: 'extra',
310
+ },
311
+ initialTools,
312
+ );
209
313
 
210
314
  expect(screen.getByTestId('limit')).toHaveTextContent('true');
315
+
211
316
  fireEvent.click(screen.getByText('Select'));
317
+
212
318
  expect(screen.getByTestId('selected')).toHaveTextContent('false');
213
319
  });
214
320
 
215
321
  it('exposes hasMounted state to safely coordinate client hydration', () => {
216
- renderCompareHarness({ uid: 'one', title: 'Tool one' });
322
+ renderCompareHarness({
323
+ uid: 'one',
324
+ title: 'Tool one',
325
+ });
217
326
 
218
327
  expect(screen.getByTestId('mounted')).toHaveTextContent('true');
219
328
  });
@@ -221,10 +330,12 @@ describe('Compare Tools utilities', () => {
221
330
  it('tracks mounting status with useHasMounted', () => {
222
331
  const MountedComponent = () => {
223
332
  const mounted = useHasMounted();
333
+
224
334
  return <div data-testid="mounted-flag">{String(mounted)}</div>;
225
335
  };
226
336
 
227
337
  render(<MountedComponent />);
338
+
228
339
  expect(screen.getByTestId('mounted-flag')).toHaveTextContent('true');
229
340
  });
230
341
  });
@@ -359,13 +359,12 @@
359
359
  }
360
360
  }
361
361
 
362
- .ui.celled.table th:first-child,
363
- .ui.celled.table td:first-child {
362
+ .compare-criteria-column {
364
363
  width: 180px;
365
364
  }
366
365
 
367
366
  .ui.celled.table th:not(:first-child),
368
- .ui.celled.table td:not(:first-child) {
367
+ .ui.celled.table td {
369
368
  overflow-wrap: anywhere;
370
369
  }
371
370
 
@@ -398,10 +397,46 @@
398
397
  }
399
398
  }
400
399
 
401
- .compare-field-value-list {
402
- padding: 0;
403
- margin: 0;
404
- list-style: none;
400
+ .compare-status-column {
401
+ width: 64px;
402
+ }
403
+
404
+ .ui.celled.table {
405
+ td.compare-option-label,
406
+ td.compare-option-status {
407
+ padding: 0.4rem 1.1rem;
408
+ vertical-align: middle;
409
+ }
410
+
411
+ td.compare-option-label {
412
+ border-left: 1px solid @navigatorBorderColor;
413
+
414
+ &.available {
415
+ color: @secondaryColor;
416
+ font-weight: 700;
417
+ }
418
+ }
419
+
420
+ td.compare-option-status {
421
+ padding-right: 0.5rem;
422
+ padding-left: 0.5rem;
423
+ text-align: center;
424
+
425
+ i.icon.ri-check-line {
426
+ color: @secondaryColor;
427
+ }
428
+
429
+ i.icon {
430
+ margin: 0;
431
+ font-size: 24px;
432
+ line-height: 1;
433
+ }
434
+ }
435
+
436
+ tbody + tbody tr:first-child > th,
437
+ tbody + tbody tr:first-child > td {
438
+ border-top: 1px solid @navigatorBorderColor;
439
+ }
405
440
  }
406
441
 
407
442
  .compare-criteria-label {
@@ -666,6 +666,18 @@ hr {
666
666
  font-size: 14px;
667
667
  }
668
668
 
669
+ .navigator-catalogue-compare-view {
670
+ .metadata-tag {
671
+ font-weight: 400;
672
+ }
673
+
674
+ .metadata-tag.selected {
675
+ border-color: @secondaryColor;
676
+ color: @secondaryColor;
677
+ font-weight: 600;
678
+ }
679
+ }
680
+
669
681
  .metadata-tag-more {
670
682
  background: transparent;
671
683
  cursor: pointer;