@eeacms/volto-cca-policy 0.3.117 → 0.3.119

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,45 @@ 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
+ ### [0.3.119](https://github.com/eea/volto-cca-policy/compare/0.3.118...0.3.119) - 30 April 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: add ArchivedVersionListing component and update DatabaseItemView to display archived indicator versions [kreafox - [`858e868`](https://github.com/eea/volto-cca-policy/commit/858e868f124642cd112fea45f079a13dcd27d39e)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - test: add unit tests for ArchivedVersionListing component [kreafox - [`010ef2c`](https://github.com/eea/volto-cca-policy/commit/010ef2c127bf851ab602cb1518d1e1012894e2d9)]
16
+ - test: update ArchivedVersionNotice test cases to use archived_versions instead of relatedItems [kreafox - [`09a7619`](https://github.com/eea/volto-cca-policy/commit/09a761987dc28b710798effb62496eb6d6165543)]
17
+ ### [0.3.118](https://github.com/eea/volto-cca-policy/compare/0.3.117...0.3.118) - 28 April 2026
18
+
19
+ #### :rocket: New Features
20
+
21
+ - feat: Add archived version notice and versions group for indicators [Tiberiu Ichim - [`10a4a52`](https://github.com/eea/volto-cca-policy/commit/10a4a5285cfd352e48300fbc6d8b89652151dda1)]
22
+
23
+ #### :bug: Bug Fixes
24
+
25
+ - fix: Import INDICATOR from correct constants module [Tiberiu Ichim - [`b2c3ae4`](https://github.com/eea/volto-cca-policy/commit/b2c3ae4b43ae69ae5a8da92ed6a6461d1ca2fdba)]
26
+ - fix: Add missing INDICATOR import, comment out unused VersionsGroup import [Tiberiu Ichim - [`0578df8`](https://github.com/eea/volto-cca-policy/commit/0578df8636e9ed4f48b0ed7f72114755c6d12dc4)]
27
+ - fix: Use expandToBackendURL for API POST to avoid 404 [Tiberiu Ichim - [`9eb849c`](https://github.com/eea/volto-cca-policy/commit/9eb849cf2291e80610d50cf73d0b693a8feef051)]
28
+
29
+ #### :nail_care: Enhancements
30
+
31
+ - change: Stay on page and show toast after creating archived copy [Tiberiu Ichim - [`26665ba`](https://github.com/eea/volto-cca-policy/commit/26665ba17d33a48faf3d2d69a6de3e86c2a43cb6)]
32
+
33
+ #### :house: Internal changes
34
+
35
+ - style: Automated code fix [eea-jenkins - [`4c4f710`](https://github.com/eea/volto-cca-policy/commit/4c4f7107cff730f022da14506385f96a410a8654)]
36
+ - style: Automated code fix [eea-jenkins - [`b520b39`](https://github.com/eea/volto-cca-policy/commit/b520b3945fccd8075912cac73aa17faf42dbf49f)]
37
+ - style: Automated code fix [eea-jenkins - [`d8224b1`](https://github.com/eea/volto-cca-policy/commit/d8224b1a96182fa1fdeabe0fb4ca95fb20c27cd5)]
38
+
39
+ #### :hammer_and_wrench: Others
40
+
41
+ - Add tests for archived indicator components [Tiberiu Ichim - [`bf6209e`](https://github.com/eea/volto-cca-policy/commit/bf6209eb71893d8d501af212d54929afe35e1ddc)]
42
+ - Merge develop into archived_indicators [Tiberiu Ichim - [`b269ff4`](https://github.com/eea/volto-cca-policy/commit/b269ff4f157f52b4d837f3818b1c621a5644de59)]
43
+ - comment out VersionsGroup - relatedItems already shows versions [Tiberiu Ichim - [`a86c9d4`](https://github.com/eea/volto-cca-policy/commit/a86c9d4b6d2fbd94a107f76625f11c9aeb557117)]
44
+ - Merge remote automated code fix [Tiberiu Ichim - [`c52789f`](https://github.com/eea/volto-cca-policy/commit/c52789f4c87683fd10690b7a78cd048663b407fe)]
45
+ - Add button [Tiberiu Ichim - [`1ea8db9`](https://github.com/eea/volto-cca-policy/commit/1ea8db90e7ca741700b87caa315c029235b13684)]
7
46
  ### [0.3.117](https://github.com/eea/volto-cca-policy/compare/0.3.116...0.3.117) - 28 April 2026
8
47
 
9
48
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "0.3.117",
3
+ "version": "0.3.119",
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",
@@ -11,3 +11,11 @@ export { default as AccordionList } from './theme/AccordionList/AccordionList';
11
11
  // Widgets
12
12
  export { default as RASTWidgetView } from './theme/Widgets/RASTWidgetView';
13
13
  export { default as ImageWidget } from './theme/Widgets/ImageWidget';
14
+
15
+ // Manage
16
+ export { default as CreateArchivedCopyButton } from './manage/CreateArchivedCopyButton/CreateArchivedCopyButton';
17
+
18
+ // Views
19
+ export { default as ArchivedVersionListing } from './theme/Views/ArchivedVersionListing';
20
+ export { default as ArchivedVersionNotice } from './theme/Views/ArchivedVersionNotice';
21
+ export { default as VersionsGroup } from './theme/Views/VersionsGroup';
@@ -0,0 +1,153 @@
1
+ import React from 'react';
2
+ import { connect } from 'react-redux';
3
+ import { Plug } from '@plone/volto/components/manage/Pluggable';
4
+ import { Modal, Button, Form, Message } from 'semantic-ui-react';
5
+ import superagent from 'superagent';
6
+ import { toast } from 'react-toastify';
7
+ import { Toast } from '@plone/volto/components';
8
+ import { flattenToAppURL, expandToBackendURL } from '@plone/volto/helpers';
9
+ import { INDICATOR } from '@eeacms/volto-cca-policy/constants';
10
+
11
+ function CreateArchivedCopyButton(props) {
12
+ const { content, token } = props;
13
+ const [open, setOpen] = React.useState(false);
14
+ const [title, setTitle] = React.useState('');
15
+ const [id, setId] = React.useState('');
16
+ const [error, setError] = React.useState(null);
17
+ const [loading, setLoading] = React.useState(false);
18
+
19
+ const contentId = content?.['@id'] || '';
20
+ const contentType = content?.['@type'];
21
+ const reviewState = content?.review_state;
22
+ const originalId = content?.id;
23
+
24
+ const show =
25
+ !!token && contentType === INDICATOR && reviewState === 'published';
26
+
27
+ const handleOpen = () => {
28
+ setTitle(content?.title ? `${content.title} (Archived)` : '');
29
+ setId(originalId ? `${originalId}-v2` : '');
30
+ setError(null);
31
+ setOpen(true);
32
+ };
33
+
34
+ const handleClose = () => {
35
+ setOpen(false);
36
+ setError(null);
37
+ };
38
+
39
+ const handleSubmit = async () => {
40
+ if (!id || id === originalId) {
41
+ setError('You must change the ID of the archived copy.');
42
+ return;
43
+ }
44
+
45
+ setLoading(true);
46
+ setError(null);
47
+
48
+ try {
49
+ const url = expandToBackendURL(contentId);
50
+ const response = await superagent
51
+ .post(`${url}/@create-archived-copy`)
52
+ .set('Accept', 'application/json')
53
+ .set('Content-Type', 'application/json')
54
+ .send({ title, id });
55
+
56
+ const newUrl = flattenToAppURL(response.body['@id']);
57
+ setOpen(false);
58
+ setLoading(false);
59
+ setTitle('');
60
+ setId('');
61
+ toast.success(
62
+ <Toast
63
+ success
64
+ title="Archived copy created"
65
+ content={`The archived copy has been created. You can view it here: ${newUrl}`}
66
+ />,
67
+ );
68
+ } catch (err) {
69
+ const message =
70
+ err.response?.body?.message ||
71
+ err.response?.text ||
72
+ err.message ||
73
+ 'An error occurred while creating the archived copy.';
74
+ setError(message);
75
+ setLoading(false);
76
+ }
77
+ };
78
+
79
+ if (!show) return null;
80
+
81
+ return (
82
+ <>
83
+ <Plug
84
+ pluggable="main.toolbar.top"
85
+ id="create-archived-copy"
86
+ order={5}
87
+ dependencies={[contentId]}
88
+ >
89
+ <button
90
+ className="circle-right-btn"
91
+ id="create-archived-copy-btn"
92
+ onClick={handleOpen}
93
+ title="Create an archived copy"
94
+ >
95
+ AC
96
+ </button>
97
+ </Plug>
98
+
99
+ <Modal size="small" open={open} onClose={handleClose} closeIcon>
100
+ <Modal.Header>Create an archived copy</Modal.Header>
101
+ <Modal.Content>
102
+ {error && (
103
+ <Message negative>
104
+ <Message.Header>Error</Message.Header>
105
+ <p>{error}</p>
106
+ </Message>
107
+ )}
108
+ <Form>
109
+ <Form.Field>
110
+ <label>Title</label>
111
+ <input
112
+ value={title}
113
+ onChange={(e) => setTitle(e.target.value)}
114
+ placeholder="Title of the archived copy"
115
+ />
116
+ </Form.Field>
117
+ <Form.Field>
118
+ <label>ID</label>
119
+ <input
120
+ value={id}
121
+ onChange={(e) => setId(e.target.value)}
122
+ placeholder="ID of the archived copy"
123
+ />
124
+ {id === originalId && (
125
+ <small style={{ color: '#db2828' }}>
126
+ The ID must be different from the original indicator.
127
+ </small>
128
+ )}
129
+ </Form.Field>
130
+ </Form>
131
+ </Modal.Content>
132
+ <Modal.Actions>
133
+ <Button onClick={handleClose} disabled={loading}>
134
+ Cancel
135
+ </Button>
136
+ <Button
137
+ primary
138
+ onClick={handleSubmit}
139
+ loading={loading}
140
+ disabled={!id || id === originalId}
141
+ >
142
+ Create
143
+ </Button>
144
+ </Modal.Actions>
145
+ </Modal>
146
+ </>
147
+ );
148
+ }
149
+
150
+ export default connect((state) => ({
151
+ content: state.content.data,
152
+ token: state.userSession.token,
153
+ }))(CreateArchivedCopyButton);
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import configureStore from 'redux-mock-store';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+ import { Provider } from 'react-intl-redux';
6
+ import { render, fireEvent } from '@testing-library/react';
7
+ import CreateArchivedCopyButton from './CreateArchivedCopyButton';
8
+ import { INDICATOR } from '@eeacms/volto-cca-policy/constants';
9
+
10
+ jest.mock('@plone/volto/components/manage/Pluggable', () => ({
11
+ Plug: ({ children }) => <>{children}</>,
12
+ }));
13
+
14
+ jest.mock('superagent', () => ({
15
+ post: jest.fn(() => ({
16
+ set: jest.fn(() => ({
17
+ set: jest.fn(() => ({
18
+ send: jest.fn(() =>
19
+ Promise.resolve({
20
+ body: { '@id': 'http://localhost:3000/my-indicator-v2' },
21
+ }),
22
+ ),
23
+ })),
24
+ })),
25
+ })),
26
+ }));
27
+
28
+ jest.mock('react-toastify', () => ({
29
+ toast: { success: jest.fn(), error: jest.fn() },
30
+ }));
31
+
32
+ jest.mock('@plone/volto/components', () => ({
33
+ Toast: () => null,
34
+ }));
35
+
36
+ jest.mock('@plone/volto/helpers', () => ({
37
+ flattenToAppURL: (url) => url.replace('http://localhost:3000', ''),
38
+ expandToBackendURL: (url) =>
39
+ url.replace('http://localhost:3000', 'http://localhost:8080/Plone'),
40
+ }));
41
+
42
+ const mockStore = configureStore();
43
+
44
+ function makeStore(contentOverrides = {}, token = '1234') {
45
+ return mockStore({
46
+ content: {
47
+ data: {
48
+ '@type': INDICATOR,
49
+ review_state: 'published',
50
+ '@id': 'http://localhost:3000/my-indicator',
51
+ id: 'my-indicator',
52
+ title: 'My Indicator',
53
+ ...contentOverrides,
54
+ },
55
+ },
56
+ userSession: { token },
57
+ intl: { locale: 'en', messages: {} },
58
+ });
59
+ }
60
+
61
+ function renderWithProviders(ui, store) {
62
+ return render(
63
+ <Provider store={store}>
64
+ <MemoryRouter>{ui}</MemoryRouter>
65
+ </Provider>,
66
+ );
67
+ }
68
+
69
+ describe('CreateArchivedCopyButton', () => {
70
+ it('returns null when user is not logged in', () => {
71
+ const store = makeStore({}, null);
72
+ const { container } = renderWithProviders(
73
+ <CreateArchivedCopyButton />,
74
+ store,
75
+ );
76
+ expect(container.innerHTML).toBe('');
77
+ });
78
+
79
+ it('returns null for non-indicator content type', () => {
80
+ const store = makeStore({ '@type': 'Document' });
81
+ const { container } = renderWithProviders(
82
+ <CreateArchivedCopyButton />,
83
+ store,
84
+ );
85
+ expect(container.innerHTML).toBe('');
86
+ });
87
+
88
+ it('returns null for non-published indicator', () => {
89
+ const store = makeStore({ review_state: 'draft' });
90
+ const { container } = renderWithProviders(
91
+ <CreateArchivedCopyButton />,
92
+ store,
93
+ );
94
+ expect(container.innerHTML).toBe('');
95
+ });
96
+
97
+ it('renders the button for a published indicator with a logged-in user', () => {
98
+ const store = makeStore();
99
+ const { container } = renderWithProviders(
100
+ <CreateArchivedCopyButton />,
101
+ store,
102
+ );
103
+ expect(
104
+ container.querySelector('#create-archived-copy-btn'),
105
+ ).toBeInTheDocument();
106
+ expect(container.querySelector('#create-archived-copy-btn').title).toBe(
107
+ 'Create an archived copy',
108
+ );
109
+ });
110
+
111
+ it('opens modal when button is clicked', () => {
112
+ const store = makeStore();
113
+ const { container, getByText } = renderWithProviders(
114
+ <CreateArchivedCopyButton />,
115
+ store,
116
+ );
117
+ fireEvent.click(container.querySelector('#create-archived-copy-btn'));
118
+ expect(getByText('Create an archived copy')).toBeInTheDocument();
119
+ expect(getByText('Cancel')).toBeInTheDocument();
120
+ expect(getByText('Create')).toBeInTheDocument();
121
+ });
122
+
123
+ it('closes modal on cancel', () => {
124
+ const store = makeStore();
125
+ const { container, getByText, queryByText } = renderWithProviders(
126
+ <CreateArchivedCopyButton />,
127
+ store,
128
+ );
129
+ fireEvent.click(container.querySelector('#create-archived-copy-btn'));
130
+ expect(getByText('Create an archived copy')).toBeInTheDocument();
131
+ fireEvent.click(getByText('Cancel'));
132
+ expect(queryByText('Title')).toBeNull();
133
+ });
134
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { flattenToAppURL } from '@plone/volto/helpers';
5
+ import { AccordionList } from '@eeacms/volto-cca-policy/components';
6
+
7
+ function ArchivedVersionListing({ content }) {
8
+ const { archived_versions } = content;
9
+
10
+ const items = archived_versions?.map((version) => (
11
+ <div key={version['@id']}>
12
+ <Link to={flattenToAppURL(version['@id'])}>
13
+ {version.title || flattenToAppURL(version['@id'])}
14
+ </Link>
15
+ </div>
16
+ ));
17
+
18
+ if (!items?.length) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <AccordionList
24
+ variation="secondary"
25
+ accordions={[
26
+ {
27
+ title: (
28
+ <FormattedMessage
29
+ id="Previous versions of this indicator"
30
+ defaultMessage="Previous versions of this indicator"
31
+ />
32
+ ),
33
+ content: items,
34
+ },
35
+ ]}
36
+ />
37
+ );
38
+ }
39
+
40
+ export default ArchivedVersionListing;
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { render, screen } from '@testing-library/react';
5
+ import ArchivedVersionListing from './ArchivedVersionListing';
6
+
7
+ jest.mock('@plone/volto/helpers', () => ({
8
+ flattenToAppURL: (url) => url.replace('http://localhost:3000', ''),
9
+ }));
10
+
11
+ jest.mock('@eeacms/volto-cca-policy/components', () => ({
12
+ AccordionList: ({ accordions }) => (
13
+ <div className="accordion-list">
14
+ {accordions.map((accordion, index) => (
15
+ <div key={index}>
16
+ <div>{accordion.title}</div>
17
+ <div>{accordion.content}</div>
18
+ </div>
19
+ ))}
20
+ </div>
21
+ ),
22
+ }));
23
+
24
+ describe('ArchivedVersionListing', () => {
25
+ it('returns null when there are no archived versions', () => {
26
+ const content = {
27
+ archived_versions: [],
28
+ };
29
+
30
+ const { container } = render(
31
+ <MemoryRouter>
32
+ <ArchivedVersionListing content={content} />
33
+ </MemoryRouter>,
34
+ );
35
+
36
+ expect(container.innerHTML).toBe('');
37
+ });
38
+
39
+ it('returns null when archived_versions is missing', () => {
40
+ const content = {};
41
+
42
+ const { container } = render(
43
+ <MemoryRouter>
44
+ <ArchivedVersionListing content={content} />
45
+ </MemoryRouter>,
46
+ );
47
+
48
+ expect(container.innerHTML).toBe('');
49
+ });
50
+
51
+ it('renders archived version links', () => {
52
+ const content = {
53
+ archived_versions: [
54
+ {
55
+ '@id': 'http://localhost:3000/indicator-v1',
56
+ title: 'Indicator v1',
57
+ },
58
+ {
59
+ '@id': 'http://localhost:3000/indicator-v2',
60
+ title: 'Indicator v2',
61
+ },
62
+ ],
63
+ };
64
+
65
+ render(
66
+ <MemoryRouter>
67
+ <ArchivedVersionListing content={content} />
68
+ </MemoryRouter>,
69
+ );
70
+
71
+ expect(
72
+ screen.getByText('Previous versions of this indicator'),
73
+ ).toBeInTheDocument();
74
+
75
+ expect(screen.getByText('Indicator v1')).toHaveAttribute(
76
+ 'href',
77
+ '/indicator-v1',
78
+ );
79
+
80
+ expect(screen.getByText('Indicator v2')).toHaveAttribute(
81
+ 'href',
82
+ '/indicator-v2',
83
+ );
84
+ });
85
+
86
+ it('uses the URL as link text when title is missing', () => {
87
+ const content = {
88
+ archived_versions: [
89
+ {
90
+ '@id': 'http://localhost:3000/indicator-v1',
91
+ },
92
+ ],
93
+ };
94
+
95
+ render(
96
+ <MemoryRouter>
97
+ <ArchivedVersionListing content={content} />
98
+ </MemoryRouter>,
99
+ );
100
+
101
+ expect(screen.getByText('/indicator-v1')).toHaveAttribute(
102
+ 'href',
103
+ '/indicator-v1',
104
+ );
105
+ });
106
+ });
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { Message } from 'semantic-ui-react';
4
+ import { FormattedMessage } from 'react-intl';
5
+ import { flattenToAppURL } from '@plone/volto/helpers';
6
+
7
+ function ArchivedVersionNotice({ content }) {
8
+ if (content?.review_state !== 'archived') return null;
9
+
10
+ const archivedVersions = content?.archived_versions || [];
11
+ const latestVersion = archivedVersions.find(
12
+ (item) => item.review_state === 'published',
13
+ );
14
+
15
+ if (!latestVersion) return null;
16
+
17
+ const latestUrl = flattenToAppURL(latestVersion['@id']);
18
+
19
+ return (
20
+ <Message info className="archived-version-notice">
21
+ <Message.Content>
22
+ <FormattedMessage
23
+ id="You are viewing an archived version."
24
+ defaultMessage="You are viewing an archived version."
25
+ />{' '}
26
+ <Link to={latestUrl}>
27
+ <FormattedMessage
28
+ id="View latest version"
29
+ defaultMessage="View latest version"
30
+ />
31
+ </Link>
32
+ </Message.Content>
33
+ </Message>
34
+ );
35
+ }
36
+
37
+ export default ArchivedVersionNotice;
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import configureStore from 'redux-mock-store';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+ import { Provider } from 'react-intl-redux';
6
+ import { render } from '@testing-library/react';
7
+ import ArchivedVersionNotice from './ArchivedVersionNotice';
8
+
9
+ const mockStore = configureStore();
10
+
11
+ const store = mockStore({
12
+ userSession: { token: '1234' },
13
+ intl: {
14
+ locale: 'en',
15
+ messages: {},
16
+ },
17
+ });
18
+
19
+ describe('ArchivedVersionNotice', () => {
20
+ it('returns null when content is not archived', () => {
21
+ const content = {
22
+ '@type': 'Document',
23
+ review_state: 'published',
24
+ archived_versions: [],
25
+ };
26
+ const { container } = render(
27
+ <Provider store={store}>
28
+ <MemoryRouter>
29
+ <ArchivedVersionNotice content={content} />
30
+ </MemoryRouter>
31
+ </Provider>,
32
+ );
33
+ expect(container.innerHTML).toBe('');
34
+ });
35
+
36
+ it('returns null when archived but no published related item', () => {
37
+ const content = {
38
+ '@type': 'Document',
39
+ review_state: 'archived',
40
+ archived_versions: [
41
+ {
42
+ '@id': 'http://localhost:3000/another-archived',
43
+ review_state: 'archived',
44
+ },
45
+ ],
46
+ };
47
+
48
+ const { container } = render(
49
+ <Provider store={store}>
50
+ <MemoryRouter>
51
+ <ArchivedVersionNotice content={content} />
52
+ </MemoryRouter>
53
+ </Provider>,
54
+ );
55
+
56
+ expect(container.innerHTML).toBe('');
57
+ });
58
+
59
+ it('renders a notice with a link to the published version', () => {
60
+ const content = {
61
+ '@id': 'http://localhost:3000/archived-indicator',
62
+ '@type': 'Indicator',
63
+ review_state: 'archived',
64
+ title: 'My Indicator (Archived)',
65
+ archived_versions: [
66
+ {
67
+ '@id': 'http://localhost:3000/my-indicator',
68
+ review_state: 'published',
69
+ title: 'My Indicator',
70
+ },
71
+ ],
72
+ };
73
+
74
+ const { container } = render(
75
+ <Provider store={store}>
76
+ <MemoryRouter>
77
+ <ArchivedVersionNotice content={content} />
78
+ </MemoryRouter>
79
+ </Provider>,
80
+ );
81
+
82
+ expect(
83
+ container.querySelector('.archived-version-notice'),
84
+ ).toBeInTheDocument();
85
+ expect(container.querySelector('a')).toHaveAttribute(
86
+ 'href',
87
+ '/my-indicator',
88
+ );
89
+ });
90
+ });
@@ -12,7 +12,11 @@ import {
12
12
  flourishDataprotection,
13
13
  getDataSrcFromEmbedCode,
14
14
  } from '@eeacms/volto-cca-policy/helpers/flourishUtils';
15
- import { VIDEO, CONTENT_TYPE_LABELS } from '@eeacms/volto-cca-policy/constants';
15
+ import {
16
+ VIDEO,
17
+ INDICATOR,
18
+ CONTENT_TYPE_LABELS,
19
+ } from '@eeacms/volto-cca-policy/constants';
16
20
  import {
17
21
  HTMLField,
18
22
  ReferenceInfo,
@@ -24,6 +28,11 @@ import {
24
28
  ExternalLink,
25
29
  BannerTitle,
26
30
  } from '@eeacms/volto-cca-policy/helpers';
31
+ import {
32
+ ArchivedVersionNotice,
33
+ ArchivedVersionListing,
34
+ // VersionsGroup, // commented out - relatedItems already shows versions
35
+ } from '@eeacms/volto-cca-policy/components';
27
36
 
28
37
  const SHARE_EEA = ['https://cmshare.eea.eu', 'shareit.eea.europa.eu'];
29
38
 
@@ -133,6 +142,7 @@ const BottomInfo = (props) => {
133
142
 
134
143
  <ContentRelatedItems {...props} />
135
144
  <PublishedModifiedInfo {...props} />
145
+ {/* <VersionsGroup {...props} /> */}
136
146
  <ShareInfoButton {...props} />
137
147
  </>
138
148
  );
@@ -176,6 +186,7 @@ const DatabaseItemView = (props) => {
176
186
  />
177
187
 
178
188
  <Container>
189
+ {type === INDICATOR && <ArchivedVersionNotice content={content} />}
179
190
  <PortalMessage content={content} />
180
191
  <Grid columns="12">
181
192
  <Grid.Row>
@@ -287,6 +298,8 @@ const DatabaseItemView = (props) => {
287
298
  </>
288
299
  )}
289
300
  </Grid>
301
+
302
+ {type === INDICATOR && <ArchivedVersionListing content={content} />}
290
303
  </Container>
291
304
  </div>
292
305
  );
@@ -0,0 +1,101 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { flattenToAppURL } from '@plone/volto/helpers';
5
+
6
+ function VersionsGroup({ content }) {
7
+ const relatedItems = content?.relatedItems || [];
8
+ const currentId = content?.['@id'];
9
+ const currentState = content?.review_state;
10
+
11
+ // Find the latest (published) version
12
+ const latestFromRelated = relatedItems.find(
13
+ (item) => item.review_state === 'published',
14
+ );
15
+
16
+ // The latest version is either the current object (if published)
17
+ // or the published item found in relatedItems
18
+ const latestVersion =
19
+ currentState === 'published'
20
+ ? { '@id': currentId, title: content?.title, created: content?.created }
21
+ : latestFromRelated;
22
+
23
+ // Collect archived versions:
24
+ // - If current is published: archived copies are in relatedItems
25
+ // - If current is archived: include current object + any archived copies in relatedItems
26
+ let archivedVersions = relatedItems.filter(
27
+ (item) => item.review_state === 'archived',
28
+ );
29
+
30
+ if (currentState === 'archived') {
31
+ // Ensure the current archived copy is included in the list
32
+ const currentInList = archivedVersions.some(
33
+ (item) => item['@id'] === currentId,
34
+ );
35
+ if (!currentInList) {
36
+ archivedVersions = [
37
+ ...archivedVersions,
38
+ {
39
+ '@id': currentId,
40
+ title: content?.title,
41
+ created: content?.created,
42
+ },
43
+ ];
44
+ }
45
+ }
46
+
47
+ // Sort by creation date ascending
48
+ archivedVersions.sort((a, b) => new Date(a.created) - new Date(b.created));
49
+
50
+ // Build the full version list: latest first, then archived copies
51
+ const versions = [];
52
+ if (latestVersion) {
53
+ versions.push({ ...latestVersion, isLatest: true });
54
+ }
55
+ versions.push(...archivedVersions.map((v) => ({ ...v, isLatest: false })));
56
+
57
+ if (versions.length <= 1) return null;
58
+
59
+ return (
60
+ <div className="versions-group">
61
+ <h5>
62
+ <FormattedMessage id="Versions" defaultMessage="Versions" />
63
+ </h5>
64
+ <ul>
65
+ {versions.map((version) => {
66
+ const url = flattenToAppURL(version['@id']);
67
+ const isCurrent = version['@id'] === currentId;
68
+ return (
69
+ <li key={version['@id']}>
70
+ {isCurrent ? (
71
+ <strong>
72
+ {version.title}
73
+ {version.isLatest && (
74
+ <span>
75
+ {' '}
76
+ (
77
+ <FormattedMessage id="latest" defaultMessage="latest" />)
78
+ </span>
79
+ )}
80
+ </strong>
81
+ ) : (
82
+ <Link to={url}>
83
+ {version.title}
84
+ {version.isLatest && (
85
+ <span>
86
+ {' '}
87
+ (
88
+ <FormattedMessage id="latest" defaultMessage="latest" />)
89
+ </span>
90
+ )}
91
+ </Link>
92
+ )}
93
+ </li>
94
+ );
95
+ })}
96
+ </ul>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ export default VersionsGroup;
@@ -0,0 +1,130 @@
1
+ import React from 'react';
2
+ import { MemoryRouter } from 'react-router-dom';
3
+ import configureStore from 'redux-mock-store';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+ import { Provider } from 'react-intl-redux';
6
+ import { render } from '@testing-library/react';
7
+ import VersionsGroup from './VersionsGroup';
8
+
9
+ const mockStore = configureStore();
10
+
11
+ const store = mockStore({
12
+ userSession: { token: '1234' },
13
+ intl: {
14
+ locale: 'en',
15
+ messages: {},
16
+ },
17
+ });
18
+
19
+ describe('VersionsGroup', () => {
20
+ it('returns null when there is only one version', () => {
21
+ const content = {
22
+ '@id': 'http://localhost:3000/my-indicator',
23
+ '@type': 'Indicator',
24
+ review_state: 'published',
25
+ title: 'My Indicator',
26
+ created: '2024-01-01T00:00:00Z',
27
+ relatedItems: [],
28
+ };
29
+ const { container } = render(
30
+ <Provider store={store}>
31
+ <MemoryRouter>
32
+ <VersionsGroup content={content} />
33
+ </MemoryRouter>
34
+ </Provider>,
35
+ );
36
+ expect(container.innerHTML).toBe('');
37
+ });
38
+
39
+ it('renders versions list with published as latest', () => {
40
+ const content = {
41
+ '@id': 'http://localhost:3000/my-indicator',
42
+ '@type': 'Indicator',
43
+ review_state: 'published',
44
+ title: 'My Indicator',
45
+ created: '2024-01-01T00:00:00Z',
46
+ relatedItems: [
47
+ {
48
+ '@id': 'http://localhost:3000/my-indicator-v2',
49
+ review_state: 'archived',
50
+ title: 'My Indicator (Archived)',
51
+ created: '2024-06-01T00:00:00Z',
52
+ },
53
+ ],
54
+ };
55
+ const { container } = render(
56
+ <Provider store={store}>
57
+ <MemoryRouter>
58
+ <VersionsGroup content={content} />
59
+ </MemoryRouter>
60
+ </Provider>,
61
+ );
62
+ expect(container.querySelector('.versions-group')).toBeInTheDocument();
63
+ expect(container.querySelectorAll('li')).toHaveLength(2);
64
+ expect(container.querySelector('strong')).toBeInTheDocument();
65
+ });
66
+
67
+ it('renders versions when viewing an archived copy', () => {
68
+ const content = {
69
+ '@id': 'http://localhost:3000/my-indicator-v2',
70
+ '@type': 'Indicator',
71
+ review_state: 'archived',
72
+ title: 'My Indicator (Archived)',
73
+ created: '2024-06-01T00:00:00Z',
74
+ relatedItems: [
75
+ {
76
+ '@id': 'http://localhost:3000/my-indicator',
77
+ review_state: 'published',
78
+ title: 'My Indicator',
79
+ created: '2024-01-01T00:00:00Z',
80
+ },
81
+ ],
82
+ };
83
+ const { container } = render(
84
+ <Provider store={store}>
85
+ <MemoryRouter>
86
+ <VersionsGroup content={content} />
87
+ </MemoryRouter>
88
+ </Provider>,
89
+ );
90
+ expect(container.querySelector('.versions-group')).toBeInTheDocument();
91
+ expect(container.querySelectorAll('li')).toHaveLength(2);
92
+ const links = container.querySelectorAll('li a');
93
+ expect(links.length).toBeGreaterThanOrEqual(1);
94
+ });
95
+
96
+ it('sorts archived versions by creation date ascending', () => {
97
+ const content = {
98
+ '@id': 'http://localhost:3000/my-indicator',
99
+ '@type': 'Indicator',
100
+ review_state: 'published',
101
+ title: 'My Indicator',
102
+ created: '2024-01-01T00:00:00Z',
103
+ relatedItems: [
104
+ {
105
+ '@id': 'http://localhost:3000/my-indicator-v3',
106
+ review_state: 'archived',
107
+ title: 'My Indicator v3',
108
+ created: '2024-12-01T00:00:00Z',
109
+ },
110
+ {
111
+ '@id': 'http://localhost:3000/my-indicator-v2',
112
+ review_state: 'archived',
113
+ title: 'My Indicator v2',
114
+ created: '2024-06-01T00:00:00Z',
115
+ },
116
+ ],
117
+ };
118
+ const { container } = render(
119
+ <Provider store={store}>
120
+ <MemoryRouter>
121
+ <VersionsGroup content={content} />
122
+ </MemoryRouter>
123
+ </Provider>,
124
+ );
125
+ const items = container.querySelectorAll('li');
126
+ expect(items).toHaveLength(3);
127
+ expect(items[1].textContent).toContain('v2');
128
+ expect(items[2].textContent).toContain('v3');
129
+ });
130
+ });
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  TranslationDisclaimer,
10
10
  RedirectToLogin,
11
11
  MissionSignatoryProfileView,
12
+ CreateArchivedCopyButton,
12
13
  ImageWidget,
13
14
  } from '@eeacms/volto-cca-policy/components';
14
15
 
@@ -509,6 +510,10 @@ const applyConfig = (config) => {
509
510
  match: '',
510
511
  component: TranslationDisclaimer,
511
512
  },
513
+ {
514
+ match: '',
515
+ component: CreateArchivedCopyButton,
516
+ },
512
517
  {
513
518
  match: {
514
519
  path: /^.*\/add$/,