@eeacms/volto-eea-website-policy 1.14.0 → 2.0.0
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 +26 -0
- package/package.json +1 -1
- package/src/customizations/volto/components/manage/Display/Display.jsx +0 -306
- package/src/customizations/volto/components/manage/Display/Readme.md +0 -1
- package/src/customizations/volto/components/manage/Toolbar/More.jsx +0 -541
- package/src/customizations/volto/components/manage/Workflow/README.txt +0 -1
- package/src/customizations/volto/components/manage/Workflow/Workflow.jsx +0 -421
- package/src/customizations/volto/components/manage/Workflow/Workflow.test.jsx +0 -81
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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
|
+
### [2.0.0](https://github.com/eea/volto-eea-website-policy/compare/1.14.1-beta.1...2.0.0) - 22 April 2024
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat: Release 2.0.0 - Volto 17 support [alin - [`d6cf5c8`](https://github.com/eea/volto-eea-website-policy/commit/d6cf5c8f41950a94cd68e7a36a45b5be8cc6195b)]
|
|
12
|
+
- feat: Default view layout restriction to Manager - refs #268249 [ichim-david - [`1840c1b`](https://github.com/eea/volto-eea-website-policy/commit/1840c1bd9aae6ce18a23800ce618f8bbe47525ab)]
|
|
13
|
+
|
|
14
|
+
#### :nail_care: Enhancements
|
|
15
|
+
|
|
16
|
+
- refactor: Move customizations to volto-eea-website-theme [alin - [`9641437`](https://github.com/eea/volto-eea-website-policy/commit/9641437c7f10ba6c7ec1f896b180b49d2718a7d0)]
|
|
17
|
+
|
|
18
|
+
### [1.14.1-beta.1](https://github.com/eea/volto-eea-website-policy/compare/1.14.1-beta.0...1.14.1-beta.1) - 19 April 2024
|
|
19
|
+
|
|
20
|
+
#### :hammer_and_wrench: Others
|
|
21
|
+
|
|
22
|
+
- Release 1.14.1-beta.1 [nileshgulia1 - [`417ea33`](https://github.com/eea/volto-eea-website-policy/commit/417ea33818550b3f94a576e0a8c98dfd1b1b0d30)]
|
|
23
|
+
### [1.14.1-beta.0](https://github.com/eea/volto-eea-website-policy/compare/1.14.0...1.14.1-beta.0) - 19 April 2024
|
|
24
|
+
|
|
25
|
+
#### :house: Internal changes
|
|
26
|
+
|
|
27
|
+
- style: Automated code fix [eea-jenkins - [`6738335`](https://github.com/eea/volto-eea-website-policy/commit/6738335d49068446818199eb5eee352b3ff03f38)]
|
|
28
|
+
|
|
29
|
+
#### :hammer_and_wrench: Others
|
|
30
|
+
|
|
31
|
+
- ensure logout works [dobri1408 - [`bf0dca4`](https://github.com/eea/volto-eea-website-policy/commit/bf0dca4f615ef687da8309aef1bf894b3fea4938)]
|
|
32
|
+
- azure_login [dobri1408 - [`c6222f9`](https://github.com/eea/volto-eea-website-policy/commit/c6222f9a97cf7956020f715f76a012051332503b)]
|
|
7
33
|
### [1.14.0](https://github.com/eea/volto-eea-website-policy/compare/1.13.1...1.14.0) - 16 April 2024
|
|
8
34
|
|
|
9
35
|
#### :rocket: New Features
|
package/package.json
CHANGED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import React, { Component, Fragment } from 'react';
|
|
2
|
-
import PropTypes from 'prop-types';
|
|
3
|
-
import { connect } from 'react-redux';
|
|
4
|
-
import { compose } from 'redux';
|
|
5
|
-
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
6
|
-
|
|
7
|
-
import jwtDecode from 'jwt-decode';
|
|
8
|
-
import {
|
|
9
|
-
getSchema,
|
|
10
|
-
getUser,
|
|
11
|
-
updateContent,
|
|
12
|
-
getContent,
|
|
13
|
-
} from '@plone/volto/actions';
|
|
14
|
-
import { getLayoutFieldname } from '@plone/volto/helpers';
|
|
15
|
-
import { FormFieldWrapper, Icon } from '@plone/volto/components';
|
|
16
|
-
import { defineMessages, injectIntl } from 'react-intl';
|
|
17
|
-
import config from '@plone/volto/registry';
|
|
18
|
-
|
|
19
|
-
import downSVG from '@plone/volto/icons/down-key.svg';
|
|
20
|
-
import upSVG from '@plone/volto/icons/up-key.svg';
|
|
21
|
-
import checkSVG from '@plone/volto/icons/check.svg';
|
|
22
|
-
|
|
23
|
-
const messages = defineMessages({
|
|
24
|
-
Viewmode: {
|
|
25
|
-
id: 'Viewmode',
|
|
26
|
-
defaultMessage: 'View',
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const Option = injectLazyLibs('reactSelect')((props) => {
|
|
31
|
-
const { Option } = props.reactSelect.components;
|
|
32
|
-
return (
|
|
33
|
-
<Option {...props}>
|
|
34
|
-
<div>{props.label}</div>
|
|
35
|
-
{props.isFocused && !props.isSelected && (
|
|
36
|
-
<Icon name={checkSVG} size="18px" color="#b8c6c8" />
|
|
37
|
-
)}
|
|
38
|
-
{props.isSelected && <Icon name={checkSVG} size="18px" color="#007bc1" />}
|
|
39
|
-
</Option>
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const DropdownIndicator = injectLazyLibs('reactSelect')((props) => {
|
|
44
|
-
const { DropdownIndicator } = props.reactSelect.components;
|
|
45
|
-
return (
|
|
46
|
-
<DropdownIndicator {...props}>
|
|
47
|
-
{props.selectProps.menuIsOpen ? (
|
|
48
|
-
<Icon name={upSVG} size="24px" color="#007bc1" />
|
|
49
|
-
) : (
|
|
50
|
-
<Icon name={downSVG} size="24px" color="#007bc1" />
|
|
51
|
-
)}
|
|
52
|
-
</DropdownIndicator>
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const selectTheme = (theme) => ({
|
|
57
|
-
...theme,
|
|
58
|
-
borderRadius: 0,
|
|
59
|
-
colors: {
|
|
60
|
-
...theme.colors,
|
|
61
|
-
primary25: 'hotpink',
|
|
62
|
-
primary: '#b8c6c8',
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const customSelectStyles = {
|
|
67
|
-
control: (styles, state) => ({
|
|
68
|
-
...styles,
|
|
69
|
-
border: 'none',
|
|
70
|
-
borderBottom: '2px solid #b8c6c8',
|
|
71
|
-
boxShadow: 'none',
|
|
72
|
-
borderBottomStyle: state.menuIsOpen ? 'dotted' : 'solid',
|
|
73
|
-
}),
|
|
74
|
-
menu: (styles, state) => ({
|
|
75
|
-
...styles,
|
|
76
|
-
top: null,
|
|
77
|
-
marginTop: 0,
|
|
78
|
-
boxShadow: 'none',
|
|
79
|
-
borderBottom: '2px solid #b8c6c8',
|
|
80
|
-
}),
|
|
81
|
-
menuList: (styles, state) => ({
|
|
82
|
-
...styles,
|
|
83
|
-
maxHeight: '400px',
|
|
84
|
-
}),
|
|
85
|
-
indicatorSeparator: (styles) => ({
|
|
86
|
-
...styles,
|
|
87
|
-
width: null,
|
|
88
|
-
}),
|
|
89
|
-
valueContainer: (styles) => ({
|
|
90
|
-
...styles,
|
|
91
|
-
padding: 0,
|
|
92
|
-
}),
|
|
93
|
-
option: (styles, state) => ({
|
|
94
|
-
...styles,
|
|
95
|
-
backgroundColor: null,
|
|
96
|
-
minHeight: '50px',
|
|
97
|
-
display: 'flex',
|
|
98
|
-
justifyContent: 'space-between',
|
|
99
|
-
alignItems: 'center',
|
|
100
|
-
padding: '12px 12px',
|
|
101
|
-
color: state.isSelected
|
|
102
|
-
? '#007bc1'
|
|
103
|
-
: state.isFocused
|
|
104
|
-
? '#4a4a4a'
|
|
105
|
-
: 'inherit',
|
|
106
|
-
':active': {
|
|
107
|
-
backgroundColor: null,
|
|
108
|
-
},
|
|
109
|
-
span: {
|
|
110
|
-
flex: '0 0 auto',
|
|
111
|
-
},
|
|
112
|
-
svg: {
|
|
113
|
-
flex: '0 0 auto',
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Display container class.
|
|
120
|
-
* @class Display
|
|
121
|
-
* @extends Component
|
|
122
|
-
*/
|
|
123
|
-
class DisplaySelect extends Component {
|
|
124
|
-
/**
|
|
125
|
-
* Property types.
|
|
126
|
-
* @property {Object} propTypes Property types.
|
|
127
|
-
* @static
|
|
128
|
-
*/
|
|
129
|
-
static propTypes = {
|
|
130
|
-
getSchema: PropTypes.func.isRequired,
|
|
131
|
-
updateContent: PropTypes.func.isRequired,
|
|
132
|
-
getContent: PropTypes.func.isRequired,
|
|
133
|
-
loaded: PropTypes.bool.isRequired,
|
|
134
|
-
pathname: PropTypes.string.isRequired,
|
|
135
|
-
layouts: PropTypes.arrayOf(PropTypes.string),
|
|
136
|
-
layout: PropTypes.string,
|
|
137
|
-
type: PropTypes.string.isRequired,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Default properties
|
|
142
|
-
* @property {Object} defaultProps Default properties.
|
|
143
|
-
* @static
|
|
144
|
-
*/
|
|
145
|
-
static defaultProps = {
|
|
146
|
-
layouts: [],
|
|
147
|
-
layout: '',
|
|
148
|
-
rolesWhoCanChangeLayout: [],
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
state = {
|
|
152
|
-
hasMatchingRole: false,
|
|
153
|
-
selectedOption: {
|
|
154
|
-
value: this.props.layout,
|
|
155
|
-
label: config.views.layoutViewsNamesMapping?.[this.props.layout]
|
|
156
|
-
? this.props.intl.formatMessage({
|
|
157
|
-
id: config.views.layoutViewsNamesMapping?.[this.props.layout],
|
|
158
|
-
defaultMessage:
|
|
159
|
-
config.views.layoutViewsNamesMapping?.[this.props.layout],
|
|
160
|
-
})
|
|
161
|
-
: this.props.layout,
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
componentDidMount() {
|
|
166
|
-
this.props.getSchema(this.props.type);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
UNSAFE_componentWillMount() {
|
|
170
|
-
if (!this.props.rolesWhoCanChangeLayout.length) {
|
|
171
|
-
this.props.rolesWhoCanChangeLayout.push(
|
|
172
|
-
...(config?.settings?.eea?.rolesWhoCanChangeLayout || []),
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
if (!this.props.layouts.length) {
|
|
176
|
-
this.props.getSchema(this.props.type);
|
|
177
|
-
}
|
|
178
|
-
if (Object.keys(this.props.user).length === 0) {
|
|
179
|
-
this.props.getUser(this.props.userId);
|
|
180
|
-
} else {
|
|
181
|
-
const hasMatchingRole = this.props.user.roles.some((role) =>
|
|
182
|
-
this.props.rolesWhoCanChangeLayout.includes(role),
|
|
183
|
-
);
|
|
184
|
-
if (hasMatchingRole !== this.state.hasMatchingRole) {
|
|
185
|
-
this.setState({ hasMatchingRole });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Component will receive props
|
|
192
|
-
* @method componentWillReceiveProps
|
|
193
|
-
* @param {Object} nextProps Next properties
|
|
194
|
-
* @returns {undefined}
|
|
195
|
-
*/
|
|
196
|
-
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
197
|
-
if (nextProps.pathname !== this.props.pathname) {
|
|
198
|
-
this.props.getSchema(nextProps.type);
|
|
199
|
-
}
|
|
200
|
-
if (!this.props.loaded && nextProps.loaded) {
|
|
201
|
-
this.props.getContent(nextProps.pathname);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (Object.keys(nextProps.user).length !== 0) {
|
|
205
|
-
const hasMatchingRole = nextProps.user.roles.some((role) =>
|
|
206
|
-
this.props.rolesWhoCanChangeLayout.includes(role),
|
|
207
|
-
);
|
|
208
|
-
if (hasMatchingRole !== this.state.hasMatchingRole) {
|
|
209
|
-
this.setState({ hasMatchingRole });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* On set layout handler
|
|
216
|
-
* @method setLayout
|
|
217
|
-
* @param {Object} event Event object
|
|
218
|
-
* @returns {undefined}
|
|
219
|
-
*/
|
|
220
|
-
setLayout = (selectedOption) => {
|
|
221
|
-
this.props.updateContent(this.props.pathname, {
|
|
222
|
-
layout: selectedOption.value,
|
|
223
|
-
});
|
|
224
|
-
this.setState({ selectedOption });
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
selectValue = (option) => (
|
|
228
|
-
<Fragment>
|
|
229
|
-
<span className="Select-value-label">{option.label}</span>
|
|
230
|
-
</Fragment>
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
optionRenderer = (option) => (
|
|
234
|
-
<Fragment>
|
|
235
|
-
<span style={{ marginRight: 'auto' }}>{option.label}</span>
|
|
236
|
-
<Icon name={checkSVG} size="24px" />
|
|
237
|
-
</Fragment>
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
render() {
|
|
241
|
-
if (!this.state.hasMatchingRole) {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
const { selectedOption } = this.state;
|
|
245
|
-
const Select = this.props.reactSelect.default;
|
|
246
|
-
const layoutsNames = config.views.layoutViewsNamesMapping;
|
|
247
|
-
const layoutOptions = this.props.layouts
|
|
248
|
-
.filter(
|
|
249
|
-
(layout) =>
|
|
250
|
-
Object.keys(config.views.contentTypesViews).includes(layout) ||
|
|
251
|
-
Object.keys(config.views.layoutViews).includes(layout),
|
|
252
|
-
)
|
|
253
|
-
.map((item) => ({
|
|
254
|
-
value: item,
|
|
255
|
-
label:
|
|
256
|
-
this.props.intl.formatMessage({
|
|
257
|
-
id: layoutsNames[item],
|
|
258
|
-
defaultMessage: layoutsNames[item],
|
|
259
|
-
}) || item,
|
|
260
|
-
}));
|
|
261
|
-
|
|
262
|
-
return layoutOptions?.length > 1 ? (
|
|
263
|
-
<FormFieldWrapper
|
|
264
|
-
id="display-select"
|
|
265
|
-
title={this.props.intl.formatMessage(messages.Viewmode)}
|
|
266
|
-
{...this.props}
|
|
267
|
-
>
|
|
268
|
-
<Select
|
|
269
|
-
name="display-select"
|
|
270
|
-
className="react-select-container"
|
|
271
|
-
classNamePrefix="react-select"
|
|
272
|
-
options={layoutOptions}
|
|
273
|
-
styles={customSelectStyles}
|
|
274
|
-
theme={selectTheme}
|
|
275
|
-
components={{ DropdownIndicator, Option }}
|
|
276
|
-
onChange={this.setLayout}
|
|
277
|
-
defaultValue={selectedOption}
|
|
278
|
-
isSearchable={false}
|
|
279
|
-
/>
|
|
280
|
-
</FormFieldWrapper>
|
|
281
|
-
) : null;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export default compose(
|
|
286
|
-
injectIntl,
|
|
287
|
-
injectLazyLibs('reactSelect'),
|
|
288
|
-
connect(
|
|
289
|
-
(state) => ({
|
|
290
|
-
loaded: state.content.update.loaded,
|
|
291
|
-
layouts: state.schema.schema ? state.schema.schema.layouts : [],
|
|
292
|
-
layout: state.content.data
|
|
293
|
-
? state.content.data[getLayoutFieldname(state.content.data)]
|
|
294
|
-
: '',
|
|
295
|
-
layout_fieldname: state.content.data
|
|
296
|
-
? getLayoutFieldname(state.content.data)
|
|
297
|
-
: '',
|
|
298
|
-
type: state.content.data ? state.content.data['@type'] : '',
|
|
299
|
-
user: state.users.user,
|
|
300
|
-
userId: state.userSession.token
|
|
301
|
-
? jwtDecode(state.userSession.token).sub
|
|
302
|
-
: '',
|
|
303
|
-
}),
|
|
304
|
-
{ getSchema, getUser, updateContent, getContent },
|
|
305
|
-
),
|
|
306
|
-
)(DisplaySelect);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Added customization to condition the display of the layout options based on the user's roles.
|
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* More component.
|
|
3
|
-
* @module components/manage/Toolbar/More
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { Component } from 'react';
|
|
7
|
-
import { defineMessages, injectIntl } from 'react-intl';
|
|
8
|
-
import PropTypes from 'prop-types';
|
|
9
|
-
import { connect } from 'react-redux';
|
|
10
|
-
import { compose } from 'redux';
|
|
11
|
-
import { Link, withRouter } from 'react-router-dom';
|
|
12
|
-
import { find } from 'lodash';
|
|
13
|
-
import { toast } from 'react-toastify';
|
|
14
|
-
import { Toast } from '@plone/volto/components';
|
|
15
|
-
import { Pluggable, Plug } from '@plone/volto/components/manage/Pluggable';
|
|
16
|
-
import {
|
|
17
|
-
FormattedDate,
|
|
18
|
-
Icon,
|
|
19
|
-
Display,
|
|
20
|
-
Workflow,
|
|
21
|
-
} from '@plone/volto/components';
|
|
22
|
-
import {
|
|
23
|
-
applyWorkingCopy,
|
|
24
|
-
createWorkingCopy,
|
|
25
|
-
removeWorkingCopy,
|
|
26
|
-
} from '@plone/volto/actions';
|
|
27
|
-
import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
|
|
28
|
-
import config from '@plone/volto/registry';
|
|
29
|
-
|
|
30
|
-
import rightArrowSVG from '@plone/volto/icons/right-key.svg';
|
|
31
|
-
import userSVG from '@plone/volto/icons/user.svg';
|
|
32
|
-
import applySVG from '@plone/volto/icons/ready.svg';
|
|
33
|
-
import removeSVG from '@plone/volto/icons/circle-dismiss.svg';
|
|
34
|
-
|
|
35
|
-
const messages = defineMessages({
|
|
36
|
-
personalTools: {
|
|
37
|
-
id: 'Personal tools',
|
|
38
|
-
defaultMessage: 'Personal tools',
|
|
39
|
-
},
|
|
40
|
-
history: {
|
|
41
|
-
id: 'History',
|
|
42
|
-
defaultMessage: 'History',
|
|
43
|
-
},
|
|
44
|
-
sharing: {
|
|
45
|
-
id: 'Sharing',
|
|
46
|
-
defaultMessage: 'Sharing',
|
|
47
|
-
},
|
|
48
|
-
rules: {
|
|
49
|
-
id: 'Rules',
|
|
50
|
-
defaultMessage: 'Rules',
|
|
51
|
-
},
|
|
52
|
-
aliases: {
|
|
53
|
-
id: 'URL Management',
|
|
54
|
-
defaultMessage: 'URL Management',
|
|
55
|
-
},
|
|
56
|
-
ManageTranslations: {
|
|
57
|
-
id: 'Manage Translations',
|
|
58
|
-
defaultMessage: 'Manage Translations',
|
|
59
|
-
},
|
|
60
|
-
manageContent: {
|
|
61
|
-
id: 'Manage content…',
|
|
62
|
-
defaultMessage: 'Manage content…',
|
|
63
|
-
},
|
|
64
|
-
CreateWorkingCopy: {
|
|
65
|
-
id: 'Create working copy',
|
|
66
|
-
defaultMessage: 'Create working copy',
|
|
67
|
-
},
|
|
68
|
-
applyWorkingCopy: {
|
|
69
|
-
id: 'Apply working copy',
|
|
70
|
-
defaultMessage: 'Apply working copy',
|
|
71
|
-
},
|
|
72
|
-
removeWorkingCopy: {
|
|
73
|
-
id: 'Remove working copy',
|
|
74
|
-
defaultMessage: 'Remove working copy',
|
|
75
|
-
},
|
|
76
|
-
viewWorkingCopy: {
|
|
77
|
-
id: 'View working copy',
|
|
78
|
-
defaultMessage: 'View working copy',
|
|
79
|
-
},
|
|
80
|
-
workingAppliedTitle: {
|
|
81
|
-
id: 'Changes applied.',
|
|
82
|
-
defaultMessage: 'Changes applied',
|
|
83
|
-
},
|
|
84
|
-
workingCopyAppliedBy: {
|
|
85
|
-
id: 'Made by {creator} on {date}. This is not a working copy anymore, but the main content.',
|
|
86
|
-
defaultMessage:
|
|
87
|
-
'Made by {creator} on {date}. This is not a working copy anymore, but the main content.',
|
|
88
|
-
},
|
|
89
|
-
workingCopyRemovedTitle: {
|
|
90
|
-
id: 'The working copy was discarded',
|
|
91
|
-
defaultMessage: 'The working copy was discarded',
|
|
92
|
-
},
|
|
93
|
-
Unauthorized: {
|
|
94
|
-
id: 'Unauthorized',
|
|
95
|
-
defaultMessage: 'Unauthorized',
|
|
96
|
-
},
|
|
97
|
-
workingCopyErrorUnauthorized: {
|
|
98
|
-
id: 'workingCopyErrorUnauthorized',
|
|
99
|
-
defaultMessage: 'You are not authorized to perform this operation.',
|
|
100
|
-
},
|
|
101
|
-
Error: {
|
|
102
|
-
id: 'Error',
|
|
103
|
-
defaultMessage: 'Error',
|
|
104
|
-
},
|
|
105
|
-
workingCopyGenericError: {
|
|
106
|
-
id: 'workingCopyGenericError',
|
|
107
|
-
defaultMessage: 'An error occurred while performing this operation.',
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* More container class.
|
|
113
|
-
* @class More
|
|
114
|
-
* @extends Component
|
|
115
|
-
*/
|
|
116
|
-
class More extends Component {
|
|
117
|
-
static propTypes = {
|
|
118
|
-
actions: PropTypes.shape({
|
|
119
|
-
object: PropTypes.arrayOf(PropTypes.object),
|
|
120
|
-
object_buttons: PropTypes.arrayOf(PropTypes.object),
|
|
121
|
-
user: PropTypes.arrayOf(PropTypes.object),
|
|
122
|
-
}),
|
|
123
|
-
pathname: PropTypes.string.isRequired,
|
|
124
|
-
content: PropTypes.shape({
|
|
125
|
-
title: PropTypes.string,
|
|
126
|
-
'@type': PropTypes.string,
|
|
127
|
-
is_folderish: PropTypes.bool,
|
|
128
|
-
review_state: PropTypes.string,
|
|
129
|
-
}),
|
|
130
|
-
loadComponent: PropTypes.func.isRequired,
|
|
131
|
-
closeMenu: PropTypes.func.isRequired,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Default properties.
|
|
136
|
-
* @property {Object} defaultProps Default properties.
|
|
137
|
-
* @static
|
|
138
|
-
*/
|
|
139
|
-
static defaultProps = {
|
|
140
|
-
actions: null,
|
|
141
|
-
content: null,
|
|
142
|
-
};
|
|
143
|
-
state = {
|
|
144
|
-
openManageTranslations: false,
|
|
145
|
-
pushed: false,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
push = (selector) => {
|
|
149
|
-
this.setState(() => ({
|
|
150
|
-
pushed: true,
|
|
151
|
-
}));
|
|
152
|
-
this.props.loadComponent(selector);
|
|
153
|
-
document.removeEventListener('mousedown', this.handleClickOutside, false);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
componentDidUpdate(prevProps, prevState) {
|
|
157
|
-
let erroredAction = '';
|
|
158
|
-
if (
|
|
159
|
-
prevProps.workingCopy.apply.loading &&
|
|
160
|
-
this.props.workingCopy.apply.error
|
|
161
|
-
) {
|
|
162
|
-
erroredAction = 'apply';
|
|
163
|
-
} else if (
|
|
164
|
-
prevProps.workingCopy.create.loading &&
|
|
165
|
-
this.props.workingCopy.create.error
|
|
166
|
-
) {
|
|
167
|
-
erroredAction = 'create';
|
|
168
|
-
} else if (
|
|
169
|
-
prevProps.workingCopy.remove.loading &&
|
|
170
|
-
this.props.workingCopy.remove.error
|
|
171
|
-
) {
|
|
172
|
-
erroredAction = 'remove';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (erroredAction) {
|
|
176
|
-
const errorStatus = this.props.workingCopy[erroredAction].error.status;
|
|
177
|
-
if (errorStatus === 401 || errorStatus === 403) {
|
|
178
|
-
toast.error(
|
|
179
|
-
<Toast
|
|
180
|
-
error
|
|
181
|
-
title={this.props.intl.formatMessage(messages.Unauthorized)}
|
|
182
|
-
content={this.props.intl.formatMessage(
|
|
183
|
-
messages.workingCopyErrorUnauthorized,
|
|
184
|
-
)}
|
|
185
|
-
/>,
|
|
186
|
-
{
|
|
187
|
-
toastId: 'workingCopyErrorUnauthorized',
|
|
188
|
-
autoClose: 10000,
|
|
189
|
-
},
|
|
190
|
-
);
|
|
191
|
-
} else {
|
|
192
|
-
toast.error(
|
|
193
|
-
<Toast
|
|
194
|
-
error
|
|
195
|
-
title={this.props.intl.formatMessage(messages.Error)}
|
|
196
|
-
content={this.props.intl.formatMessage(
|
|
197
|
-
messages.workingCopyGenericError,
|
|
198
|
-
)}
|
|
199
|
-
/>,
|
|
200
|
-
{
|
|
201
|
-
toastId: 'workingCopyGenericError',
|
|
202
|
-
autoClose: 10000,
|
|
203
|
-
},
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Render method.
|
|
211
|
-
* @method render
|
|
212
|
-
* @returns {string} Markup for the component.
|
|
213
|
-
*/
|
|
214
|
-
render() {
|
|
215
|
-
const path = getBaseUrl(this.props.pathname);
|
|
216
|
-
const editAction = find(this.props.actions.object, { id: 'edit' });
|
|
217
|
-
const historyAction = find(this.props.actions.object, { id: 'history' });
|
|
218
|
-
const sharingAction = find(this.props.actions.object, {
|
|
219
|
-
id: 'local_roles',
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const rulesAction = find(this.props.actions.object, {
|
|
223
|
-
id: 'contentrules',
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const aliasesAction = find(this.props.actions.object_buttons, {
|
|
227
|
-
id: 'redirection',
|
|
228
|
-
});
|
|
229
|
-
const { content, intl } = this.props;
|
|
230
|
-
|
|
231
|
-
const dateOptions = {
|
|
232
|
-
year: 'numeric',
|
|
233
|
-
month: 'long',
|
|
234
|
-
day: 'numeric',
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
return (
|
|
238
|
-
<div
|
|
239
|
-
className="menu-more pastanaga-menu"
|
|
240
|
-
style={{
|
|
241
|
-
flex: this.props.theToolbar.current
|
|
242
|
-
? `0 0 ${
|
|
243
|
-
this.props.theToolbar.current.getBoundingClientRect().width
|
|
244
|
-
}px`
|
|
245
|
-
: null,
|
|
246
|
-
}}
|
|
247
|
-
>
|
|
248
|
-
<header>
|
|
249
|
-
<h2>{this.props.content.title}</h2>
|
|
250
|
-
<button
|
|
251
|
-
className="more-user"
|
|
252
|
-
aria-label={this.props.intl.formatMessage(messages.personalTools)}
|
|
253
|
-
onClick={() => this.push('personalTools')}
|
|
254
|
-
tabIndex={0}
|
|
255
|
-
>
|
|
256
|
-
<Icon
|
|
257
|
-
name={userSVG}
|
|
258
|
-
size="30px"
|
|
259
|
-
title={this.props.intl.formatMessage(messages.personalTools)}
|
|
260
|
-
/>
|
|
261
|
-
</button>
|
|
262
|
-
</header>
|
|
263
|
-
<div className="pastanaga-menu-list">
|
|
264
|
-
<ul>
|
|
265
|
-
<Pluggable name="toolbar-more-menu-list" />
|
|
266
|
-
<Plug pluggable="toolbar-more-menu-list" id="state">
|
|
267
|
-
{this.props.content['@type'] !== 'Plone Site' && (
|
|
268
|
-
// Plone Site does not have workflow
|
|
269
|
-
<li className="state-select">
|
|
270
|
-
<Workflow pathname={path} />
|
|
271
|
-
</li>
|
|
272
|
-
)}
|
|
273
|
-
</Plug>
|
|
274
|
-
<Plug pluggable="toolbar-more-menu-list" id="view">
|
|
275
|
-
{this.props.content['@type'] !== 'Plone Site' && (
|
|
276
|
-
// Plone Site does not have view (yet)
|
|
277
|
-
<li className="display-select">
|
|
278
|
-
{editAction && <Display pathname={path} />}
|
|
279
|
-
</li>
|
|
280
|
-
)}
|
|
281
|
-
</Plug>
|
|
282
|
-
<Plug pluggable="toolbar-more-menu-list" id="history">
|
|
283
|
-
{this.props.content['@type'] !== 'Plone Site' && (
|
|
284
|
-
// Plone Site does not have history (yet)
|
|
285
|
-
<li>
|
|
286
|
-
<Link to={`${path}/historyview`}>
|
|
287
|
-
<div>
|
|
288
|
-
<span className="pastanaga-menu-label">
|
|
289
|
-
{historyAction?.title ||
|
|
290
|
-
this.props.intl.formatMessage(messages.history)}
|
|
291
|
-
</span>
|
|
292
|
-
<span className="pastanaga-menu-value" />
|
|
293
|
-
</div>
|
|
294
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
295
|
-
</Link>
|
|
296
|
-
</li>
|
|
297
|
-
)}
|
|
298
|
-
</Plug>
|
|
299
|
-
<Plug pluggable="toolbar-more-menu-list" id="sharing">
|
|
300
|
-
{sharingAction && (
|
|
301
|
-
<li>
|
|
302
|
-
<Link to={`${path}/sharing`}>
|
|
303
|
-
{this.props.intl.formatMessage(messages.sharing)}
|
|
304
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
305
|
-
</Link>
|
|
306
|
-
</li>
|
|
307
|
-
)}
|
|
308
|
-
</Plug>
|
|
309
|
-
<Plug pluggable="toolbar-more-menu-list" id="aliases">
|
|
310
|
-
{aliasesAction && (
|
|
311
|
-
<li>
|
|
312
|
-
<Link to={`${path}/aliases`}>
|
|
313
|
-
{this.props.intl.formatMessage(messages.aliases)}
|
|
314
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
315
|
-
</Link>
|
|
316
|
-
</li>
|
|
317
|
-
)}
|
|
318
|
-
</Plug>
|
|
319
|
-
<Plug pluggable="toolbar-more-menu-list" id="rules">
|
|
320
|
-
{rulesAction && (
|
|
321
|
-
<li>
|
|
322
|
-
<Link to={`${path}/rules`}>
|
|
323
|
-
{this.props.intl.formatMessage(messages.rules)}
|
|
324
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
325
|
-
</Link>
|
|
326
|
-
</li>
|
|
327
|
-
)}
|
|
328
|
-
</Plug>
|
|
329
|
-
</ul>
|
|
330
|
-
</div>
|
|
331
|
-
<Pluggable name="toolbar-more-manage-content">
|
|
332
|
-
{(pluggables) => (
|
|
333
|
-
<>
|
|
334
|
-
{pluggables.length > 0 && (
|
|
335
|
-
<>
|
|
336
|
-
<header>
|
|
337
|
-
<h2>
|
|
338
|
-
{this.props.intl.formatMessage(messages.manageContent)}
|
|
339
|
-
</h2>
|
|
340
|
-
</header>
|
|
341
|
-
<div className="pastanaga-menu-list">
|
|
342
|
-
<ul>
|
|
343
|
-
{pluggables.map((p) => (
|
|
344
|
-
<React.Fragment key={p.id}>{p()}</React.Fragment>
|
|
345
|
-
))}
|
|
346
|
-
</ul>
|
|
347
|
-
</div>
|
|
348
|
-
</>
|
|
349
|
-
)}
|
|
350
|
-
</>
|
|
351
|
-
)}
|
|
352
|
-
</Pluggable>
|
|
353
|
-
{config.settings.hasWorkingCopySupport &&
|
|
354
|
-
this.props.content['@type'] !== 'Plone Site' &&
|
|
355
|
-
this.props.content['@type'] !== 'ims_indicator' && (
|
|
356
|
-
<>
|
|
357
|
-
{!this.props.content.working_copy && (
|
|
358
|
-
<Plug pluggable="toolbar-more-manage-content" id="workingcopy">
|
|
359
|
-
<li>
|
|
360
|
-
<button
|
|
361
|
-
aria-label={this.props.intl.formatMessage(
|
|
362
|
-
messages.CreateWorkingCopy,
|
|
363
|
-
)}
|
|
364
|
-
onClick={() => {
|
|
365
|
-
this.props.createWorkingCopy(path).then((response) => {
|
|
366
|
-
this.props.history.push(
|
|
367
|
-
flattenToAppURL(response['@id']),
|
|
368
|
-
);
|
|
369
|
-
this.props.closeMenu();
|
|
370
|
-
});
|
|
371
|
-
}}
|
|
372
|
-
>
|
|
373
|
-
{this.props.intl.formatMessage(
|
|
374
|
-
messages.CreateWorkingCopy,
|
|
375
|
-
)}
|
|
376
|
-
|
|
377
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
378
|
-
</button>
|
|
379
|
-
</li>
|
|
380
|
-
</Plug>
|
|
381
|
-
)}
|
|
382
|
-
{this.props.content.working_copy &&
|
|
383
|
-
this.props.content.working_copy_of && (
|
|
384
|
-
<Plug
|
|
385
|
-
pluggable="toolbar-more-manage-content"
|
|
386
|
-
id="workingcopy"
|
|
387
|
-
>
|
|
388
|
-
<li>
|
|
389
|
-
<button
|
|
390
|
-
aria-label={this.props.intl.formatMessage(
|
|
391
|
-
messages.applyWorkingCopy,
|
|
392
|
-
)}
|
|
393
|
-
onClick={() => {
|
|
394
|
-
this.props.applyWorkingCopy(path).then((response) => {
|
|
395
|
-
this.props.history.push(
|
|
396
|
-
flattenToAppURL(
|
|
397
|
-
this.props.content.working_copy_of['@id'],
|
|
398
|
-
),
|
|
399
|
-
);
|
|
400
|
-
this.props.closeMenu();
|
|
401
|
-
toast.info(
|
|
402
|
-
<Toast
|
|
403
|
-
info
|
|
404
|
-
title={intl.formatMessage(
|
|
405
|
-
messages.workingAppliedTitle,
|
|
406
|
-
)}
|
|
407
|
-
content={intl.formatMessage(
|
|
408
|
-
messages.workingCopyAppliedBy,
|
|
409
|
-
{
|
|
410
|
-
creator: content.working_copy?.creator_name,
|
|
411
|
-
date: (
|
|
412
|
-
<FormattedDate
|
|
413
|
-
date={content.working_copy?.created}
|
|
414
|
-
format={dateOptions}
|
|
415
|
-
/>
|
|
416
|
-
),
|
|
417
|
-
},
|
|
418
|
-
)}
|
|
419
|
-
/>,
|
|
420
|
-
{
|
|
421
|
-
toastId: 'workingcopyapplyinfo',
|
|
422
|
-
autoClose: 10000,
|
|
423
|
-
},
|
|
424
|
-
);
|
|
425
|
-
});
|
|
426
|
-
}}
|
|
427
|
-
>
|
|
428
|
-
{this.props.intl.formatMessage(
|
|
429
|
-
messages.applyWorkingCopy,
|
|
430
|
-
)}
|
|
431
|
-
|
|
432
|
-
<Icon
|
|
433
|
-
name={applySVG}
|
|
434
|
-
size="24px"
|
|
435
|
-
title={this.props.intl.formatMessage(
|
|
436
|
-
messages.applyWorkingCopy,
|
|
437
|
-
)}
|
|
438
|
-
/>
|
|
439
|
-
</button>
|
|
440
|
-
</li>
|
|
441
|
-
<li>
|
|
442
|
-
<button
|
|
443
|
-
aria-label={this.props.intl.formatMessage(
|
|
444
|
-
messages.removeWorkingCopy,
|
|
445
|
-
)}
|
|
446
|
-
onClick={() => {
|
|
447
|
-
this.props
|
|
448
|
-
.removeWorkingCopy(path)
|
|
449
|
-
.then((response) => {
|
|
450
|
-
this.props.history.push(
|
|
451
|
-
flattenToAppURL(
|
|
452
|
-
this.props.content.working_copy_of['@id'],
|
|
453
|
-
),
|
|
454
|
-
);
|
|
455
|
-
this.props.closeMenu();
|
|
456
|
-
toast.info(
|
|
457
|
-
<Toast
|
|
458
|
-
info
|
|
459
|
-
title={intl.formatMessage(
|
|
460
|
-
messages.workingCopyRemovedTitle,
|
|
461
|
-
)}
|
|
462
|
-
/>,
|
|
463
|
-
{
|
|
464
|
-
toastId: 'workingcopyremovednotice',
|
|
465
|
-
autoClose: 10000,
|
|
466
|
-
},
|
|
467
|
-
);
|
|
468
|
-
});
|
|
469
|
-
}}
|
|
470
|
-
>
|
|
471
|
-
{this.props.intl.formatMessage(
|
|
472
|
-
messages.removeWorkingCopy,
|
|
473
|
-
)}
|
|
474
|
-
|
|
475
|
-
<Icon
|
|
476
|
-
name={removeSVG}
|
|
477
|
-
size="24px"
|
|
478
|
-
color="#e40166"
|
|
479
|
-
title={this.props.intl.formatMessage(
|
|
480
|
-
messages.removeWorkingCopy,
|
|
481
|
-
)}
|
|
482
|
-
/>
|
|
483
|
-
</button>
|
|
484
|
-
</li>
|
|
485
|
-
</Plug>
|
|
486
|
-
)}
|
|
487
|
-
{this.props.content.working_copy &&
|
|
488
|
-
!this.props.content.working_copy_of && (
|
|
489
|
-
<Plug
|
|
490
|
-
pluggable="toolbar-more-manage-content"
|
|
491
|
-
id="workingcopy"
|
|
492
|
-
>
|
|
493
|
-
<li>
|
|
494
|
-
<Link
|
|
495
|
-
to={flattenToAppURL(
|
|
496
|
-
this.props.content.working_copy['@id'],
|
|
497
|
-
)}
|
|
498
|
-
onClick={() => this.props.closeMenu()}
|
|
499
|
-
>
|
|
500
|
-
{this.props.intl.formatMessage(
|
|
501
|
-
messages.viewWorkingCopy,
|
|
502
|
-
)}
|
|
503
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
504
|
-
</Link>
|
|
505
|
-
</li>
|
|
506
|
-
</Plug>
|
|
507
|
-
)}
|
|
508
|
-
</>
|
|
509
|
-
)}
|
|
510
|
-
{editAction &&
|
|
511
|
-
config.settings.isMultilingual &&
|
|
512
|
-
this.props.content['@type'] !== 'ims_indicator' && (
|
|
513
|
-
<Plug pluggable="toolbar-more-manage-content" id="multilingual">
|
|
514
|
-
<li>
|
|
515
|
-
<Link to={`${path}/manage-translations`}>
|
|
516
|
-
{this.props.intl.formatMessage(messages.ManageTranslations)}
|
|
517
|
-
|
|
518
|
-
<Icon name={rightArrowSVG} size="24px" />
|
|
519
|
-
</Link>
|
|
520
|
-
</li>
|
|
521
|
-
</Plug>
|
|
522
|
-
)}
|
|
523
|
-
</div>
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
export default compose(
|
|
529
|
-
injectIntl,
|
|
530
|
-
withRouter,
|
|
531
|
-
connect(
|
|
532
|
-
(state, props) => ({
|
|
533
|
-
actions: state.actions.actions,
|
|
534
|
-
pathname: props.pathname,
|
|
535
|
-
content: state.content.data,
|
|
536
|
-
lang: state.intl.locale,
|
|
537
|
-
workingCopy: state.workingCopy,
|
|
538
|
-
}),
|
|
539
|
-
{ applyWorkingCopy, createWorkingCopy, removeWorkingCopy },
|
|
540
|
-
),
|
|
541
|
-
)(More);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Workflow.jsx - copied from @plone/volto 16.22.2 - refs #256563
|
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow component.
|
|
3
|
-
* @module components/manage/Workflow/Workflow
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { FormFieldWrapper, Icon, Toast } from '@plone/volto/components';
|
|
7
|
-
import {
|
|
8
|
-
flattenToAppURL,
|
|
9
|
-
getCurrentStateMapping,
|
|
10
|
-
getWorkflowOptions,
|
|
11
|
-
} from '@plone/volto/helpers';
|
|
12
|
-
import { uniqBy } from 'lodash';
|
|
13
|
-
import PropTypes from 'prop-types';
|
|
14
|
-
import { Component, Fragment } from 'react';
|
|
15
|
-
import { defineMessages, injectIntl } from 'react-intl';
|
|
16
|
-
import { connect } from 'react-redux';
|
|
17
|
-
import { withRouter } from 'react-router-dom';
|
|
18
|
-
import { toast } from 'react-toastify';
|
|
19
|
-
import { compose } from 'redux';
|
|
20
|
-
|
|
21
|
-
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
getContent,
|
|
25
|
-
getWorkflow,
|
|
26
|
-
transitionWorkflow,
|
|
27
|
-
} from '@plone/volto/actions';
|
|
28
|
-
|
|
29
|
-
import checkSVG from '@plone/volto/icons/check.svg';
|
|
30
|
-
import downSVG from '@plone/volto/icons/down-key.svg';
|
|
31
|
-
import upSVG from '@plone/volto/icons/up-key.svg';
|
|
32
|
-
|
|
33
|
-
const messages = defineMessages({
|
|
34
|
-
messageUpdated: {
|
|
35
|
-
id: 'Workflow updated.',
|
|
36
|
-
defaultMessage: 'Workflow updated.',
|
|
37
|
-
},
|
|
38
|
-
notAllowedToUpdateWorkflow: {
|
|
39
|
-
id: 'notAllowedToUpdateWorkflow',
|
|
40
|
-
defaultMessage: 'Please fill out all the required fields',
|
|
41
|
-
},
|
|
42
|
-
messageNoWorkflow: {
|
|
43
|
-
id: 'No workflow',
|
|
44
|
-
defaultMessage: 'No workflow',
|
|
45
|
-
},
|
|
46
|
-
state: {
|
|
47
|
-
id: 'State',
|
|
48
|
-
defaultMessage: 'State',
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const filter_remaining_steps = (values, key) => {
|
|
53
|
-
return values.filter((value) => {
|
|
54
|
-
const is_not_ready = !value.is_ready;
|
|
55
|
-
if (!is_not_ready) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
const states = value.states;
|
|
59
|
-
const required_for_all = states?.indexOf('all') !== -1;
|
|
60
|
-
return (
|
|
61
|
-
(is_not_ready && required_for_all) ||
|
|
62
|
-
(is_not_ready && states?.indexOf(key) !== -1)
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const SingleValue = injectLazyLibs('reactSelect')(({ children, ...props }) => {
|
|
68
|
-
const stateDecorator = {
|
|
69
|
-
marginRight: '10px',
|
|
70
|
-
display: 'inline-block',
|
|
71
|
-
backgroundColor: props.selectProps.value.color || null,
|
|
72
|
-
content: ' ',
|
|
73
|
-
height: '10px',
|
|
74
|
-
width: '10px',
|
|
75
|
-
borderRadius: '50%',
|
|
76
|
-
};
|
|
77
|
-
const { SingleValue } = props.reactSelect.components;
|
|
78
|
-
return (
|
|
79
|
-
<SingleValue {...props}>
|
|
80
|
-
<span style={stateDecorator} />
|
|
81
|
-
{children}
|
|
82
|
-
</SingleValue>
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const Option = injectLazyLibs('reactSelect')((props) => {
|
|
87
|
-
const stateDecorator = {
|
|
88
|
-
marginRight: '10px',
|
|
89
|
-
display: 'inline-block',
|
|
90
|
-
backgroundColor:
|
|
91
|
-
props.selectProps.value.value === props.data.value
|
|
92
|
-
? props.selectProps.value.color
|
|
93
|
-
: null,
|
|
94
|
-
content: ' ',
|
|
95
|
-
height: '10px',
|
|
96
|
-
width: '10px',
|
|
97
|
-
borderRadius: '50%',
|
|
98
|
-
border:
|
|
99
|
-
props.selectProps.value.value !== props.data.value
|
|
100
|
-
? `1px solid ${props.data.color}`
|
|
101
|
-
: null,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const { Option } = props['reactSelect'].components;
|
|
105
|
-
return (
|
|
106
|
-
<Option {...props}>
|
|
107
|
-
<span style={stateDecorator} />
|
|
108
|
-
<div style={{ marginRight: 'auto' }}>{props.label}</div>
|
|
109
|
-
{props.isFocused && !props.isSelected && (
|
|
110
|
-
<Icon name={checkSVG} size="18px" color="#b8c6c8" />
|
|
111
|
-
)}
|
|
112
|
-
{props.isSelected && <Icon name={checkSVG} size="18px" color="#007bc1" />}
|
|
113
|
-
</Option>
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const DropdownIndicator = injectLazyLibs('reactSelect')((props) => {
|
|
118
|
-
const { DropdownIndicator } = props.reactSelect.components;
|
|
119
|
-
return (
|
|
120
|
-
<DropdownIndicator {...props} data-testid="workflow-select-dropdown">
|
|
121
|
-
{props.selectProps.menuIsOpen ? (
|
|
122
|
-
<Icon name={upSVG} size="24px" color="#007bc1" />
|
|
123
|
-
) : (
|
|
124
|
-
<Icon name={downSVG} size="24px" color="#007bc1" />
|
|
125
|
-
)}
|
|
126
|
-
</DropdownIndicator>
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const selectTheme = (theme) => ({
|
|
131
|
-
...theme,
|
|
132
|
-
borderRadius: 0,
|
|
133
|
-
colors: {
|
|
134
|
-
...theme.colors,
|
|
135
|
-
primary25: 'hotpink',
|
|
136
|
-
primary: '#b8c6c8',
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const customSelectStyles = {
|
|
141
|
-
control: (styles, state) => ({
|
|
142
|
-
...styles,
|
|
143
|
-
border: 'none',
|
|
144
|
-
borderBottom: '2px solid #b8c6c8',
|
|
145
|
-
boxShadow: 'none',
|
|
146
|
-
borderBottomStyle: state.menuIsOpen ? 'dotted' : 'solid',
|
|
147
|
-
}),
|
|
148
|
-
menu: (styles, state) => ({
|
|
149
|
-
...styles,
|
|
150
|
-
top: null,
|
|
151
|
-
marginTop: 0,
|
|
152
|
-
boxShadow: 'none',
|
|
153
|
-
borderBottom: '2px solid #b8c6c8',
|
|
154
|
-
}),
|
|
155
|
-
indicatorSeparator: (styles) => ({
|
|
156
|
-
...styles,
|
|
157
|
-
width: null,
|
|
158
|
-
}),
|
|
159
|
-
valueContainer: (styles) => ({
|
|
160
|
-
...styles,
|
|
161
|
-
padding: 0,
|
|
162
|
-
}),
|
|
163
|
-
option: (styles, state) => ({
|
|
164
|
-
...styles,
|
|
165
|
-
backgroundColor: null,
|
|
166
|
-
minHeight: '50px',
|
|
167
|
-
display: 'flex',
|
|
168
|
-
justifyContent: 'space-between',
|
|
169
|
-
alignItems: 'center',
|
|
170
|
-
padding: '12px 12px',
|
|
171
|
-
color: state.isSelected
|
|
172
|
-
? '#007bc1'
|
|
173
|
-
: state.isFocused
|
|
174
|
-
? '#4a4a4a'
|
|
175
|
-
: 'inherit',
|
|
176
|
-
':active': {
|
|
177
|
-
backgroundColor: null,
|
|
178
|
-
},
|
|
179
|
-
span: {
|
|
180
|
-
flex: '0 0 auto',
|
|
181
|
-
},
|
|
182
|
-
svg: {
|
|
183
|
-
flex: '0 0 auto',
|
|
184
|
-
},
|
|
185
|
-
}),
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Workflow container class.
|
|
190
|
-
* @class Workflow
|
|
191
|
-
* @extends Component
|
|
192
|
-
*/
|
|
193
|
-
class Workflow extends Component {
|
|
194
|
-
/**
|
|
195
|
-
* Property types.
|
|
196
|
-
* @property {Object} propTypes Property types.
|
|
197
|
-
* @static
|
|
198
|
-
*/
|
|
199
|
-
static propTypes = {
|
|
200
|
-
getContent: PropTypes.func.isRequired,
|
|
201
|
-
getWorkflow: PropTypes.func.isRequired,
|
|
202
|
-
transitionWorkflow: PropTypes.func.isRequired,
|
|
203
|
-
workflowLoaded: PropTypes.bool,
|
|
204
|
-
loaded: PropTypes.bool.isRequired,
|
|
205
|
-
pathname: PropTypes.string.isRequired,
|
|
206
|
-
|
|
207
|
-
contentHistory: PropTypes.arrayOf(
|
|
208
|
-
PropTypes.shape({
|
|
209
|
-
review_state: PropTypes.string,
|
|
210
|
-
}),
|
|
211
|
-
),
|
|
212
|
-
transitions: PropTypes.arrayOf(
|
|
213
|
-
PropTypes.shape({
|
|
214
|
-
'@id': PropTypes.string,
|
|
215
|
-
title: PropTypes.string,
|
|
216
|
-
}),
|
|
217
|
-
),
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Default properties
|
|
222
|
-
* @property {Object} defaultProps Default properties.
|
|
223
|
-
* @static
|
|
224
|
-
*/
|
|
225
|
-
static defaultProps = {
|
|
226
|
-
history: [],
|
|
227
|
-
transitions: [],
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
state = {
|
|
231
|
-
selectedOption: this.props.currentStateValue,
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
componentDidMount() {
|
|
235
|
-
this.props.getWorkflow(this.props.pathname);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Component will receive props
|
|
240
|
-
* @method componentWillReceiveProps
|
|
241
|
-
* @param {Object} nextProps Next properties
|
|
242
|
-
* @returns {undefined}
|
|
243
|
-
*/
|
|
244
|
-
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
245
|
-
if (nextProps.pathname !== this.props.pathname) {
|
|
246
|
-
this.props.getWorkflow(nextProps.pathname);
|
|
247
|
-
}
|
|
248
|
-
if (!this.props.loaded && nextProps.loaded) {
|
|
249
|
-
this.props.getWorkflow(nextProps.pathname);
|
|
250
|
-
this.props.getContent(nextProps.pathname);
|
|
251
|
-
}
|
|
252
|
-
if (!this.props.workflowLoaded && nextProps.workflowLoaded) {
|
|
253
|
-
this.props.getContent(nextProps.pathname);
|
|
254
|
-
// #153145 - Redirect to the newly created version
|
|
255
|
-
if (this.state?.selectedOption?.value === 'createNewVersion') {
|
|
256
|
-
this.props.history.push(`${nextProps.pathname}.1`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* On transition handler
|
|
263
|
-
* @method transition
|
|
264
|
-
* @param {string} event Event object
|
|
265
|
-
* @returns {undefined}
|
|
266
|
-
*/
|
|
267
|
-
transition = (selectedOption) => {
|
|
268
|
-
if (
|
|
269
|
-
filter_remaining_steps(
|
|
270
|
-
this.props.editingProgressSteps,
|
|
271
|
-
this.props?.content?.review_state || '',
|
|
272
|
-
).length === 0
|
|
273
|
-
) {
|
|
274
|
-
this.props.transitionWorkflow(flattenToAppURL(selectedOption.url));
|
|
275
|
-
this.setState({ selectedOption });
|
|
276
|
-
toast.success(
|
|
277
|
-
<Toast
|
|
278
|
-
success
|
|
279
|
-
title={this.props.intl.formatMessage(messages.messageUpdated)}
|
|
280
|
-
content=""
|
|
281
|
-
/>,
|
|
282
|
-
);
|
|
283
|
-
} else {
|
|
284
|
-
toast.error(
|
|
285
|
-
<Toast
|
|
286
|
-
error
|
|
287
|
-
title={this.props.intl.formatMessage(
|
|
288
|
-
messages.notAllowedToUpdateWorkflow,
|
|
289
|
-
)}
|
|
290
|
-
content=""
|
|
291
|
-
/>,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
selectValue = (option) => {
|
|
297
|
-
const stateDecorator = {
|
|
298
|
-
marginLeft: '10px',
|
|
299
|
-
marginRight: '10px',
|
|
300
|
-
display: 'inline-block',
|
|
301
|
-
backgroundColor: option.color || null,
|
|
302
|
-
content: ' ',
|
|
303
|
-
height: '10px',
|
|
304
|
-
width: '10px',
|
|
305
|
-
borderRadius: '50%',
|
|
306
|
-
};
|
|
307
|
-
return (
|
|
308
|
-
<Fragment>
|
|
309
|
-
<span style={stateDecorator} />
|
|
310
|
-
<span className="Select-value-label">{option.label}</span>
|
|
311
|
-
</Fragment>
|
|
312
|
-
);
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
optionRenderer = (option) => {
|
|
316
|
-
const stateDecorator = {
|
|
317
|
-
marginLeft: '10px',
|
|
318
|
-
marginRight: '10px',
|
|
319
|
-
display: 'inline-block',
|
|
320
|
-
backgroundColor:
|
|
321
|
-
this.props.currentStateValue.value === option.value
|
|
322
|
-
? option.color
|
|
323
|
-
: null,
|
|
324
|
-
content: ' ',
|
|
325
|
-
height: '10px',
|
|
326
|
-
width: '10px',
|
|
327
|
-
borderRadius: '50%',
|
|
328
|
-
border:
|
|
329
|
-
this.props.currentStateValue.value !== option.value
|
|
330
|
-
? `1px solid ${option.color}`
|
|
331
|
-
: null,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<Fragment>
|
|
336
|
-
<span style={stateDecorator} />
|
|
337
|
-
<span style={{ marginRight: 'auto' }}>{option.label}</span>
|
|
338
|
-
<Icon name={checkSVG} size="24px" />
|
|
339
|
-
</Fragment>
|
|
340
|
-
);
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
render() {
|
|
344
|
-
const { Placeholder } = this.props.reactSelect.components;
|
|
345
|
-
const Select = this.props.reactSelect.default;
|
|
346
|
-
// Remove markForDeletion transition if item is published
|
|
347
|
-
// in order not to un-publish items by mistake. This transition
|
|
348
|
-
// can still be executed from /contents - refs #256563, #153145
|
|
349
|
-
const transitions = this.props.transitions.filter((transition) => {
|
|
350
|
-
if (
|
|
351
|
-
transition?.['@id']?.endsWith('markForDeletion') &&
|
|
352
|
-
this.props?.content?.review_state === 'published'
|
|
353
|
-
) {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
return true;
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
return (
|
|
360
|
-
<FormFieldWrapper
|
|
361
|
-
id="state-select"
|
|
362
|
-
title={this.props.intl.formatMessage(messages.state)}
|
|
363
|
-
{...this.props}
|
|
364
|
-
>
|
|
365
|
-
<Select
|
|
366
|
-
name="state-select"
|
|
367
|
-
className="react-select-container"
|
|
368
|
-
classNamePrefix="react-select"
|
|
369
|
-
isDisabled={
|
|
370
|
-
!this.props.content.review_state || transitions.length === 0
|
|
371
|
-
}
|
|
372
|
-
options={uniqBy(
|
|
373
|
-
transitions.map((transition) => getWorkflowOptions(transition)),
|
|
374
|
-
'label',
|
|
375
|
-
).concat(this.props.currentStateValue)}
|
|
376
|
-
styles={customSelectStyles}
|
|
377
|
-
theme={selectTheme}
|
|
378
|
-
components={{
|
|
379
|
-
DropdownIndicator,
|
|
380
|
-
Placeholder,
|
|
381
|
-
Option,
|
|
382
|
-
SingleValue,
|
|
383
|
-
}}
|
|
384
|
-
onChange={this.transition}
|
|
385
|
-
value={
|
|
386
|
-
this.props.content.review_state
|
|
387
|
-
? this.props.currentStateValue
|
|
388
|
-
: {
|
|
389
|
-
label: this.props.intl.formatMessage(
|
|
390
|
-
messages.messageNoWorkflow,
|
|
391
|
-
),
|
|
392
|
-
value: 'noworkflow',
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
isSearchable={false}
|
|
396
|
-
/>
|
|
397
|
-
</FormFieldWrapper>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export default compose(
|
|
403
|
-
injectIntl,
|
|
404
|
-
injectLazyLibs(['reactSelect']),
|
|
405
|
-
withRouter,
|
|
406
|
-
connect(
|
|
407
|
-
(state, props) => ({
|
|
408
|
-
loaded: state.workflow.transition.loaded,
|
|
409
|
-
content: state.content.data,
|
|
410
|
-
workflowLoaded: state.workflow.get?.loaded,
|
|
411
|
-
contentHistory: state.workflow.history,
|
|
412
|
-
transitions: state.workflow.transitions,
|
|
413
|
-
currentStateValue: getCurrentStateMapping(state.workflow.currentState),
|
|
414
|
-
editingProgressSteps:
|
|
415
|
-
state?.editingProgress?.editing?.loaded === true
|
|
416
|
-
? state?.editingProgress?.result?.steps
|
|
417
|
-
: [],
|
|
418
|
-
}),
|
|
419
|
-
{ getContent, getWorkflow, transitionWorkflow },
|
|
420
|
-
),
|
|
421
|
-
)(Workflow);
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import configureStore from 'redux-mock-store';
|
|
3
|
-
import { Provider } from 'react-intl-redux';
|
|
4
|
-
import { waitFor, render, screen } from '@testing-library/react';
|
|
5
|
-
import { MemoryRouter } from 'react-router-dom';
|
|
6
|
-
import config from '@plone/volto/registry';
|
|
7
|
-
|
|
8
|
-
import Workflow from './Workflow';
|
|
9
|
-
|
|
10
|
-
const mockStore = configureStore();
|
|
11
|
-
|
|
12
|
-
jest.mock('@plone/volto/helpers/Loadable/Loadable');
|
|
13
|
-
beforeAll(
|
|
14
|
-
async () =>
|
|
15
|
-
await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
config.settings.workflowMapping = {
|
|
20
|
-
published: { value: 'published', color: '#007bc1' },
|
|
21
|
-
publish: { value: 'publish', color: '#007bc1' },
|
|
22
|
-
private: { value: 'private', color: '#ed4033' },
|
|
23
|
-
pending: { value: 'pending', color: '#f6a808' },
|
|
24
|
-
send_back: { value: 'private', color: '#ed4033' },
|
|
25
|
-
retract: { value: 'private', color: '#ed4033' },
|
|
26
|
-
submit: { value: 'review', color: '#f4e037' },
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('Workflow', () => {
|
|
31
|
-
it('renders an empty workflow component', async () => {
|
|
32
|
-
const store = mockStore({
|
|
33
|
-
workflow: {
|
|
34
|
-
currentState: { id: 'published', title: 'Published' },
|
|
35
|
-
history: [],
|
|
36
|
-
transition: { loaded: true },
|
|
37
|
-
transitions: [],
|
|
38
|
-
},
|
|
39
|
-
intl: {
|
|
40
|
-
locale: 'en',
|
|
41
|
-
messages: {},
|
|
42
|
-
},
|
|
43
|
-
content: { data: { review_state: 'published' } },
|
|
44
|
-
});
|
|
45
|
-
const { container } = render(
|
|
46
|
-
<Provider store={store}>
|
|
47
|
-
<MemoryRouter>
|
|
48
|
-
<Workflow pathname="/test" />
|
|
49
|
-
</MemoryRouter>
|
|
50
|
-
</Provider>,
|
|
51
|
-
);
|
|
52
|
-
await waitFor(() => screen.getByText(/Published/));
|
|
53
|
-
expect(container).toMatchSnapshot();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('renders a workflow component', async () => {
|
|
57
|
-
const store = mockStore({
|
|
58
|
-
workflow: {
|
|
59
|
-
currentState: { id: 'private', title: 'Private' },
|
|
60
|
-
history: [{ review_state: 'private' }],
|
|
61
|
-
transition: { loaded: true },
|
|
62
|
-
transitions: [{ '@id': 'http://publish', title: 'Publish' }],
|
|
63
|
-
},
|
|
64
|
-
intl: {
|
|
65
|
-
locale: 'en',
|
|
66
|
-
messages: {},
|
|
67
|
-
},
|
|
68
|
-
content: { data: { review_state: 'private' } },
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const { container } = render(
|
|
72
|
-
<Provider store={store}>
|
|
73
|
-
<MemoryRouter>
|
|
74
|
-
<Workflow pathname="/test" />
|
|
75
|
-
</MemoryRouter>
|
|
76
|
-
</Provider>,
|
|
77
|
-
);
|
|
78
|
-
await waitFor(() => screen.getByText('Private'));
|
|
79
|
-
expect(container).toMatchSnapshot();
|
|
80
|
-
});
|
|
81
|
-
});
|