@eeacms/volto-marine-policy 2.0.16 → 2.0.18
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 +44 -0
- package/package.json +9 -3
- package/src/components/index.js +2 -1
- package/src/components/theme/NISListingView/NISListingView.jsx +185 -23
- package/src/components/theme/NISListingView/style.less +137 -1
- package/src/components/theme/NISMetadataSectionTableView/NISMetadataSectionTableView.jsx +12 -0
- package/src/components/theme/NISMetadataSectionTableView/style.less +18 -0
- package/src/components/theme/ProgressWorkflow/ProgressWorkflow.jsx +474 -0
- package/src/customizations/@plone-collective/volto-authomatic/components/Login/Login.jsx +389 -0
- package/src/customizations/@plone-collective/volto-authomatic/components/Login/Login.less +8 -0
- package/src/index.js +18 -4
- package/src/reducers/index.js +1 -0
- package/src/reducers/workflowprogress/workflow.js +86 -0
- package/theme/globals/site.overrides +62 -60
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
7
|
+
import Select from 'react-select';
|
|
8
|
+
import { toast } from 'react-toastify';
|
|
9
|
+
import last from 'lodash/last';
|
|
10
|
+
import split from 'lodash/split';
|
|
11
|
+
import uniqBy from 'lodash/uniqBy';
|
|
12
|
+
import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib';
|
|
13
|
+
import Toast from '@plone/volto/components/manage/Toast/Toast';
|
|
14
|
+
// import {
|
|
15
|
+
// getWorkflowOptions,
|
|
16
|
+
// getCurrentStateMapping,
|
|
17
|
+
// } from '@plone/volto/helpers/Workflows/Workflows';
|
|
18
|
+
import { transitionWorkflow } from '@plone/volto/actions/workflow/workflow';
|
|
19
|
+
import '@eeacms/volto-workflow-progress/less/editor.less';
|
|
20
|
+
|
|
21
|
+
const currentStateClass = {
|
|
22
|
+
draft: 'draft',
|
|
23
|
+
submitted: 'submitted',
|
|
24
|
+
approved: 'approved',
|
|
25
|
+
published: 'published',
|
|
26
|
+
private: 'private',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const selectTheme = (theme) => ({
|
|
30
|
+
...theme,
|
|
31
|
+
borderRadius: 0,
|
|
32
|
+
colors: {
|
|
33
|
+
...theme.colors,
|
|
34
|
+
primary25: 'hotpink',
|
|
35
|
+
primary: '#b8c6c8',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const messages = defineMessages({
|
|
40
|
+
messageUpdated: {
|
|
41
|
+
id: 'Workflow updated.',
|
|
42
|
+
defaultMessage: 'Workflow updated.',
|
|
43
|
+
},
|
|
44
|
+
messageNoWorkflow: {
|
|
45
|
+
id: 'No workflow',
|
|
46
|
+
defaultMessage: 'No workflow',
|
|
47
|
+
},
|
|
48
|
+
state: {
|
|
49
|
+
id: 'State',
|
|
50
|
+
defaultMessage: 'State',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const getWorkflowOptions = (transition) => {
|
|
55
|
+
const mapping = config.settings.workflowMapping;
|
|
56
|
+
const key = last(split(transition['@id'], '/'));
|
|
57
|
+
|
|
58
|
+
if (key in mapping) {
|
|
59
|
+
return {
|
|
60
|
+
new_state_id: transition.new_state_id,
|
|
61
|
+
label: transition.title,
|
|
62
|
+
...mapping[key],
|
|
63
|
+
url: transition['@id'],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Return an option with a neutral color
|
|
68
|
+
return {
|
|
69
|
+
new_state_id: transition.new_state_id,
|
|
70
|
+
value: key,
|
|
71
|
+
label: transition.title,
|
|
72
|
+
color: '#000',
|
|
73
|
+
url: transition['@id'],
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const customSelectStyles = {
|
|
78
|
+
control: (styles, state) => ({
|
|
79
|
+
...styles,
|
|
80
|
+
// border: 'none',
|
|
81
|
+
border: '1px solid #b8c6c8',
|
|
82
|
+
borderRadius: '0.25rem',
|
|
83
|
+
borderBottom: '1px solid #b8c6c8',
|
|
84
|
+
boxShadow: 'none',
|
|
85
|
+
borderBottomStyle: state.menuIsOpen ? 'dotted' : 'solid',
|
|
86
|
+
}),
|
|
87
|
+
menu: (styles, state) => ({
|
|
88
|
+
...styles,
|
|
89
|
+
top: null,
|
|
90
|
+
marginTop: 0,
|
|
91
|
+
boxShadow: 'none',
|
|
92
|
+
borderBottom: '2px solid #b8c6c8',
|
|
93
|
+
}),
|
|
94
|
+
indicatorSeparator: (styles) => ({
|
|
95
|
+
...styles,
|
|
96
|
+
width: null,
|
|
97
|
+
}),
|
|
98
|
+
valueContainer: (styles) => ({
|
|
99
|
+
...styles,
|
|
100
|
+
padding: 0,
|
|
101
|
+
}),
|
|
102
|
+
option: (styles, state) => ({
|
|
103
|
+
...styles,
|
|
104
|
+
backgroundColor: null,
|
|
105
|
+
minHeight: '50px',
|
|
106
|
+
display: 'flex',
|
|
107
|
+
justifyContent: 'space-between',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
padding: '0.5em 0.8em',
|
|
110
|
+
color: state.isSelected
|
|
111
|
+
? '#007bc1'
|
|
112
|
+
: state.isFocused
|
|
113
|
+
? '#4a4a4a'
|
|
114
|
+
: 'inherit',
|
|
115
|
+
':active': {
|
|
116
|
+
backgroundColor: null,
|
|
117
|
+
},
|
|
118
|
+
span: {
|
|
119
|
+
flex: '0 0 auto',
|
|
120
|
+
},
|
|
121
|
+
svg: {
|
|
122
|
+
flex: '0 0 auto',
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* getGeonames function.
|
|
129
|
+
* @function getGeonames
|
|
130
|
+
* @param {url} url URL.
|
|
131
|
+
* @returns {Object} Object.
|
|
132
|
+
*/
|
|
133
|
+
export function getWorkflowProgress(item) {
|
|
134
|
+
return {
|
|
135
|
+
type: 'WORKFLOW_PROGRESS_PATH',
|
|
136
|
+
item,
|
|
137
|
+
request: {
|
|
138
|
+
op: 'get',
|
|
139
|
+
path: `${item}/@workflow.progress.nis`,
|
|
140
|
+
headers: {
|
|
141
|
+
Accept: 'application/json',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const itemTracker = (tracker, currentStateKey, currentState) => {
|
|
148
|
+
const tracker_key_array = tracker[0];
|
|
149
|
+
const is_active = tracker_key_array.indexOf(currentStateKey) > -1;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<li
|
|
153
|
+
key={`progress__item ${tracker_key_array}`}
|
|
154
|
+
className={`progress__item ${
|
|
155
|
+
is_active
|
|
156
|
+
? 'progress__item--active'
|
|
157
|
+
: tracker[1] < currentState.done
|
|
158
|
+
? 'progress__item--completed'
|
|
159
|
+
: 'progress__item--next'
|
|
160
|
+
}`}
|
|
161
|
+
>
|
|
162
|
+
{tracker[2].map((title, index) => (
|
|
163
|
+
<div
|
|
164
|
+
key={`progress__title ${tracker_key_array}${index}`}
|
|
165
|
+
className={`progress__title ${
|
|
166
|
+
currentState.title !== title ? 'title-incomplete' : ''
|
|
167
|
+
}`}
|
|
168
|
+
>
|
|
169
|
+
{title}
|
|
170
|
+
{is_active && <div name="active-workflow-progress" />}
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</li>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @summary The React component that shows progress tracking of selected content.
|
|
179
|
+
*/
|
|
180
|
+
const ProgressWorkflow = (props) => {
|
|
181
|
+
const { content, pathname, token } = props;
|
|
182
|
+
// const Select = props.reactSelect.default;
|
|
183
|
+
const intl = useIntl();
|
|
184
|
+
const isAuth = !!token;
|
|
185
|
+
// const currentStateKey = content?.review_state;
|
|
186
|
+
const dispatch = useDispatch();
|
|
187
|
+
const contentId = content?.['@id'];
|
|
188
|
+
const basePathname = getBaseUrl(pathname);
|
|
189
|
+
const contentContainsPathname =
|
|
190
|
+
contentId &&
|
|
191
|
+
basePathname &&
|
|
192
|
+
flattenToAppURL(contentId).endsWith(basePathname);
|
|
193
|
+
const fetchCondition =
|
|
194
|
+
pathname.endsWith('/contents') ||
|
|
195
|
+
pathname.endsWith('/edit') ||
|
|
196
|
+
pathname === basePathname;
|
|
197
|
+
// console.log(basePathname);
|
|
198
|
+
const [workflowProgressSteps, setWorkflowProgressSteps] = useState([]);
|
|
199
|
+
const [currentState, setCurrentState] = useState(null);
|
|
200
|
+
const [currentStateKey, setCurrentStateKey] = useState(content?.review_state);
|
|
201
|
+
|
|
202
|
+
const transition = (selectedOption) => {
|
|
203
|
+
// console.log('selectedOption: ', selectedOption);
|
|
204
|
+
dispatch(transitionWorkflow(flattenToAppURL(selectedOption.url))).then(
|
|
205
|
+
() => {
|
|
206
|
+
toast.success(
|
|
207
|
+
<Toast
|
|
208
|
+
success
|
|
209
|
+
title={intl.formatMessage(messages.messageUpdated)}
|
|
210
|
+
content=""
|
|
211
|
+
/>,
|
|
212
|
+
);
|
|
213
|
+
setCurrentStateKey(selectedOption.new_state_id);
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const workflowProgressPath = useSelector((state) => {
|
|
219
|
+
if (state?.workflowProgressPath?.[basePathname]?.get?.loaded === true) {
|
|
220
|
+
const progress = state?.workflowProgressPath?.[basePathname]?.result;
|
|
221
|
+
if (
|
|
222
|
+
progress &&
|
|
223
|
+
flattenToAppURL(progress['@id']).endsWith(
|
|
224
|
+
basePathname + '/@workflow.progress.nis',
|
|
225
|
+
)
|
|
226
|
+
) {
|
|
227
|
+
return state?.workflowProgressPath?.[basePathname];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
});
|
|
232
|
+
const pusherRef = useRef(null);
|
|
233
|
+
const transitions = workflowProgressPath?.result?.transitions || [];
|
|
234
|
+
|
|
235
|
+
// set visible by clicking oustisde
|
|
236
|
+
const hideVisibleSide = () => {
|
|
237
|
+
pusherRef.current &&
|
|
238
|
+
pusherRef.current.lastElementChild.classList.add('is-hidden');
|
|
239
|
+
};
|
|
240
|
+
// toggle visible by clicking on the button
|
|
241
|
+
const toggleVisibleSide = (event) => {
|
|
242
|
+
// pusherRef.current &&
|
|
243
|
+
// pusherRef.current.lastElementChild.classList.toggle('is-hidden');
|
|
244
|
+
const button = event.currentTarget;
|
|
245
|
+
const dropdown = pusherRef.current?.lastElementChild;
|
|
246
|
+
|
|
247
|
+
if (!dropdown) return;
|
|
248
|
+
|
|
249
|
+
const rect = button.getBoundingClientRect();
|
|
250
|
+
|
|
251
|
+
dropdown.style.position = 'fixed';
|
|
252
|
+
dropdown.style.top = `${rect.bottom - 35}px`; // or rect.top
|
|
253
|
+
dropdown.style.left = `${rect.left - 200}px`;
|
|
254
|
+
dropdown.classList.toggle('is-hidden');
|
|
255
|
+
// console.log(rect.bottom, rect.left);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// apply all computing when the workflowProgress results come from the api
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
const findCurrentState = (steps, done) => {
|
|
261
|
+
const arrayContainingCurrentState = steps.find(
|
|
262
|
+
(itemElements) => itemElements[1] === done,
|
|
263
|
+
);
|
|
264
|
+
const indexOfCurrentStateKey =
|
|
265
|
+
arrayContainingCurrentState[0].indexOf(currentStateKey);
|
|
266
|
+
const title = arrayContainingCurrentState[2][indexOfCurrentStateKey];
|
|
267
|
+
const description =
|
|
268
|
+
arrayContainingCurrentState[3][indexOfCurrentStateKey];
|
|
269
|
+
|
|
270
|
+
setCurrentState({
|
|
271
|
+
done,
|
|
272
|
+
title,
|
|
273
|
+
description,
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* remove states that are 0% unless if it is current state
|
|
279
|
+
* @param {Object[]} states - array of arrays
|
|
280
|
+
* @param {Object[]} states[0][0] - array of state keys (ex: [private, published])
|
|
281
|
+
* @param {number} states[0][1] - percent
|
|
282
|
+
* @param {Object[]} states[0][2] - array of state titles (ex: [Private, Published])
|
|
283
|
+
* @param {Object[]} states[0][3] - array of state descriptions
|
|
284
|
+
* @returns {Object[]} result - array of arrays, same structure but filtered
|
|
285
|
+
*/
|
|
286
|
+
const filterOutZeroStatesNotCurrent = (states) => {
|
|
287
|
+
return states; // do not filter
|
|
288
|
+
// const [firstState, ...rest] = states;
|
|
289
|
+
// const result =
|
|
290
|
+
// firstState[1] > 0 // there aren't any 0% states
|
|
291
|
+
// ? states // return all states
|
|
292
|
+
// : (() => {
|
|
293
|
+
// // there are 0% states
|
|
294
|
+
// const indexOfCurrentStateKey =
|
|
295
|
+
// firstState[0].indexOf(currentStateKey);
|
|
296
|
+
// if (indexOfCurrentStateKey > -1) {
|
|
297
|
+
// const keys = [firstState[0][indexOfCurrentStateKey]];
|
|
298
|
+
// const titles = [firstState[2][indexOfCurrentStateKey]];
|
|
299
|
+
// const description = [firstState[3][indexOfCurrentStateKey]];
|
|
300
|
+
|
|
301
|
+
// return [[keys, 0, titles, description], ...rest]; // return only the current 0% state and test
|
|
302
|
+
// }
|
|
303
|
+
// return rest; // if current state in not a 0% return all rest
|
|
304
|
+
// })();
|
|
305
|
+
|
|
306
|
+
// return result;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// filter out paths that don't have workflow (home, login, dexterity even if the content obj stays the same etc)
|
|
310
|
+
if (
|
|
311
|
+
contentId &&
|
|
312
|
+
contentContainsPathname &&
|
|
313
|
+
basePathname &&
|
|
314
|
+
basePathname !== '/' && // wihout this there will be a flicker for going back to home ('/' is included in all api paths)
|
|
315
|
+
workflowProgressPath?.result?.steps &&
|
|
316
|
+
workflowProgressPath.result.steps.length > 0 &&
|
|
317
|
+
!workflowProgressPath.get?.error &&
|
|
318
|
+
Array.isArray(workflowProgressPath?.result?.steps)
|
|
319
|
+
) {
|
|
320
|
+
findCurrentState(
|
|
321
|
+
workflowProgressPath.result.steps,
|
|
322
|
+
workflowProgressPath.result.done,
|
|
323
|
+
);
|
|
324
|
+
setWorkflowProgressSteps(
|
|
325
|
+
filterOutZeroStatesNotCurrent(
|
|
326
|
+
workflowProgressPath.result.steps,
|
|
327
|
+
).reverse(),
|
|
328
|
+
);
|
|
329
|
+
} else {
|
|
330
|
+
if (currentState) {
|
|
331
|
+
setCurrentState(null); // reset current state only if a path without workflow is
|
|
332
|
+
// chosen to avoid flicker for those that have workflow
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}, [workflowProgressPath?.result, currentStateKey, pathname]); // eslint-disable-line
|
|
336
|
+
|
|
337
|
+
// get progress again if path or content changes
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (token && fetchCondition && contentContainsPathname) {
|
|
340
|
+
dispatch(getWorkflowProgress(basePathname));
|
|
341
|
+
} // the are paths that don't have workflow (home, login etc) only if logged in
|
|
342
|
+
}, [
|
|
343
|
+
dispatch,
|
|
344
|
+
pathname,
|
|
345
|
+
basePathname,
|
|
346
|
+
token,
|
|
347
|
+
currentStateKey,
|
|
348
|
+
contentContainsPathname,
|
|
349
|
+
fetchCondition,
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
// on mount subscribe to mousedown to be able to close on click outside
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
const handleClickOutside = (e) => {
|
|
355
|
+
const parentDiv = pusherRef.current;
|
|
356
|
+
if (parentDiv) {
|
|
357
|
+
if (
|
|
358
|
+
!doesNodeContainClick(parentDiv, e) &&
|
|
359
|
+
!parentDiv.lastElementChild.classList.contains('is-hidden')
|
|
360
|
+
) {
|
|
361
|
+
hideVisibleSide();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
document.addEventListener('mousedown', handleClickOutside, false);
|
|
367
|
+
|
|
368
|
+
return () => {
|
|
369
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
370
|
+
};
|
|
371
|
+
}, []);
|
|
372
|
+
|
|
373
|
+
// console.log('currentState: ', currentState);
|
|
374
|
+
// console.log('currentStateKey:', currentStateKey);
|
|
375
|
+
// console.log(workflowProgressPath?.result?.transitions);
|
|
376
|
+
return isAuth && currentState && contentContainsPathname ? (
|
|
377
|
+
<>
|
|
378
|
+
<div className="toolbar-workflow-progress">
|
|
379
|
+
<div ref={pusherRef}>
|
|
380
|
+
<button
|
|
381
|
+
className={`circle-right-btn ${
|
|
382
|
+
currentStateClass[currentStateKey]
|
|
383
|
+
? `review-state-${currentStateKey}`
|
|
384
|
+
: currentState.done === 100
|
|
385
|
+
? 'review-state-published'
|
|
386
|
+
: ''
|
|
387
|
+
}`}
|
|
388
|
+
id="toolbar-cut-blocks"
|
|
389
|
+
onClick={toggleVisibleSide}
|
|
390
|
+
title="Editing progress"
|
|
391
|
+
>
|
|
392
|
+
{`${currentState.done}%`}
|
|
393
|
+
</button>
|
|
394
|
+
<div className={`sidenav-ol sidenav-ol--wp is-hidden`}>
|
|
395
|
+
<div className="workflow-select">
|
|
396
|
+
<Select
|
|
397
|
+
// menuIsOpen={true}
|
|
398
|
+
name="state-select"
|
|
399
|
+
className="react-select-container"
|
|
400
|
+
classNamePrefix="react-select"
|
|
401
|
+
isDisabled={!content.review_state || transitions.length === 0}
|
|
402
|
+
options={uniqBy(
|
|
403
|
+
transitions.map((transition) =>
|
|
404
|
+
getWorkflowOptions(transition),
|
|
405
|
+
),
|
|
406
|
+
'label',
|
|
407
|
+
).concat({ value: currentStateKey, label: currentState.title })}
|
|
408
|
+
styles={customSelectStyles}
|
|
409
|
+
theme={selectTheme}
|
|
410
|
+
// components={{
|
|
411
|
+
// DropdownIndicator,
|
|
412
|
+
// Placeholder,
|
|
413
|
+
// Option,
|
|
414
|
+
// SingleValue,
|
|
415
|
+
// }}
|
|
416
|
+
onChange={transition}
|
|
417
|
+
value={{ value: currentStateKey, label: currentState.title }}
|
|
418
|
+
isSearchable={false}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
<ol
|
|
422
|
+
className="progress-reversed"
|
|
423
|
+
style={{
|
|
424
|
+
counterReset: `item ${workflowProgressSteps.length + 1}`,
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
{workflowProgressSteps.map((progressItem) =>
|
|
428
|
+
itemTracker(progressItem, currentStateKey, currentState),
|
|
429
|
+
)}
|
|
430
|
+
</ol>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div
|
|
434
|
+
className={`review-state-text ${
|
|
435
|
+
currentStateClass[currentStateKey]
|
|
436
|
+
? `review-state-${currentStateKey}`
|
|
437
|
+
: currentState.done === 100
|
|
438
|
+
? 'review-state-published'
|
|
439
|
+
: ''
|
|
440
|
+
}`}
|
|
441
|
+
id="toolbar-cut-blocks"
|
|
442
|
+
onClick={toggleVisibleSide}
|
|
443
|
+
onKeyDown={() => {}}
|
|
444
|
+
title="Editing progress"
|
|
445
|
+
role="presentation"
|
|
446
|
+
>
|
|
447
|
+
{`${currentState.title}`}
|
|
448
|
+
</div>
|
|
449
|
+
<div className={`sidenav-ol sidenav-ol--wp is-hidden`}>
|
|
450
|
+
<ol
|
|
451
|
+
className="progress-reversed"
|
|
452
|
+
style={{
|
|
453
|
+
counterReset: `item ${workflowProgressSteps.length + 1}`,
|
|
454
|
+
}}
|
|
455
|
+
>
|
|
456
|
+
{workflowProgressSteps.map((progressItem) =>
|
|
457
|
+
itemTracker(progressItem, currentStateKey, currentState),
|
|
458
|
+
)}
|
|
459
|
+
</ol>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</>
|
|
463
|
+
) : (
|
|
464
|
+
// </Plug>
|
|
465
|
+
''
|
|
466
|
+
);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
ProgressWorkflow.propTypes = {
|
|
470
|
+
pathname: PropTypes.string.isRequired,
|
|
471
|
+
content: PropTypes.object,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export default ProgressWorkflow;
|