@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.
@@ -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;