@eeacms/volto-cca-policy 0.3.57 → 0.3.59
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 +10 -0
- package/package.json +1 -1
- package/src/customizations/volto/components/theme/App/App.jsx +0 -6
- package/src/customizations/volto/components/theme/View/README.md +1 -0
- package/src/customizations/volto/components/theme/View/View.jsx +299 -0
- package/src/customizations/volto/reducers/actions/README.md +7 -0
- package/src/customizations/volto/reducers/actions/actions.js +90 -0
- package/src/customizations/volto/reducers/actions/actions.test.js +280 -0
- package/src/customizations/volto/reducers/breadcrumbs/README.md +7 -0
- package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.js +98 -0
- package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.test.js +120 -0
- package/src/customizations/volto/reducers/navigation/navigation.js +3 -0
- package/src/index.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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.59](https://github.com/eea/volto-cca-policy/compare/0.3.58...0.3.59) - 3 July 2025
|
|
8
|
+
|
|
9
|
+
#### :hammer_and_wrench: Others
|
|
10
|
+
|
|
11
|
+
- Add customization for View.jsx [Tiberiu Ichim - [`8647300`](https://github.com/eea/volto-cca-policy/commit/864730053d2fe5dcf01d02085669bbab83203838)]
|
|
12
|
+
### [0.3.58](https://github.com/eea/volto-cca-policy/compare/0.3.57...0.3.58) - 3 July 2025
|
|
13
|
+
|
|
14
|
+
#### :hammer_and_wrench: Others
|
|
15
|
+
|
|
16
|
+
- Solve eslint [Tiberiu Ichim - [`29f656a`](https://github.com/eea/volto-cca-policy/commit/29f656a48416a04a2ebdf82091f25134b117cd7d)]
|
|
7
17
|
### [0.3.57](https://github.com/eea/volto-cca-policy/compare/0.3.56...0.3.57) - 27 June 2025
|
|
8
18
|
|
|
9
19
|
#### :house: Internal changes
|
package/package.json
CHANGED
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
getContent,
|
|
45
45
|
getNavigation,
|
|
46
46
|
getTypes,
|
|
47
|
-
getWorkflow,
|
|
48
47
|
} from '@plone/volto/actions';
|
|
49
48
|
|
|
50
49
|
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
@@ -316,11 +315,6 @@ export function connectAppComponent(AppComponent) {
|
|
|
316
315
|
}
|
|
317
316
|
},
|
|
318
317
|
},
|
|
319
|
-
{
|
|
320
|
-
key: 'workflow',
|
|
321
|
-
promise: ({ location, store: { dispatch } }) =>
|
|
322
|
-
__SERVER__ && dispatch(getWorkflow(getBaseUrl(location.pathname))),
|
|
323
|
-
},
|
|
324
318
|
]),
|
|
325
319
|
injectIntl,
|
|
326
320
|
connect((state, props) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Customized View.jsx to add the stripping of ++api++
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View container.
|
|
3
|
+
* @module components/theme/View/View
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { Component } from 'react';
|
|
7
|
+
import PropTypes from 'prop-types';
|
|
8
|
+
import { connect } from 'react-redux';
|
|
9
|
+
import { compose } from 'redux';
|
|
10
|
+
import { Redirect } from 'react-router-dom';
|
|
11
|
+
import { Portal } from 'react-portal';
|
|
12
|
+
import { injectIntl } from 'react-intl';
|
|
13
|
+
import qs from 'query-string';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ContentMetadataTags,
|
|
17
|
+
Comments,
|
|
18
|
+
Tags,
|
|
19
|
+
Toolbar,
|
|
20
|
+
} from '@plone/volto/components';
|
|
21
|
+
import { listActions, getContent } from '@plone/volto/actions';
|
|
22
|
+
import {
|
|
23
|
+
BodyClass,
|
|
24
|
+
getBaseUrl,
|
|
25
|
+
flattenToAppURL,
|
|
26
|
+
getLayoutFieldname,
|
|
27
|
+
hasApiExpander,
|
|
28
|
+
} from '@plone/volto/helpers';
|
|
29
|
+
|
|
30
|
+
import config from '@plone/volto/registry';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* View container class.
|
|
34
|
+
* @class View
|
|
35
|
+
* @extends Component
|
|
36
|
+
*/
|
|
37
|
+
class View extends Component {
|
|
38
|
+
/**
|
|
39
|
+
* Property types.
|
|
40
|
+
* @property {Object} propTypes Property types.
|
|
41
|
+
* @static
|
|
42
|
+
*/
|
|
43
|
+
static propTypes = {
|
|
44
|
+
actions: PropTypes.shape({
|
|
45
|
+
object: PropTypes.arrayOf(PropTypes.object),
|
|
46
|
+
object_buttons: PropTypes.arrayOf(PropTypes.object),
|
|
47
|
+
user: PropTypes.arrayOf(PropTypes.object),
|
|
48
|
+
}),
|
|
49
|
+
listActions: PropTypes.func.isRequired,
|
|
50
|
+
/**
|
|
51
|
+
* Action to get the content
|
|
52
|
+
*/
|
|
53
|
+
getContent: PropTypes.func.isRequired,
|
|
54
|
+
/**
|
|
55
|
+
* Pathname of the object
|
|
56
|
+
*/
|
|
57
|
+
pathname: PropTypes.string.isRequired,
|
|
58
|
+
location: PropTypes.shape({
|
|
59
|
+
search: PropTypes.string,
|
|
60
|
+
pathname: PropTypes.string,
|
|
61
|
+
}).isRequired,
|
|
62
|
+
/**
|
|
63
|
+
* Version id of the object
|
|
64
|
+
*/
|
|
65
|
+
versionId: PropTypes.string,
|
|
66
|
+
/**
|
|
67
|
+
* Content of the object
|
|
68
|
+
*/
|
|
69
|
+
content: PropTypes.shape({
|
|
70
|
+
/**
|
|
71
|
+
* Layout of the object
|
|
72
|
+
*/
|
|
73
|
+
layout: PropTypes.string,
|
|
74
|
+
/**
|
|
75
|
+
* Allow discussion of the object
|
|
76
|
+
*/
|
|
77
|
+
allow_discussion: PropTypes.bool,
|
|
78
|
+
/**
|
|
79
|
+
* Title of the object
|
|
80
|
+
*/
|
|
81
|
+
title: PropTypes.string,
|
|
82
|
+
/**
|
|
83
|
+
* Description of the object
|
|
84
|
+
*/
|
|
85
|
+
description: PropTypes.string,
|
|
86
|
+
/**
|
|
87
|
+
* Type of the object
|
|
88
|
+
*/
|
|
89
|
+
'@type': PropTypes.string,
|
|
90
|
+
/**
|
|
91
|
+
* Subjects of the object
|
|
92
|
+
*/
|
|
93
|
+
subjects: PropTypes.arrayOf(PropTypes.string),
|
|
94
|
+
is_folderish: PropTypes.bool,
|
|
95
|
+
}),
|
|
96
|
+
error: PropTypes.shape({
|
|
97
|
+
/**
|
|
98
|
+
* Error type
|
|
99
|
+
*/
|
|
100
|
+
status: PropTypes.number,
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default properties.
|
|
106
|
+
* @property {Object} defaultProps Default properties.
|
|
107
|
+
* @static
|
|
108
|
+
*/
|
|
109
|
+
static defaultProps = {
|
|
110
|
+
actions: null,
|
|
111
|
+
content: null,
|
|
112
|
+
versionId: null,
|
|
113
|
+
error: null,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
state = {
|
|
117
|
+
hasObjectButtons: null,
|
|
118
|
+
isClient: false,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
componentDidMount() {
|
|
122
|
+
// Do not trigger the actions action if the expander is present
|
|
123
|
+
if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) {
|
|
124
|
+
this.props.listActions(getBaseUrl(this.props.pathname));
|
|
125
|
+
}
|
|
126
|
+
this.props.getContent(
|
|
127
|
+
getBaseUrl(this.props.pathname),
|
|
128
|
+
this.props.versionId,
|
|
129
|
+
);
|
|
130
|
+
this.setState({ isClient: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Component will receive props
|
|
135
|
+
* @method componentWillReceiveProps
|
|
136
|
+
* @param {Object} nextProps Next properties
|
|
137
|
+
* @returns {undefined}
|
|
138
|
+
*/
|
|
139
|
+
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
140
|
+
if (nextProps.pathname !== this.props.pathname) {
|
|
141
|
+
// Do not trigger the actions action if the expander is present
|
|
142
|
+
if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) {
|
|
143
|
+
this.props.listActions(getBaseUrl(nextProps.pathname));
|
|
144
|
+
}
|
|
145
|
+
this.props.getContent(
|
|
146
|
+
getBaseUrl(nextProps.pathname),
|
|
147
|
+
this.props.versionId,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (nextProps.actions.object_buttons) {
|
|
152
|
+
const objectButtons = nextProps.actions.object_buttons;
|
|
153
|
+
this.setState({
|
|
154
|
+
hasObjectButtons: !!objectButtons.length,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Default fallback view
|
|
161
|
+
* @method getViewDefault
|
|
162
|
+
* @returns {string} Markup for component.
|
|
163
|
+
*/
|
|
164
|
+
getViewDefault = () => config.views.defaultView;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get view by content type
|
|
168
|
+
* @method getViewByType
|
|
169
|
+
* @returns {string} Markup for component.
|
|
170
|
+
*/
|
|
171
|
+
getViewByType = () =>
|
|
172
|
+
config.views.contentTypesViews[this.props.content['@type']] || null;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get view by content layout property
|
|
176
|
+
* @method getViewByLayout
|
|
177
|
+
* @returns {string} Markup for component.
|
|
178
|
+
*/
|
|
179
|
+
getViewByLayout = () =>
|
|
180
|
+
config.views.layoutViews[
|
|
181
|
+
this.props.content[getLayoutFieldname(this.props.content)]
|
|
182
|
+
] || null;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Cleans the component displayName (specially for connected components)
|
|
186
|
+
* which have the Connect(componentDisplayName)
|
|
187
|
+
* @method cleanViewName
|
|
188
|
+
* @param {string} dirtyDisplayName The displayName
|
|
189
|
+
* @returns {string} Clean displayName (no Connect(...)).
|
|
190
|
+
*/
|
|
191
|
+
cleanViewName = (dirtyDisplayName) =>
|
|
192
|
+
dirtyDisplayName
|
|
193
|
+
.replace('Connect(', '')
|
|
194
|
+
.replace('injectIntl(', '')
|
|
195
|
+
.replace(')', '')
|
|
196
|
+
.replace('connect(', '')
|
|
197
|
+
.toLowerCase();
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render method.
|
|
201
|
+
* @method render
|
|
202
|
+
* @returns {string} Markup for the component.
|
|
203
|
+
*/
|
|
204
|
+
render() {
|
|
205
|
+
const { views } = config;
|
|
206
|
+
if (this.props.error && this.props.error.code === 301) {
|
|
207
|
+
const redirect = flattenToAppURL(this.props.error.url)
|
|
208
|
+
.split('?')[0]
|
|
209
|
+
.replaceAll('/++api++', '/');
|
|
210
|
+
return <Redirect to={`${redirect}${this.props.location.search}`} />;
|
|
211
|
+
} else if (this.props.error && !this.props.connectionRefused) {
|
|
212
|
+
let FoundView;
|
|
213
|
+
if (this.props.error.status === undefined) {
|
|
214
|
+
// For some reason, while development and if CORS is in place and the
|
|
215
|
+
// requested resource is 404, it returns undefined as status, then the
|
|
216
|
+
// next statement will fail
|
|
217
|
+
FoundView = views.errorViews.corsError;
|
|
218
|
+
} else {
|
|
219
|
+
FoundView = views.errorViews[this.props.error.status.toString()];
|
|
220
|
+
}
|
|
221
|
+
if (!FoundView) {
|
|
222
|
+
FoundView = views.errorViews['404']; // default to 404
|
|
223
|
+
}
|
|
224
|
+
return (
|
|
225
|
+
<div id="view">
|
|
226
|
+
<FoundView {...this.props} />
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (!this.props.content) {
|
|
231
|
+
return <span />;
|
|
232
|
+
}
|
|
233
|
+
const RenderedView =
|
|
234
|
+
this.getViewByLayout() || this.getViewByType() || this.getViewDefault();
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div id="view">
|
|
238
|
+
<ContentMetadataTags content={this.props.content} />
|
|
239
|
+
{/* Body class if displayName in component is set */}
|
|
240
|
+
<BodyClass
|
|
241
|
+
className={
|
|
242
|
+
RenderedView.displayName
|
|
243
|
+
? `view-${this.cleanViewName(RenderedView.displayName)}`
|
|
244
|
+
: null
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
<RenderedView
|
|
248
|
+
key={this.props.content['@id']}
|
|
249
|
+
content={this.props.content}
|
|
250
|
+
location={this.props.location}
|
|
251
|
+
token={this.props.token}
|
|
252
|
+
history={this.props.history}
|
|
253
|
+
/>
|
|
254
|
+
{config.settings.showTags &&
|
|
255
|
+
this.props.content.subjects &&
|
|
256
|
+
this.props.content.subjects.length > 0 && (
|
|
257
|
+
<Tags tags={this.props.content.subjects} />
|
|
258
|
+
)}
|
|
259
|
+
{/* Add opt-in social sharing if required, disabled by default */}
|
|
260
|
+
{/* In the future this might be parameterized from the app config */}
|
|
261
|
+
{/* <SocialSharing
|
|
262
|
+
url={typeof window === 'undefined' ? '' : window.location.href}
|
|
263
|
+
title={this.props.content.title}
|
|
264
|
+
description={this.props.content.description || ''}
|
|
265
|
+
/> */}
|
|
266
|
+
{this.props.content.allow_discussion && (
|
|
267
|
+
<Comments pathname={this.props.pathname} />
|
|
268
|
+
)}
|
|
269
|
+
{this.state.isClient && (
|
|
270
|
+
<Portal node={document.getElementById('toolbar')}>
|
|
271
|
+
<Toolbar pathname={this.props.pathname} inner={<span />} />
|
|
272
|
+
</Portal>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export default compose(
|
|
280
|
+
injectIntl,
|
|
281
|
+
connect(
|
|
282
|
+
(state, props) => ({
|
|
283
|
+
actions: state.actions.actions,
|
|
284
|
+
token: state.userSession.token,
|
|
285
|
+
content: state.content.data,
|
|
286
|
+
error: state.content.get.error,
|
|
287
|
+
apiError: state.apierror.error,
|
|
288
|
+
connectionRefused: state.apierror.connectionRefused,
|
|
289
|
+
pathname: props.location.pathname,
|
|
290
|
+
versionId:
|
|
291
|
+
qs.parse(props.location.search) &&
|
|
292
|
+
qs.parse(props.location.search).version,
|
|
293
|
+
}),
|
|
294
|
+
{
|
|
295
|
+
listActions,
|
|
296
|
+
getContent,
|
|
297
|
+
},
|
|
298
|
+
),
|
|
299
|
+
)(View);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actions reducer.
|
|
3
|
+
* @module reducers/actions/actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GET_CONTENT, LIST_ACTIONS } from '@plone/volto/constants/ActionTypes';
|
|
7
|
+
import {
|
|
8
|
+
flattenToAppURL,
|
|
9
|
+
getBaseUrl,
|
|
10
|
+
hasApiExpander,
|
|
11
|
+
} from '@plone/volto/helpers';
|
|
12
|
+
|
|
13
|
+
const initialState = {
|
|
14
|
+
error: null,
|
|
15
|
+
actions: {
|
|
16
|
+
object: [],
|
|
17
|
+
object_buttons: [],
|
|
18
|
+
site_actions: [],
|
|
19
|
+
user: [],
|
|
20
|
+
document_actions: [],
|
|
21
|
+
portal_tabs: [],
|
|
22
|
+
},
|
|
23
|
+
loaded: false,
|
|
24
|
+
loading: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Actions reducer.
|
|
29
|
+
* @function actions
|
|
30
|
+
* @param {Object} state Current state.
|
|
31
|
+
* @param {Object} action Action to be handled.
|
|
32
|
+
* @returns {Object} New state.
|
|
33
|
+
*/
|
|
34
|
+
export default function actions(state = initialState, action = {}) {
|
|
35
|
+
let hasExpander;
|
|
36
|
+
switch (action.type) {
|
|
37
|
+
case `${LIST_ACTIONS}_PENDING`:
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
error: null,
|
|
41
|
+
loaded: false,
|
|
42
|
+
loading: true,
|
|
43
|
+
};
|
|
44
|
+
case `${GET_CONTENT}_SUCCESS`:
|
|
45
|
+
if (Object.keys(action.result).length === 0) {
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
hasExpander = hasApiExpander(
|
|
49
|
+
'actions',
|
|
50
|
+
getBaseUrl(flattenToAppURL(action.result['@id'])),
|
|
51
|
+
);
|
|
52
|
+
if (hasExpander && !action.subrequest) {
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
error: null,
|
|
56
|
+
actions: action.result['@components']?.actions,
|
|
57
|
+
loaded: true,
|
|
58
|
+
loading: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
case `${LIST_ACTIONS}_SUCCESS`:
|
|
63
|
+
// Even if the expander is set or not, if the LIST_ACTIONS is
|
|
64
|
+
// called, we want it to store the data if the actions data is
|
|
65
|
+
// not set in the expander data (['@components']) but in the "normal"
|
|
66
|
+
// action result (we look for the object property returned by the endpoint)
|
|
67
|
+
// Unfortunately, this endpoint returns all the actions in a plain object
|
|
68
|
+
// with no structure :(
|
|
69
|
+
if (action.result.object) {
|
|
70
|
+
return {
|
|
71
|
+
...state,
|
|
72
|
+
error: null,
|
|
73
|
+
actions: action.result,
|
|
74
|
+
loaded: true,
|
|
75
|
+
loading: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return state;
|
|
79
|
+
case `${LIST_ACTIONS}_FAIL`:
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
error: action.error,
|
|
83
|
+
actions: {},
|
|
84
|
+
loaded: false,
|
|
85
|
+
loading: false,
|
|
86
|
+
};
|
|
87
|
+
default:
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import actions from './actions';
|
|
2
|
+
import { GET_CONTENT, LIST_ACTIONS } from '@plone/volto/constants/ActionTypes';
|
|
3
|
+
import config from '@plone/volto/registry';
|
|
4
|
+
|
|
5
|
+
describe('Actions reducer', () => {
|
|
6
|
+
it('should return the initial state', () => {
|
|
7
|
+
expect(actions()).toEqual({
|
|
8
|
+
error: null,
|
|
9
|
+
actions: {
|
|
10
|
+
object: [],
|
|
11
|
+
object_buttons: [],
|
|
12
|
+
site_actions: [],
|
|
13
|
+
user: [],
|
|
14
|
+
document_actions: [],
|
|
15
|
+
portal_tabs: [],
|
|
16
|
+
},
|
|
17
|
+
loaded: false,
|
|
18
|
+
loading: false,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle LIST_ACTIONS_PENDING', () => {
|
|
23
|
+
expect(
|
|
24
|
+
actions(undefined, {
|
|
25
|
+
type: `${LIST_ACTIONS}_PENDING`,
|
|
26
|
+
}),
|
|
27
|
+
).toEqual({
|
|
28
|
+
error: null,
|
|
29
|
+
actions: {
|
|
30
|
+
object: [],
|
|
31
|
+
object_buttons: [],
|
|
32
|
+
site_actions: [],
|
|
33
|
+
user: [],
|
|
34
|
+
document_actions: [],
|
|
35
|
+
portal_tabs: [],
|
|
36
|
+
},
|
|
37
|
+
loaded: false,
|
|
38
|
+
loading: true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle LIST_ACTIONS_SUCCESS', () => {
|
|
43
|
+
expect(
|
|
44
|
+
actions(undefined, {
|
|
45
|
+
type: `${LIST_ACTIONS}_SUCCESS`,
|
|
46
|
+
result: {
|
|
47
|
+
object: [],
|
|
48
|
+
object_buttons: [],
|
|
49
|
+
site_actions: [],
|
|
50
|
+
user: [
|
|
51
|
+
{
|
|
52
|
+
icon: '',
|
|
53
|
+
id: 'preferences',
|
|
54
|
+
title: 'Preferences',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
icon: '',
|
|
58
|
+
id: 'dashboard',
|
|
59
|
+
title: 'Dashboard',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
icon: '',
|
|
63
|
+
id: 'plone_setup',
|
|
64
|
+
title: 'Site Setup',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
icon: '',
|
|
68
|
+
id: 'logout',
|
|
69
|
+
title: 'Log out',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
document_actions: [],
|
|
73
|
+
portal_tabs: [],
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
).toEqual({
|
|
77
|
+
error: null,
|
|
78
|
+
actions: {
|
|
79
|
+
object: [],
|
|
80
|
+
object_buttons: [],
|
|
81
|
+
site_actions: [],
|
|
82
|
+
user: [
|
|
83
|
+
{
|
|
84
|
+
icon: '',
|
|
85
|
+
id: 'preferences',
|
|
86
|
+
title: 'Preferences',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
icon: '',
|
|
90
|
+
id: 'dashboard',
|
|
91
|
+
title: 'Dashboard',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
icon: '',
|
|
95
|
+
id: 'plone_setup',
|
|
96
|
+
title: 'Site Setup',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
icon: '',
|
|
100
|
+
id: 'logout',
|
|
101
|
+
title: 'Log out',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
document_actions: [],
|
|
105
|
+
portal_tabs: [],
|
|
106
|
+
},
|
|
107
|
+
loaded: true,
|
|
108
|
+
loading: false,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle LIST_ACTIONS_FAIL', () => {
|
|
113
|
+
expect(
|
|
114
|
+
actions(undefined, {
|
|
115
|
+
type: `${LIST_ACTIONS}_FAIL`,
|
|
116
|
+
error: 'failed',
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
error: 'failed',
|
|
120
|
+
actions: {},
|
|
121
|
+
loaded: false,
|
|
122
|
+
loading: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Actions reducer - (ACTIONS)GET_CONTENT', () => {
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
config.settings.apiExpanders = [
|
|
130
|
+
{
|
|
131
|
+
match: '',
|
|
132
|
+
GET_CONTENT: ['actions'],
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle (ACTIONS)GET_CONTENT', () => {
|
|
138
|
+
expect(
|
|
139
|
+
actions(undefined, {
|
|
140
|
+
type: `${GET_CONTENT}_SUCCESS`,
|
|
141
|
+
result: {
|
|
142
|
+
'@components': {
|
|
143
|
+
actions: {
|
|
144
|
+
object: [],
|
|
145
|
+
object_buttons: [],
|
|
146
|
+
site_actions: [],
|
|
147
|
+
user: [
|
|
148
|
+
{
|
|
149
|
+
icon: '',
|
|
150
|
+
id: 'preferences',
|
|
151
|
+
title: 'Preferences',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
icon: '',
|
|
155
|
+
id: 'dashboard',
|
|
156
|
+
title: 'Dashboard',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
icon: '',
|
|
160
|
+
id: 'plone_setup',
|
|
161
|
+
title: 'Site Setup',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
icon: '',
|
|
165
|
+
id: 'logout',
|
|
166
|
+
title: 'Log out',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
document_actions: [],
|
|
170
|
+
portal_tabs: [],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
).toEqual({
|
|
176
|
+
error: null,
|
|
177
|
+
actions: {
|
|
178
|
+
object: [],
|
|
179
|
+
object_buttons: [],
|
|
180
|
+
site_actions: [],
|
|
181
|
+
user: [
|
|
182
|
+
{
|
|
183
|
+
icon: '',
|
|
184
|
+
id: 'preferences',
|
|
185
|
+
title: 'Preferences',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
icon: '',
|
|
189
|
+
id: 'dashboard',
|
|
190
|
+
title: 'Dashboard',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
icon: '',
|
|
194
|
+
id: 'plone_setup',
|
|
195
|
+
title: 'Site Setup',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
icon: '',
|
|
199
|
+
id: 'logout',
|
|
200
|
+
title: 'Log out',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
document_actions: [],
|
|
204
|
+
portal_tabs: [],
|
|
205
|
+
},
|
|
206
|
+
loaded: true,
|
|
207
|
+
loading: false,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should handle (ACTIONS)LIST_ACTIONS (standalone with apiExpander enabled)', () => {
|
|
212
|
+
expect(
|
|
213
|
+
actions(undefined, {
|
|
214
|
+
type: `${LIST_ACTIONS}_SUCCESS`,
|
|
215
|
+
result: {
|
|
216
|
+
object: [],
|
|
217
|
+
object_buttons: [],
|
|
218
|
+
site_actions: [],
|
|
219
|
+
user: [
|
|
220
|
+
{
|
|
221
|
+
icon: '',
|
|
222
|
+
id: 'preferences',
|
|
223
|
+
title: 'Preferences',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
icon: '',
|
|
227
|
+
id: 'dashboard',
|
|
228
|
+
title: 'Dashboard',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
icon: '',
|
|
232
|
+
id: 'plone_setup',
|
|
233
|
+
title: 'Site Setup',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
icon: '',
|
|
237
|
+
id: 'logout',
|
|
238
|
+
title: 'Log out',
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
document_actions: [],
|
|
242
|
+
portal_tabs: [],
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
).toEqual({
|
|
246
|
+
error: null,
|
|
247
|
+
actions: {
|
|
248
|
+
object: [],
|
|
249
|
+
object_buttons: [],
|
|
250
|
+
site_actions: [],
|
|
251
|
+
user: [
|
|
252
|
+
{
|
|
253
|
+
icon: '',
|
|
254
|
+
id: 'preferences',
|
|
255
|
+
title: 'Preferences',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
icon: '',
|
|
259
|
+
id: 'dashboard',
|
|
260
|
+
title: 'Dashboard',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
icon: '',
|
|
264
|
+
id: 'plone_setup',
|
|
265
|
+
title: 'Site Setup',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
icon: '',
|
|
269
|
+
id: 'logout',
|
|
270
|
+
title: 'Log out',
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
document_actions: [],
|
|
274
|
+
portal_tabs: [],
|
|
275
|
+
},
|
|
276
|
+
loaded: true,
|
|
277
|
+
loading: false,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumbs reducer.
|
|
3
|
+
* @module reducers/breadcrumbs/breadcrumbs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { map } from 'lodash';
|
|
7
|
+
import {
|
|
8
|
+
flattenToAppURL,
|
|
9
|
+
getBaseUrl,
|
|
10
|
+
hasApiExpander,
|
|
11
|
+
} from '@plone/volto/helpers';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
GET_BREADCRUMBS,
|
|
15
|
+
GET_CONTENT,
|
|
16
|
+
} from '@plone/volto/constants/ActionTypes';
|
|
17
|
+
|
|
18
|
+
const initialState = {
|
|
19
|
+
error: null,
|
|
20
|
+
items: [],
|
|
21
|
+
root: null,
|
|
22
|
+
loaded: false,
|
|
23
|
+
loading: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Breadcrumbs reducer.
|
|
28
|
+
* @function breadcrumbs
|
|
29
|
+
* @param {Object} state Current state.
|
|
30
|
+
* @param {Object} action Action to be handled.
|
|
31
|
+
* @returns {Object} New state.
|
|
32
|
+
*/
|
|
33
|
+
export default function breadcrumbs(state = initialState, action = {}) {
|
|
34
|
+
let hasExpander;
|
|
35
|
+
switch (action.type) {
|
|
36
|
+
case `${GET_BREADCRUMBS}_PENDING`:
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
error: null,
|
|
40
|
+
loaded: false,
|
|
41
|
+
loading: true,
|
|
42
|
+
};
|
|
43
|
+
case `${GET_CONTENT}_SUCCESS`:
|
|
44
|
+
if (Object.keys(action.result).length === 0) {
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
hasExpander = hasApiExpander(
|
|
48
|
+
'breadcrumbs',
|
|
49
|
+
getBaseUrl(flattenToAppURL(action.result['@id'])),
|
|
50
|
+
);
|
|
51
|
+
if (hasExpander && !action.subrequest) {
|
|
52
|
+
return {
|
|
53
|
+
...state,
|
|
54
|
+
error: null,
|
|
55
|
+
items: map(
|
|
56
|
+
action.result['@components'].breadcrumbs.items,
|
|
57
|
+
(item) => ({
|
|
58
|
+
title: item.title,
|
|
59
|
+
url: flattenToAppURL(item['@id']),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
root: flattenToAppURL(action.result['@components'].breadcrumbs.root),
|
|
63
|
+
loaded: true,
|
|
64
|
+
loading: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return state;
|
|
68
|
+
case `${GET_BREADCRUMBS}_SUCCESS`:
|
|
69
|
+
hasExpander = hasApiExpander(
|
|
70
|
+
'breadcrumbs',
|
|
71
|
+
getBaseUrl(flattenToAppURL(action.result['@id'])),
|
|
72
|
+
);
|
|
73
|
+
if (!hasExpander) {
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
error: null,
|
|
77
|
+
items: map(action.result.items, (item) => ({
|
|
78
|
+
title: item.title,
|
|
79
|
+
url: flattenToAppURL(item['@id']),
|
|
80
|
+
})),
|
|
81
|
+
root: flattenToAppURL(action.result.root),
|
|
82
|
+
loaded: true,
|
|
83
|
+
loading: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return state;
|
|
87
|
+
case `${GET_BREADCRUMBS}_FAIL`:
|
|
88
|
+
return {
|
|
89
|
+
...state,
|
|
90
|
+
error: action.error,
|
|
91
|
+
items: [],
|
|
92
|
+
loaded: false,
|
|
93
|
+
loading: false,
|
|
94
|
+
};
|
|
95
|
+
default:
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import breadcrumbs from './breadcrumbs';
|
|
3
|
+
import {
|
|
4
|
+
GET_BREADCRUMBS,
|
|
5
|
+
GET_CONTENT,
|
|
6
|
+
} from '@plone/volto/constants/ActionTypes';
|
|
7
|
+
|
|
8
|
+
const { settings } = config;
|
|
9
|
+
|
|
10
|
+
describe('Breadcrumbs reducer', () => {
|
|
11
|
+
it('should return the initial state', () => {
|
|
12
|
+
expect(breadcrumbs()).toEqual({
|
|
13
|
+
error: null,
|
|
14
|
+
items: [],
|
|
15
|
+
root: null,
|
|
16
|
+
loaded: false,
|
|
17
|
+
loading: false,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should handle GET_BREADCRUMBS_PENDING', () => {
|
|
22
|
+
expect(
|
|
23
|
+
breadcrumbs(undefined, {
|
|
24
|
+
type: `${GET_BREADCRUMBS}_PENDING`,
|
|
25
|
+
}),
|
|
26
|
+
).toEqual({
|
|
27
|
+
error: null,
|
|
28
|
+
items: [],
|
|
29
|
+
root: null,
|
|
30
|
+
loaded: false,
|
|
31
|
+
loading: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle GET_BREADCRUMBS_SUCCESS', () => {
|
|
36
|
+
expect(
|
|
37
|
+
breadcrumbs(undefined, {
|
|
38
|
+
type: `${GET_BREADCRUMBS}_SUCCESS`,
|
|
39
|
+
result: {
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
title: 'Welcome to Plone!',
|
|
43
|
+
'@id': `${settings.apiPath}/front-page`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
root: settings.apiPath,
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
).toEqual({
|
|
50
|
+
error: null,
|
|
51
|
+
items: [
|
|
52
|
+
{
|
|
53
|
+
title: 'Welcome to Plone!',
|
|
54
|
+
url: '/front-page',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
root: '',
|
|
58
|
+
loaded: true,
|
|
59
|
+
loading: false,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle GET_BREADCRUMBS_FAIL', () => {
|
|
64
|
+
expect(
|
|
65
|
+
breadcrumbs(undefined, {
|
|
66
|
+
type: `${GET_BREADCRUMBS}_FAIL`,
|
|
67
|
+
error: 'failed',
|
|
68
|
+
}),
|
|
69
|
+
).toEqual({
|
|
70
|
+
error: 'failed',
|
|
71
|
+
items: [],
|
|
72
|
+
root: null,
|
|
73
|
+
loaded: false,
|
|
74
|
+
loading: false,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Breadcrumbs reducer - (BREADCRUMBS)GET_CONTENT', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
config.settings.apiExpanders = [
|
|
82
|
+
{
|
|
83
|
+
match: '',
|
|
84
|
+
GET_CONTENT: ['breadcrumbs'],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle (BREADCRUMBS)GET_CONTENT_SUCCESS', () => {
|
|
90
|
+
expect(
|
|
91
|
+
breadcrumbs(undefined, {
|
|
92
|
+
type: `${GET_CONTENT}_SUCCESS`,
|
|
93
|
+
result: {
|
|
94
|
+
'@components': {
|
|
95
|
+
breadcrumbs: {
|
|
96
|
+
items: [
|
|
97
|
+
{
|
|
98
|
+
title: 'Welcome to Plone!',
|
|
99
|
+
'@id': `${settings.apiPath}/front-page`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
root: settings.apiPath,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
).toEqual({
|
|
108
|
+
error: null,
|
|
109
|
+
items: [
|
|
110
|
+
{
|
|
111
|
+
title: 'Welcome to Plone!',
|
|
112
|
+
url: '/front-page',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
root: '',
|
|
116
|
+
loaded: true,
|
|
117
|
+
loading: false,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -57,6 +57,9 @@ export default function navigation(state = initialState, action = {}) {
|
|
|
57
57
|
loading: true,
|
|
58
58
|
};
|
|
59
59
|
case `${GET_CONTENT}_SUCCESS`:
|
|
60
|
+
if (Object.keys(action.result).length === 0) {
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
60
63
|
hasExpander = hasApiExpander(
|
|
61
64
|
'navigation',
|
|
62
65
|
getBaseUrl(flattenToAppURL(action.result['@id'])),
|
package/src/index.js
CHANGED
|
@@ -544,7 +544,7 @@ const applyConfig = (config) => {
|
|
|
544
544
|
},
|
|
545
545
|
{
|
|
546
546
|
match: '',
|
|
547
|
-
GET_CONTENT: ['navigation', 'breadcrumbs', 'actions'],
|
|
547
|
+
GET_CONTENT: ['navigation', 'breadcrumbs', 'actions', 'workflow'],
|
|
548
548
|
querystring: { 'expand.navigation.depth': '3' },
|
|
549
549
|
},
|
|
550
550
|
];
|