@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 +39 -0
- package/package.json +1 -1
- package/src/components/index.js +8 -0
- package/src/components/manage/CreateArchivedCopyButton/CreateArchivedCopyButton.jsx +153 -0
- package/src/components/manage/CreateArchivedCopyButton/CreateArchivedCopyButton.test.jsx +134 -0
- package/src/components/theme/Views/ArchivedVersionListing.jsx +40 -0
- package/src/components/theme/Views/ArchivedVersionListing.test.jsx +106 -0
- package/src/components/theme/Views/ArchivedVersionNotice.jsx +37 -0
- package/src/components/theme/Views/ArchivedVersionNotice.test.jsx +90 -0
- package/src/components/theme/Views/DatabaseItemView.jsx +14 -1
- package/src/components/theme/Views/VersionsGroup.jsx +101 -0
- package/src/components/theme/Views/VersionsGroup.test.jsx +130 -0
- package/src/index.js +5 -0
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
package/src/components/index.js
CHANGED
|
@@ -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 {
|
|
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$/,
|