@eeacms/volto-cca-policy 0.3.124 → 0.3.126
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 +161 -0
- package/artifacts/BLOCKS.md +167 -0
- package/artifacts/link-integrity-workflow/README.md +69 -0
- package/artifacts/link-integrity-workflow/index.md +33 -0
- package/artifacts/link-integrity-workflow/link-integrity-block-fields-report.md +132 -0
- package/artifacts/link-integrity-workflow/link-integrity-blocks-report.md +52 -0
- package/artifacts/link-integrity-workflow/understanding-link-integrity.md +143 -0
- package/artifacts/link-integrity-workflow/volto-block-link-analysis.md +63 -0
- package/artifacts/link-integrity-workflow/volto-block-link-discovery.md +60 -0
- package/artifacts/test-fixes/test-fixes-specification.md +267 -0
- package/jest-addon.config.js +1 -0
- package/jest-node-crypto-mock.js +3 -0
- package/package.json +1 -1
- package/src/components/index.js +1 -0
- package/src/components/manage/Blocks/ReadMore/ReadMoreView.test.jsx +13 -0
- package/src/components/manage/Workflow/WorkflowLinkIntegrityModal.jsx +186 -0
- package/src/components/manage/Workflow/WorkflowLinkIntegrityModal.test.jsx +149 -0
- package/src/components/theme/Views/ArchivedVersionListing.test.jsx +14 -18
- package/src/components/theme/Views/CcaEventView.test.jsx +12 -3
- package/src/components/theme/Views/DatabaseItemView.test.jsx +2 -2
- package/src/components/theme/Widgets/GeolocationWidget.jsx +1 -1
- package/src/customizations/volto/components/manage/Workflow/README.md +23 -0
- package/src/customizations/volto/components/manage/Workflow/Workflow.jsx +359 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
3
4
|
import '@testing-library/jest-dom/extend-expect';
|
|
4
5
|
import { render, screen } from '@testing-library/react';
|
|
5
6
|
import ArchivedVersionListing from './ArchivedVersionListing';
|
|
@@ -21,16 +22,21 @@ jest.mock('@eeacms/volto-cca-policy/components', () => ({
|
|
|
21
22
|
),
|
|
22
23
|
}));
|
|
23
24
|
|
|
25
|
+
const renderWithIntl = (ui) =>
|
|
26
|
+
render(
|
|
27
|
+
<IntlProvider locale="en">
|
|
28
|
+
<MemoryRouter>{ui}</MemoryRouter>
|
|
29
|
+
</IntlProvider>,
|
|
30
|
+
);
|
|
31
|
+
|
|
24
32
|
describe('ArchivedVersionListing', () => {
|
|
25
33
|
it('returns null when there are no archived versions', () => {
|
|
26
34
|
const content = {
|
|
27
35
|
archived_versions: [],
|
|
28
36
|
};
|
|
29
37
|
|
|
30
|
-
const { container } =
|
|
31
|
-
<
|
|
32
|
-
<ArchivedVersionListing content={content} />
|
|
33
|
-
</MemoryRouter>,
|
|
38
|
+
const { container } = renderWithIntl(
|
|
39
|
+
<ArchivedVersionListing content={content} />,
|
|
34
40
|
);
|
|
35
41
|
|
|
36
42
|
expect(container.innerHTML).toBe('');
|
|
@@ -39,10 +45,8 @@ describe('ArchivedVersionListing', () => {
|
|
|
39
45
|
it('returns null when archived_versions is missing', () => {
|
|
40
46
|
const content = {};
|
|
41
47
|
|
|
42
|
-
const { container } =
|
|
43
|
-
<
|
|
44
|
-
<ArchivedVersionListing content={content} />
|
|
45
|
-
</MemoryRouter>,
|
|
48
|
+
const { container } = renderWithIntl(
|
|
49
|
+
<ArchivedVersionListing content={content} />,
|
|
46
50
|
);
|
|
47
51
|
|
|
48
52
|
expect(container.innerHTML).toBe('');
|
|
@@ -62,11 +66,7 @@ describe('ArchivedVersionListing', () => {
|
|
|
62
66
|
],
|
|
63
67
|
};
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
<MemoryRouter>
|
|
67
|
-
<ArchivedVersionListing content={content} />
|
|
68
|
-
</MemoryRouter>,
|
|
69
|
-
);
|
|
69
|
+
renderWithIntl(<ArchivedVersionListing content={content} />);
|
|
70
70
|
|
|
71
71
|
expect(
|
|
72
72
|
screen.getByText('Previous versions of this indicator'),
|
|
@@ -92,11 +92,7 @@ describe('ArchivedVersionListing', () => {
|
|
|
92
92
|
],
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
<MemoryRouter>
|
|
97
|
-
<ArchivedVersionListing content={content} />
|
|
98
|
-
</MemoryRouter>,
|
|
99
|
-
);
|
|
95
|
+
renderWithIntl(<ArchivedVersionListing content={content} />);
|
|
100
96
|
|
|
101
97
|
expect(screen.getByText('/indicator-v1')).toHaveAttribute(
|
|
102
98
|
'href',
|
|
@@ -64,7 +64,10 @@ test('renders an event view component with all props', () => {
|
|
|
64
64
|
subjects: ['Guillotina', 'Volto'],
|
|
65
65
|
whole_day: false,
|
|
66
66
|
agenda_files: {},
|
|
67
|
-
background_documents: {
|
|
67
|
+
background_documents: {
|
|
68
|
+
download: 'http://localhost:8080/Plone/my-page/background.pdf',
|
|
69
|
+
filename: 'background.pdf',
|
|
70
|
+
},
|
|
68
71
|
}}
|
|
69
72
|
/>
|
|
70
73
|
</Provider>,
|
|
@@ -85,7 +88,10 @@ test('renders an event view component with only required props', () => {
|
|
|
85
88
|
start: '2019-06-23T15:20:00+00:00',
|
|
86
89
|
subjects: [],
|
|
87
90
|
agenda_files: {},
|
|
88
|
-
background_documents: {
|
|
91
|
+
background_documents: {
|
|
92
|
+
download: 'http://localhost:8080/Plone/my-page/background.pdf',
|
|
93
|
+
filename: 'background.pdf',
|
|
94
|
+
},
|
|
89
95
|
}}
|
|
90
96
|
/>
|
|
91
97
|
</Provider>,
|
|
@@ -106,7 +112,10 @@ test('renders an event view component without links to api in the text', () => {
|
|
|
106
112
|
start: '2019-06-23T15:20:00+00:00',
|
|
107
113
|
subjects: [],
|
|
108
114
|
agenda_files: {},
|
|
109
|
-
background_documents: {
|
|
115
|
+
background_documents: {
|
|
116
|
+
download: 'http://localhost:8080/Plone/my-page/background.pdf',
|
|
117
|
+
filename: 'background.pdf',
|
|
118
|
+
},
|
|
110
119
|
text: {
|
|
111
120
|
data: `<p>Hello World!</p><p>This is an <a href="${settings.apiPath}/foo/bar">internal link</a> and a <a href="${settings.apiPath}/foo/baz">second link</a></p>`,
|
|
112
121
|
},
|
|
@@ -5,7 +5,7 @@ import config from '@plone/volto/registry';
|
|
|
5
5
|
|
|
6
6
|
import { injectIntl } from 'react-intl';
|
|
7
7
|
import { FormFieldWrapper } from '@plone/volto/components';
|
|
8
|
-
import MapContainer from '
|
|
8
|
+
import MapContainer from '@eeacms/volto-cca-policy/components/theme/Widgets/GeolocationWidgetMapContainer';
|
|
9
9
|
|
|
10
10
|
const defaultValue = {
|
|
11
11
|
latitude: 55.6761,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Workflow Component Customization
|
|
2
|
+
|
|
3
|
+
This component shadows the core Volto `Workflow` component located at `@plone/volto/components/manage/Workflow/Workflow.jsx`.
|
|
4
|
+
|
|
5
|
+
## Why this is needed
|
|
6
|
+
|
|
7
|
+
In Climate-ADAPT, we want to prevent users from unintentionally breaking internal links when making content private. Volto's core workflow transition dropdown does not perform any link integrity checks before executing a state change.
|
|
8
|
+
|
|
9
|
+
By shadowing this component, we can intercept transitions that might hide content from public users and warn the editor if other pages are linking to the current item.
|
|
10
|
+
|
|
11
|
+
## Modifications
|
|
12
|
+
|
|
13
|
+
1. **Interception Logic**: The `transition` function was modified to check for "private-like" transitions (`private`, `reject`, `retract`).
|
|
14
|
+
2. **Link Integrity Check**: When a sensitive transition is selected, the `linkIntegrityCheck` action is dispatched to the backend.
|
|
15
|
+
3. **State Management**: Added local state (`showWarningModal`, `pendingOption`) to handle the asynchronous check and the confirmation flow.
|
|
16
|
+
4. **Confirmation Modal**: Integrated `WorkflowLinkIntegrityModal` which displays the list of pages that would have broken links.
|
|
17
|
+
5. **Auto-proceed**: Added an `useEffect` that automatically executes the transition if the link integrity check returns zero breaches.
|
|
18
|
+
6. **Activity Indicators**: Added `Dimmer` and `Loader` components from `semantic-ui-react` to provide visual feedback while the link integrity check is loading and during the workflow transition execution.
|
|
19
|
+
|
|
20
|
+
## Reference
|
|
21
|
+
|
|
22
|
+
See implementation details in:
|
|
23
|
+
`artifacts/link-integrity-workflow/understanding-link-integrity.md`
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { compose } from 'redux';
|
|
4
|
+
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
|
5
|
+
import { uniqBy } from 'lodash';
|
|
6
|
+
import { toast } from 'react-toastify';
|
|
7
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
8
|
+
|
|
9
|
+
import { FormFieldWrapper, Icon, Toast } from '@plone/volto/components';
|
|
10
|
+
import { Dimmer, Loader } from 'semantic-ui-react';
|
|
11
|
+
import {
|
|
12
|
+
flattenToAppURL,
|
|
13
|
+
getWorkflowOptions,
|
|
14
|
+
getCurrentStateMapping,
|
|
15
|
+
} from '@plone/volto/helpers';
|
|
16
|
+
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
getContent,
|
|
20
|
+
getWorkflow,
|
|
21
|
+
transitionWorkflow,
|
|
22
|
+
linkIntegrityCheck,
|
|
23
|
+
} from '@plone/volto/actions';
|
|
24
|
+
import WorkflowLinkIntegrityModal from '@eeacms/volto-cca-policy/components/manage/Workflow/WorkflowLinkIntegrityModal';
|
|
25
|
+
import downSVG from '@plone/volto/icons/down-key.svg';
|
|
26
|
+
import upSVG from '@plone/volto/icons/up-key.svg';
|
|
27
|
+
import checkSVG from '@plone/volto/icons/check.svg';
|
|
28
|
+
|
|
29
|
+
const messages = defineMessages({
|
|
30
|
+
messageUpdated: {
|
|
31
|
+
id: 'Workflow updated.',
|
|
32
|
+
defaultMessage: 'Workflow updated.',
|
|
33
|
+
},
|
|
34
|
+
messageNoWorkflow: {
|
|
35
|
+
id: 'No workflow',
|
|
36
|
+
defaultMessage: 'No workflow',
|
|
37
|
+
},
|
|
38
|
+
state: {
|
|
39
|
+
id: 'State',
|
|
40
|
+
defaultMessage: 'State',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const SingleValue = injectLazyLibs('reactSelect')(({ children, ...props }) => {
|
|
45
|
+
const stateDecorator = {
|
|
46
|
+
marginRight: '10px',
|
|
47
|
+
display: 'inline-block',
|
|
48
|
+
backgroundColor: props.selectProps.value.color || null,
|
|
49
|
+
content: ' ',
|
|
50
|
+
height: '10px',
|
|
51
|
+
width: '10px',
|
|
52
|
+
borderRadius: '50%',
|
|
53
|
+
};
|
|
54
|
+
const { SingleValue } = props.reactSelect.components;
|
|
55
|
+
return (
|
|
56
|
+
<SingleValue {...props}>
|
|
57
|
+
<span style={stateDecorator} />
|
|
58
|
+
{children}
|
|
59
|
+
</SingleValue>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const Option = injectLazyLibs('reactSelect')((props) => {
|
|
64
|
+
const stateDecorator = {
|
|
65
|
+
marginRight: '10px',
|
|
66
|
+
display: 'inline-block',
|
|
67
|
+
backgroundColor:
|
|
68
|
+
props.selectProps.value.value === props.data.value
|
|
69
|
+
? props.selectProps.value.color
|
|
70
|
+
: null,
|
|
71
|
+
content: ' ',
|
|
72
|
+
height: '10px',
|
|
73
|
+
width: '10px',
|
|
74
|
+
borderRadius: '50%',
|
|
75
|
+
border:
|
|
76
|
+
props.selectProps.value.value !== props.data.value
|
|
77
|
+
? `1px solid ${props.data.color}`
|
|
78
|
+
: null,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const { Option } = props['reactSelect'].components;
|
|
82
|
+
return (
|
|
83
|
+
<Option {...props}>
|
|
84
|
+
<span style={stateDecorator} />
|
|
85
|
+
<div style={{ marginRight: 'auto' }}>{props.label}</div>
|
|
86
|
+
{props.isFocused && !props.isSelected && (
|
|
87
|
+
<Icon name={checkSVG} size="18px" color="#b8c6c8" />
|
|
88
|
+
)}
|
|
89
|
+
{props.isSelected && <Icon name={checkSVG} size="18px" color="#007bc1" />}
|
|
90
|
+
</Option>
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const DropdownIndicator = injectLazyLibs('reactSelect')((props) => {
|
|
95
|
+
const { DropdownIndicator } = props.reactSelect.components;
|
|
96
|
+
return (
|
|
97
|
+
<DropdownIndicator {...props} data-testid="workflow-select-dropdown">
|
|
98
|
+
{props.selectProps.menuIsOpen ? (
|
|
99
|
+
<Icon name={upSVG} size="24px" color="#007bc1" />
|
|
100
|
+
) : (
|
|
101
|
+
<Icon name={downSVG} size="24px" color="#007bc1" />
|
|
102
|
+
)}
|
|
103
|
+
</DropdownIndicator>
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const selectTheme = (theme) => ({
|
|
108
|
+
...theme,
|
|
109
|
+
borderRadius: 0,
|
|
110
|
+
colors: {
|
|
111
|
+
...theme.colors,
|
|
112
|
+
primary25: 'hotpink',
|
|
113
|
+
primary: '#b8c6c8',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const customSelectStyles = {
|
|
118
|
+
control: (styles, state) => ({
|
|
119
|
+
...styles,
|
|
120
|
+
border: 'none',
|
|
121
|
+
borderBottom: '2px solid #b8c6c8',
|
|
122
|
+
boxShadow: 'none',
|
|
123
|
+
borderBottomStyle: state.menuIsOpen ? 'dotted' : 'solid',
|
|
124
|
+
}),
|
|
125
|
+
menu: (styles, state) => ({
|
|
126
|
+
...styles,
|
|
127
|
+
top: null,
|
|
128
|
+
marginTop: 0,
|
|
129
|
+
boxShadow: 'none',
|
|
130
|
+
borderBottom: '2px solid #b8c6c8',
|
|
131
|
+
}),
|
|
132
|
+
menuList: (styles, state) => ({
|
|
133
|
+
...styles,
|
|
134
|
+
maxHeight: '400px',
|
|
135
|
+
}),
|
|
136
|
+
indicatorSeparator: (styles) => ({
|
|
137
|
+
...styles,
|
|
138
|
+
width: null,
|
|
139
|
+
}),
|
|
140
|
+
valueContainer: (styles) => ({
|
|
141
|
+
...styles,
|
|
142
|
+
padding: 0,
|
|
143
|
+
}),
|
|
144
|
+
option: (styles, state) => ({
|
|
145
|
+
...styles,
|
|
146
|
+
backgroundColor: null,
|
|
147
|
+
minHeight: '50px',
|
|
148
|
+
display: 'flex',
|
|
149
|
+
justifyContent: 'space-between',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
padding: '12px 12px',
|
|
152
|
+
color: state.isSelected
|
|
153
|
+
? '#007bc1'
|
|
154
|
+
: state.isFocused
|
|
155
|
+
? '#4a4a4a'
|
|
156
|
+
: 'inherit',
|
|
157
|
+
':active': {
|
|
158
|
+
backgroundColor: null,
|
|
159
|
+
},
|
|
160
|
+
span: {
|
|
161
|
+
flex: '0 0 auto',
|
|
162
|
+
},
|
|
163
|
+
svg: {
|
|
164
|
+
flex: '0 0 auto',
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function useWorkflow() {
|
|
170
|
+
const history = useSelector((state) => state.workflow.history, shallowEqual);
|
|
171
|
+
const transitions = useSelector(
|
|
172
|
+
(state) => state.workflow.transitions,
|
|
173
|
+
shallowEqual,
|
|
174
|
+
);
|
|
175
|
+
const loaded = useSelector((state) => state.workflow.transition.loaded);
|
|
176
|
+
const transitionLoading = useSelector(
|
|
177
|
+
(state) => state.workflow.transition.loading,
|
|
178
|
+
);
|
|
179
|
+
const currentStateValue = useSelector(
|
|
180
|
+
(state) => getCurrentStateMapping(state.workflow.currentState),
|
|
181
|
+
shallowEqual,
|
|
182
|
+
);
|
|
183
|
+
const linkintegrityInfo = useSelector((state) => state.linkIntegrity?.result);
|
|
184
|
+
const linkintegrityLoaded = useSelector(
|
|
185
|
+
(state) => state.linkIntegrity?.loaded,
|
|
186
|
+
);
|
|
187
|
+
const linkintegrityLoading = useSelector(
|
|
188
|
+
(state) => state.linkIntegrity?.loading,
|
|
189
|
+
);
|
|
190
|
+
const linkintegrityError = useSelector((state) => state.linkIntegrity?.error);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
loaded,
|
|
194
|
+
transitionLoading,
|
|
195
|
+
history,
|
|
196
|
+
transitions,
|
|
197
|
+
currentStateValue,
|
|
198
|
+
linkintegrityInfo,
|
|
199
|
+
linkintegrityLoaded,
|
|
200
|
+
linkintegrityLoading,
|
|
201
|
+
linkintegrityError,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const Workflow = (props) => {
|
|
206
|
+
const intl = useIntl();
|
|
207
|
+
const dispatch = useDispatch();
|
|
208
|
+
const {
|
|
209
|
+
loaded,
|
|
210
|
+
transitionLoading,
|
|
211
|
+
transitions,
|
|
212
|
+
currentStateValue,
|
|
213
|
+
linkintegrityInfo,
|
|
214
|
+
linkintegrityLoaded,
|
|
215
|
+
linkintegrityLoading,
|
|
216
|
+
linkintegrityError,
|
|
217
|
+
} = useWorkflow();
|
|
218
|
+
const content = useSelector((state) => state.content?.data, shallowEqual);
|
|
219
|
+
const { pathname } = props;
|
|
220
|
+
|
|
221
|
+
const [showWarningModal, setShowWarningModal] = useState(false);
|
|
222
|
+
const [pendingOption, setPendingOption] = useState(null);
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
dispatch(getWorkflow(pathname));
|
|
226
|
+
dispatch(getContent(pathname));
|
|
227
|
+
}, [dispatch, pathname, loaded]);
|
|
228
|
+
|
|
229
|
+
const executeTransition = useCallback(
|
|
230
|
+
(selectedOption) => {
|
|
231
|
+
if (selectedOption?.url) {
|
|
232
|
+
dispatch(transitionWorkflow(flattenToAppURL(selectedOption.url)));
|
|
233
|
+
toast.success(
|
|
234
|
+
<Toast
|
|
235
|
+
success
|
|
236
|
+
title={intl.formatMessage(messages.messageUpdated)}
|
|
237
|
+
content=""
|
|
238
|
+
/>,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
[dispatch, intl],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (showWarningModal) {
|
|
247
|
+
if (linkintegrityError) {
|
|
248
|
+
// If the check fails, we shouldn't block the user forever. Proceed with transition.
|
|
249
|
+
executeTransition(pendingOption);
|
|
250
|
+
setShowWarningModal(false);
|
|
251
|
+
setPendingOption(null);
|
|
252
|
+
} else if (
|
|
253
|
+
linkintegrityLoaded &&
|
|
254
|
+
linkintegrityInfo &&
|
|
255
|
+
flattenToAppURL(linkintegrityInfo[0]?.['@id']) ===
|
|
256
|
+
flattenToAppURL(content?.['@id'])
|
|
257
|
+
) {
|
|
258
|
+
const breaches = linkintegrityInfo.flatMap((result) => result.breaches);
|
|
259
|
+
if (breaches.length === 0) {
|
|
260
|
+
executeTransition(pendingOption);
|
|
261
|
+
setShowWarningModal(false);
|
|
262
|
+
setPendingOption(null);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, [
|
|
267
|
+
linkintegrityLoaded,
|
|
268
|
+
linkintegrityInfo,
|
|
269
|
+
linkintegrityError,
|
|
270
|
+
showWarningModal,
|
|
271
|
+
pendingOption,
|
|
272
|
+
content,
|
|
273
|
+
executeTransition,
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
const transition = (selectedOption) => {
|
|
277
|
+
const isPrivateTransition =
|
|
278
|
+
['private', 'reject', 'retract'].includes(selectedOption.value) ||
|
|
279
|
+
selectedOption.url.endsWith('/reject') ||
|
|
280
|
+
selectedOption.url.endsWith('/retract');
|
|
281
|
+
|
|
282
|
+
if (isPrivateTransition) {
|
|
283
|
+
setPendingOption(selectedOption);
|
|
284
|
+
dispatch(linkIntegrityCheck([content.UID]));
|
|
285
|
+
setShowWarningModal(true);
|
|
286
|
+
} else {
|
|
287
|
+
executeTransition(selectedOption);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const { Placeholder } = props.reactSelect.components;
|
|
292
|
+
const Select = props.reactSelect.default;
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<>
|
|
296
|
+
<FormFieldWrapper
|
|
297
|
+
id="state-select"
|
|
298
|
+
title={intl.formatMessage(messages.state)}
|
|
299
|
+
intl={intl}
|
|
300
|
+
{...props}
|
|
301
|
+
>
|
|
302
|
+
<Dimmer active={transitionLoading} inverted>
|
|
303
|
+
<Loader size="small" />
|
|
304
|
+
</Dimmer>
|
|
305
|
+
<Select
|
|
306
|
+
name="state-select"
|
|
307
|
+
className="react-select-container"
|
|
308
|
+
classNamePrefix="react-select"
|
|
309
|
+
isDisabled={
|
|
310
|
+
!content.review_state ||
|
|
311
|
+
transitions.length === 0 ||
|
|
312
|
+
transitionLoading ||
|
|
313
|
+
(showWarningModal && linkintegrityLoading)
|
|
314
|
+
}
|
|
315
|
+
options={uniqBy(
|
|
316
|
+
transitions.map((transition) => getWorkflowOptions(transition)),
|
|
317
|
+
'label',
|
|
318
|
+
).concat(currentStateValue)}
|
|
319
|
+
styles={customSelectStyles}
|
|
320
|
+
theme={selectTheme}
|
|
321
|
+
components={{
|
|
322
|
+
DropdownIndicator,
|
|
323
|
+
Placeholder,
|
|
324
|
+
Option,
|
|
325
|
+
SingleValue,
|
|
326
|
+
}}
|
|
327
|
+
onChange={transition}
|
|
328
|
+
value={
|
|
329
|
+
content.review_state
|
|
330
|
+
? currentStateValue
|
|
331
|
+
: {
|
|
332
|
+
label: intl.formatMessage(messages.messageNoWorkflow),
|
|
333
|
+
value: 'noworkflow',
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
isSearchable={false}
|
|
337
|
+
/>
|
|
338
|
+
</FormFieldWrapper>
|
|
339
|
+
<WorkflowLinkIntegrityModal
|
|
340
|
+
open={showWarningModal}
|
|
341
|
+
onCancel={() => {
|
|
342
|
+
setShowWarningModal(false);
|
|
343
|
+
setPendingOption(null);
|
|
344
|
+
}}
|
|
345
|
+
onOk={() => {
|
|
346
|
+
executeTransition(pendingOption);
|
|
347
|
+
setShowWarningModal(false);
|
|
348
|
+
setPendingOption(null);
|
|
349
|
+
}}
|
|
350
|
+
/>
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
Workflow.propTypes = {
|
|
356
|
+
pathname: PropTypes.string.isRequired,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export default compose(injectLazyLibs(['reactSelect']))(Workflow);
|