@eeacms/volto-eea-website-policy 1.15.0 → 2.1.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.
@@ -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
- });