@strapi/admin 4.13.6 → 4.14.0-beta.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.
Files changed (73) hide show
  1. package/admin/src/content-manager/components/Wysiwyg/EditorLayout.js +1 -1
  2. package/admin/src/content-manager/pages/ListView/components/BulkActionButtons/ConfirmBulkActionDialog/index.js +3 -0
  3. package/admin/src/content-manager/pages/ListView/components/BulkActionButtons/SelectedEntriesModal/index.js +2 -1
  4. package/admin/src/hooks/useAdminRoles/index.js +17 -7
  5. package/admin/src/hooks/useAdminUsers/useAdminUsers.js +16 -7
  6. package/admin/src/hooks/useContentTypes/useContentTypes.js +18 -7
  7. package/admin/src/index.js +1 -7
  8. package/build/1227.4f48119b.chunk.js +1 -0
  9. package/build/{3483.19381b40.chunk.js → 3483.f6b2439f.chunk.js} +1 -1
  10. package/build/4174.924ebd4c.chunk.js +1 -0
  11. package/build/6266.c652bdb1.chunk.js +146 -0
  12. package/build/7897.cf22d5fe.chunk.js +6 -0
  13. package/build/{Admin-authenticatedApp.69c7ea72.chunk.js → Admin-authenticatedApp.a687d9c6.chunk.js} +1 -1
  14. package/build/{admin-app.1fde8f7a.chunk.js → admin-app.4654dc77.chunk.js} +11 -11
  15. package/build/admin-edit-roles-page.6597d934.chunk.js +267 -0
  16. package/build/admin-edit-users.3014605e.chunk.js +10 -0
  17. package/build/admin-roles-list.ab6fcfb7.chunk.js +22 -0
  18. package/build/admin-users.81bf5f4d.chunk.js +11 -0
  19. package/build/audit-logs-settings-page.4eb6cdf8.chunk.js +1 -0
  20. package/build/{content-manager.f9abb63f.chunk.js → content-manager.9187db78.chunk.js} +37 -37
  21. package/build/index.html +1 -1
  22. package/build/{main.9ed36326.js → main.da000219.js} +17 -17
  23. package/build/review-workflows-settings-create-view.5d8806b2.chunk.js +1 -0
  24. package/build/review-workflows-settings-edit-view.634903ed.chunk.js +1 -0
  25. package/build/review-workflows-settings-list-view.d138c3b5.chunk.js +56 -0
  26. package/build/{runtime~main.562cc892.js → runtime~main.9589b498.js} +2 -2
  27. package/build/sso-settings-page.caa35f7b.chunk.js +1 -0
  28. package/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js +65 -53
  29. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +50 -5
  30. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +257 -21
  31. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js +8 -23
  32. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
  33. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +17 -7
  34. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages.js +36 -0
  35. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +68 -19
  36. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +105 -35
  37. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +68 -27
  38. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/selectors.js +45 -0
  39. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/validateWorkflow.js +20 -0
  40. package/ee/server/config/admin-actions.js +6 -0
  41. package/ee/server/constants/workflows.js +13 -0
  42. package/ee/server/content-types/workflow-stage/index.js +6 -0
  43. package/ee/server/controllers/workflows/index.js +41 -16
  44. package/ee/server/controllers/workflows/stages/index.js +93 -6
  45. package/ee/server/migrations/review-workflows-stages-roles.js +68 -0
  46. package/ee/server/migrations/review-workflows-workflow-name.js +7 -0
  47. package/ee/server/register.js +2 -0
  48. package/ee/server/routes/review-workflows.js +10 -9
  49. package/ee/server/services/index.js +1 -0
  50. package/ee/server/services/review-workflows/stage-permissions.js +60 -0
  51. package/ee/server/services/review-workflows/stages.js +91 -12
  52. package/ee/server/services/review-workflows/workflows/index.js +20 -7
  53. package/ee/server/validation/review-workflows.js +11 -0
  54. package/package.json +8 -8
  55. package/server/content-types/Permission.js +6 -0
  56. package/server/domain/permission/index.js +11 -2
  57. package/server/services/role.js +12 -4
  58. package/server/validation/action-provider.js +1 -1
  59. package/server/validation/common-validators.js +92 -100
  60. package/server/validation/permission.js +0 -3
  61. package/build/1227.c72e74e9.chunk.js +0 -1
  62. package/build/2237.a7992513.chunk.js +0 -114
  63. package/build/4174.6efb0dc6.chunk.js +0 -1
  64. package/build/4724.a0ce68f3.chunk.js +0 -6
  65. package/build/admin-edit-roles-page.f76cb0aa.chunk.js +0 -267
  66. package/build/admin-edit-users.186c9d90.chunk.js +0 -10
  67. package/build/admin-roles-list.3a4edfa2.chunk.js +0 -22
  68. package/build/admin-users.e276bd9a.chunk.js +0 -11
  69. package/build/audit-logs-settings-page.0d9bc0f6.chunk.js +0 -1
  70. package/build/review-workflows-settings-create-view.b96b7be1.chunk.js +0 -1
  71. package/build/review-workflows-settings-edit-view.21a682a0.chunk.js +0 -1
  72. package/build/review-workflows-settings-list-view.14212324.chunk.js +0 -56
  73. package/build/sso-settings-page.d93bd81f.chunk.js +0 -1
@@ -9,25 +9,91 @@ import {
9
9
  Grid,
10
10
  GridItem,
11
11
  IconButton,
12
+ MultiSelect,
13
+ MultiSelectGroup,
14
+ MultiSelectOption,
12
15
  SingleSelect,
13
16
  SingleSelectOption,
14
17
  TextInput,
18
+ Typography,
15
19
  VisuallyHidden,
16
20
  } from '@strapi/design-system';
17
- import { useTracking } from '@strapi/helper-plugin';
18
- import { Drag, Trash } from '@strapi/icons';
21
+ import { Menu, MenuItem } from '@strapi/design-system/v2';
22
+ import {
23
+ ConfirmDialog,
24
+ useNotification,
25
+ NotAllowedInput,
26
+ useTracking,
27
+ } from '@strapi/helper-plugin';
28
+ import { Duplicate, Drag, More } from '@strapi/icons';
19
29
  import { useField } from 'formik';
20
30
  import PropTypes from 'prop-types';
21
31
  import { getEmptyImage } from 'react-dnd-html5-backend';
22
32
  import { useIntl } from 'react-intl';
23
- import { useDispatch } from 'react-redux';
33
+ import { useDispatch, useSelector } from 'react-redux';
34
+ import styled from 'styled-components';
24
35
 
25
36
  import { useDragAndDrop } from '../../../../../../../../../admin/src/content-manager/hooks';
26
37
  import { composeRefs } from '../../../../../../../../../admin/src/content-manager/utils';
27
- import { deleteStage, updateStage, updateStagePosition } from '../../../actions';
38
+ import {
39
+ cloneStage,
40
+ deleteStage,
41
+ updateStage,
42
+ updateStagePosition,
43
+ updateStages,
44
+ } from '../../../actions';
28
45
  import { DRAG_DROP_TYPES } from '../../../constants';
46
+ import { selectRoles } from '../../../selectors';
29
47
  import { getAvailableStageColors, getStageColorByHex } from '../../../utils/colors';
30
48
 
49
+ const NestedOption = styled(MultiSelectOption)`
50
+ padding-left: ${({ theme }) => theme.spaces[7]};
51
+ `;
52
+
53
+ // Grow the size of the permission Select
54
+ const PermissionWrapper = styled(Flex)`
55
+ > * {
56
+ flex-grow: 1;
57
+ }
58
+ `;
59
+
60
+ const DeleteMenuItem = styled(MenuItem)`
61
+ color: ${({ theme }) => theme.colors.danger600};
62
+ `;
63
+
64
+ // Removing the font-size from the child-span aligns the
65
+ // more icon vertically
66
+ const ContextMenuTrigger = styled(Menu.Trigger)`
67
+ :hover,
68
+ :focus {
69
+ background-color: ${({ theme }) => theme.colors.neutral100};
70
+ }
71
+
72
+ > span {
73
+ font-size: 0;
74
+ }
75
+ `;
76
+
77
+ // As soon as either `as` or `forwardedAs` is set, the component
78
+ // resets some styles and e.g. the `hasBorder` prop no longer works,
79
+ // which is why this bit of CSS has been added manually ¯\_(ツ)_/¯
80
+ const DragIconButton = styled(IconButton)`
81
+ align-items: center;
82
+ border-radius: ${({ theme }) => theme.borderRadius};
83
+ display: flex;
84
+ justify-content: center;
85
+
86
+ :hover,
87
+ :focus {
88
+ background-color: ${({ theme }) => theme.colors.neutral100};
89
+ }
90
+
91
+ svg {
92
+ height: auto;
93
+ width: ${({ theme }) => theme.spaces[3]}};
94
+ }
95
+ `;
96
+
31
97
  const AVAILABLE_COLORS = getAvailableStageColors();
32
98
 
33
99
  function StageDropPreview() {
@@ -137,13 +203,23 @@ export function Stage({
137
203
  dispatch(updateStagePosition(oldIndex, newIndex));
138
204
  };
139
205
 
206
+ const handleApplyPermissionsToAllStages = () => {
207
+ setIsApplyAllConfirmationOpen(true);
208
+ };
209
+
140
210
  const [liveText, setLiveText] = React.useState(null);
141
211
  const { formatMessage } = useIntl();
142
212
  const { trackUsage } = useTracking();
143
213
  const dispatch = useDispatch();
214
+ const toggleNotification = useNotification();
144
215
  const [isOpen, setIsOpen] = React.useState(isOpenDefault);
216
+ const [isApplyAllConfirmationOpen, setIsApplyAllConfirmationOpen] = React.useState(false);
145
217
  const [nameField, nameMeta, nameHelper] = useField(`stages.${index}.name`);
146
218
  const [colorField, colorMeta, colorHelper] = useField(`stages.${index}.color`);
219
+ const [permissionsField, permissionsMeta, permissionsHelper] = useField(
220
+ `stages.${index}.permissions`
221
+ );
222
+ const roles = useSelector(selectRoles);
147
223
  const [{ handlerId, isDragging, handleKeyDown }, stageRef, dropRef, dragRef, dragPreviewRef] =
148
224
  useDragAndDrop(canReorder, {
149
225
  index,
@@ -171,12 +247,17 @@ export function Stage({
171
247
  color: hex,
172
248
  }));
173
249
 
250
+ const { themeColorName } = getStageColorByHex(colorField.value) ?? {};
251
+
252
+ const filteredRoles = roles
253
+ // Super admins always have permissions to do everything and therefore
254
+ // there is no point for this role to show up in the role combobox
255
+ .filter((role) => role.code !== 'strapi-super-admin');
256
+
174
257
  React.useEffect(() => {
175
258
  dragPreviewRef(getEmptyImage(), { captureDraggingState: false });
176
259
  }, [dragPreviewRef, index]);
177
260
 
178
- const { themeColorName } = getStageColorByHex(colorField.value) ?? {};
179
-
180
261
  return (
181
262
  <Box ref={composedRef}>
182
263
  {liveText && <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>}
@@ -196,7 +277,7 @@ export function Stage({
196
277
  }}
197
278
  expanded={isOpen}
198
279
  shadow="tableShadow"
199
- error={nameMeta.error ?? colorMeta?.error ?? false}
280
+ error={nameMeta.error ?? colorMeta?.error ?? permissionsMeta?.error ?? false}
200
281
  hasErrorMessage={false}
201
282
  >
202
283
  <AccordionToggle
@@ -205,23 +286,47 @@ export function Stage({
205
286
  action={
206
287
  (canDelete || canUpdate) && (
207
288
  <Flex>
208
- {canDelete && (
209
- <IconButton
210
- background="transparent"
211
- icon={<Trash />}
212
- label={formatMessage({
213
- id: 'Settings.review-workflows.stage.delete',
214
- defaultMessage: 'Delete stage',
215
- })}
216
- noBorder
217
- onClick={() => dispatch(deleteStage(id))}
218
- />
219
- )}
289
+ <Menu.Root>
290
+ <ContextMenuTrigger size="S" endIcon={null} paddingLeft={2} paddingRight={2}>
291
+ <More aria-hidden focusable={false} />
292
+ <VisuallyHidden as="span">
293
+ {formatMessage({
294
+ id: '[tbdb].components.DynamicZone.more-actions',
295
+ defaultMessage: 'More actions',
296
+ })}
297
+ </VisuallyHidden>
298
+ </ContextMenuTrigger>
299
+ {/* z-index needs to be as big as the one defined for the wrapper in Stages, otherwise the menu
300
+ * disappears behind the accordion
301
+ */}
302
+ <Menu.Content popoverPlacement="bottom-end" zIndex={2}>
303
+ <Menu.SubRoot>
304
+ {canUpdate && (
305
+ <MenuItem onClick={() => dispatch(cloneStage(id))}>
306
+ {formatMessage({
307
+ id: 'Settings.review-workflows.stage.delete',
308
+ defaultMessage: 'Duplicate stage',
309
+ })}
310
+ </MenuItem>
311
+ )}
312
+
313
+ {canDelete && (
314
+ <DeleteMenuItem onClick={() => dispatch(deleteStage(id))}>
315
+ {formatMessage({
316
+ id: 'Settings.review-workflows.stage.delete',
317
+ defaultMessage: 'Delete',
318
+ })}
319
+ </DeleteMenuItem>
320
+ )}
321
+ </Menu.SubRoot>
322
+ </Menu.Content>
323
+ </Menu.Root>
220
324
 
221
325
  {canUpdate && (
222
- <IconButton
326
+ <DragIconButton
223
327
  background="transparent"
224
328
  forwardedAs="div"
329
+ hasRadius
225
330
  role="button"
226
331
  noBorder
227
332
  tabIndex={0}
@@ -235,7 +340,7 @@ export function Stage({
235
340
  onKeyDown={handleKeyDown}
236
341
  >
237
342
  <Drag />
238
- </IconButton>
343
+ </DragIconButton>
239
344
  )}
240
345
  </Flex>
241
346
  )
@@ -315,10 +420,141 @@ export function Stage({
315
420
  })}
316
421
  </SingleSelect>
317
422
  </GridItem>
423
+
424
+ <GridItem col={6}>
425
+ {filteredRoles.length === 0 ? (
426
+ <NotAllowedInput
427
+ description={{
428
+ id: 'Settings.review-workflows.stage.permissions.noPermissions.description',
429
+ defaultMessage: 'You don’t have the permission to see roles',
430
+ }}
431
+ intlLabel={{
432
+ id: 'Settings.review-workflows.stage.permissions.label',
433
+ defaultMessage: 'Roles that can change this stage',
434
+ }}
435
+ name={permissionsField.name}
436
+ />
437
+ ) : (
438
+ <Flex alignItems="flex-end" gap={3}>
439
+ <PermissionWrapper grow={1}>
440
+ <MultiSelect
441
+ {...permissionsField}
442
+ disabled={!canUpdate}
443
+ error={permissionsMeta.error ?? false}
444
+ id={permissionsField.name}
445
+ label={formatMessage({
446
+ id: 'Settings.review-workflows.stage.permissions.label',
447
+ defaultMessage: 'Roles that can change this stage',
448
+ })}
449
+ onChange={(values) => {
450
+ // Because the select components expects strings for values, but
451
+ // the yup schema validates we are sending full permission objects to the API,
452
+ // we must coerce the string value back to an object
453
+ const permissions = values.map((value) => ({
454
+ role: parseInt(value, 10),
455
+ action: 'admin::review-workflows.stage.transition',
456
+ }));
457
+
458
+ permissionsHelper.setValue(permissions);
459
+ dispatch(updateStage(id, { permissions }));
460
+ }}
461
+ placeholder={formatMessage({
462
+ id: 'Settings.review-workflows.stage.permissions.placeholder',
463
+ defaultMessage: 'Select a role',
464
+ })}
465
+ required
466
+ // The Select component expects strings for values
467
+ value={(permissionsField.value ?? []).map(
468
+ (permission) => `${permission.role}`
469
+ )}
470
+ withTags
471
+ >
472
+ {[
473
+ {
474
+ label: formatMessage({
475
+ id: 'Settings.review-workflows.stage.permissions.allRoles.label',
476
+ defaultMessage: 'All roles',
477
+ }),
478
+
479
+ children: filteredRoles.map((role) => ({
480
+ value: `${role.id}`,
481
+ label: role.name,
482
+ })),
483
+ },
484
+ ].map((role) => {
485
+ if ('children' in role) {
486
+ return (
487
+ <MultiSelectGroup
488
+ key={role.label}
489
+ label={role.label}
490
+ values={role.children.map((child) => child.value)}
491
+ >
492
+ {role.children.map((role) => {
493
+ return (
494
+ <NestedOption key={role.value} value={role.value}>
495
+ {role.label}
496
+ </NestedOption>
497
+ );
498
+ })}
499
+ </MultiSelectGroup>
500
+ );
501
+ }
502
+
503
+ return (
504
+ <MultiSelectOption key={role.value} value={role.value}>
505
+ {role.label}
506
+ </MultiSelectOption>
507
+ );
508
+ })}
509
+ </MultiSelect>
510
+ </PermissionWrapper>
511
+
512
+ <IconButton
513
+ disabled={!canUpdate}
514
+ icon={<Duplicate />}
515
+ label={formatMessage({
516
+ id: 'Settings.review-workflows.stage.permissions.apply.label',
517
+ defaultMessage: 'Apply to all stages',
518
+ })}
519
+ size="L"
520
+ variant="secondary"
521
+ onClick={() => handleApplyPermissionsToAllStages(permissionsField.value)}
522
+ />
523
+ </Flex>
524
+ )}
525
+ </GridItem>
318
526
  </Grid>
319
527
  </AccordionContent>
320
528
  </Accordion>
321
529
  )}
530
+
531
+ <ConfirmDialog.Root
532
+ iconRightButton={null}
533
+ isOpen={isApplyAllConfirmationOpen}
534
+ onToggleDialog={() => setIsApplyAllConfirmationOpen(false)}
535
+ onConfirm={() => {
536
+ dispatch(updateStages({ permissions: permissionsField.value }));
537
+ setIsApplyAllConfirmationOpen(false);
538
+ toggleNotification({
539
+ type: 'success',
540
+ message: formatMessage({
541
+ id: 'Settings.review-workflows.page.edit.confirm.stages.permissions.copy.success',
542
+ defaultMessage: 'Applied roles to all other stages of the workflow',
543
+ }),
544
+ });
545
+ }}
546
+ variantRightButton="primary"
547
+ >
548
+ <ConfirmDialog.Body>
549
+ <Typography textAlign="center" variant="omega">
550
+ {formatMessage({
551
+ id: 'Settings.review-workflows.page.edit.confirm.stages.permissions.copy',
552
+ defaultMessage:
553
+ 'Roles that can change that stage will be applied to all the other stages.',
554
+ })}
555
+ </Typography>
556
+ </ConfirmDialog.Body>
557
+ </ConfirmDialog.Root>
322
558
  </Box>
323
559
  );
324
560
  }
@@ -13,10 +13,11 @@ import { useCollator } from '@strapi/helper-plugin';
13
13
  import { useField } from 'formik';
14
14
  import PropTypes from 'prop-types';
15
15
  import { useIntl } from 'react-intl';
16
- import { useDispatch } from 'react-redux';
16
+ import { useDispatch, useSelector } from 'react-redux';
17
17
  import styled from 'styled-components';
18
18
 
19
19
  import { updateWorkflow } from '../../actions';
20
+ import { selectContentTypes, selectCurrentWorkflow, selectWorkflows } from '../../selectors';
20
21
 
21
22
  const NestedOption = styled(MultiSelectOption)`
22
23
  padding-left: ${({ theme }) => theme.spaces[7]};
@@ -26,14 +27,12 @@ const ContentTypeTakeNotice = styled(Typography)`
26
27
  font-style: italic;
27
28
  `;
28
29
 
29
- export function WorkflowAttributes({
30
- canUpdate,
31
- contentTypes: { collectionTypes, singleTypes },
32
- currentWorkflow,
33
- workflows,
34
- }) {
30
+ export function WorkflowAttributes({ canUpdate }) {
35
31
  const { formatMessage, locale } = useIntl();
36
32
  const dispatch = useDispatch();
33
+ const { collectionTypes, singleTypes } = useSelector(selectContentTypes);
34
+ const currentWorkflow = useSelector(selectCurrentWorkflow);
35
+ const workflows = useSelector(selectWorkflows);
37
36
  const [nameField, nameMeta, nameHelper] = useField('name');
38
37
  const [contentTypesField, contentTypesMeta, contentTypesHelper] = useField('contentTypes');
39
38
  const formatter = useCollator(locale, {
@@ -97,7 +96,7 @@ export function WorkflowAttributes({
97
96
  id: 'Settings.review-workflows.workflow.contentTypes.collectionTypes.label',
98
97
  defaultMessage: 'Collection Types',
99
98
  }),
100
- children: collectionTypes
99
+ children: [...collectionTypes]
101
100
  .sort((a, b) => formatter.compare(a.info.displayName, b.info.displayName))
102
101
  .map((contentType) => ({
103
102
  label: contentType.info.displayName,
@@ -114,7 +113,7 @@ export function WorkflowAttributes({
114
113
  id: 'Settings.review-workflows.workflow.contentTypes.singleTypes.label',
115
114
  defaultMessage: 'Single Types',
116
115
  }),
117
- children: singleTypes.map((contentType) => ({
116
+ children: [...singleTypes].map((contentType) => ({
118
117
  label: contentType.info.displayName,
119
118
  value: contentType.uid,
120
119
  })),
@@ -178,24 +177,10 @@ export function WorkflowAttributes({
178
177
  );
179
178
  }
180
179
 
181
- const ContentTypeType = PropTypes.shape({
182
- uid: PropTypes.string.isRequired,
183
- info: PropTypes.shape({
184
- displayName: PropTypes.string.isRequired,
185
- }).isRequired,
186
- });
187
-
188
180
  WorkflowAttributes.defaultProps = {
189
181
  canUpdate: true,
190
- currentWorkflow: undefined,
191
182
  };
192
183
 
193
184
  WorkflowAttributes.propTypes = {
194
185
  canUpdate: PropTypes.bool,
195
- contentTypes: PropTypes.shape({
196
- collectionTypes: PropTypes.arrayOf(ContentTypeType).isRequired,
197
- singleTypes: PropTypes.arrayOf(ContentTypeType).isRequired,
198
- }).isRequired,
199
- currentWorkflow: PropTypes.object,
200
- workflows: PropTypes.array.isRequired,
201
186
  };
@@ -3,10 +3,16 @@ import { lightTheme } from '@strapi/design-system';
3
3
  export const REDUX_NAMESPACE = 'settings_review-workflows';
4
4
 
5
5
  export const ACTION_RESET_WORKFLOW = `Settings/Review_Workflows/RESET_WORKFLOW`;
6
+ export const ACTION_SET_CONTENT_TYPES = `Settings/Review_Workflows/SET_CONTENT_TYPES`;
7
+ export const ACTION_SET_IS_LOADING = `Settings/Review_Workflows/SET_IS_LOADING`;
8
+ export const ACTION_SET_ROLES = `Settings/Review_Workflows/SET_ROLES`;
6
9
  export const ACTION_SET_WORKFLOW = `Settings/Review_Workflows/SET_WORKFLOW`;
10
+ export const ACTION_SET_WORKFLOWS = `Settings/Review_Workflows/SET_WORKFLOWS`;
7
11
  export const ACTION_DELETE_STAGE = `Settings/Review_Workflows/WORKFLOW_DELETE_STAGE`;
8
12
  export const ACTION_ADD_STAGE = `Settings/Review_Workflows/WORKFLOW_ADD_STAGE`;
13
+ export const ACTION_CLONE_STAGE = `Settings/Review_Workflows/WORKFLOW_CLONE_STAGE`;
9
14
  export const ACTION_UPDATE_STAGE = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGE`;
15
+ export const ACTION_UPDATE_STAGES = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGES`;
10
16
  export const ACTION_UPDATE_STAGE_POSITION = `Settings/Review_Workflows/WORKFLOW_UPDATE_STAGE_POSITION`;
11
17
  export const ACTION_UPDATE_WORKFLOW = `Settings/Review_Workflows/WORKFLOW_UPDATE`;
12
18
 
@@ -1,3 +1,5 @@
1
+ import * as React from 'react';
2
+
1
3
  import { useFetchClient } from '@strapi/helper-plugin';
2
4
  import { useQuery } from 'react-query';
3
5
 
@@ -20,18 +22,26 @@ export function useReviewWorkflows(params = {}) {
20
22
  }
21
23
  );
22
24
 
23
- let workflows = [];
25
+ // the return value needs to be memoized, because intantiating
26
+ // an empty array as default value would lead to an unstable return
27
+ // value, which later on triggers infinite loops if used in the
28
+ // dependency arrays of other hooks
29
+
30
+ const workflows = React.useMemo(() => {
31
+ if (id && data?.data) {
32
+ return [data.data];
33
+ }
34
+ if (Array.isArray(data?.data)) {
35
+ return data.data;
36
+ }
24
37
 
25
- if (id && data?.data) {
26
- workflows = [data.data];
27
- } else if (Array.isArray(data?.data)) {
28
- workflows = data.data;
29
- }
38
+ return [];
39
+ }, [data?.data, id]);
30
40
 
31
41
  return {
32
42
  // meta contains e.g. the total of all workflows. we can not use
33
43
  // the pagination object here, because the list is not paginated.
34
- meta: data?.meta ?? {},
44
+ meta: React.useMemo(() => data?.meta ?? {}, [data?.meta]),
35
45
  workflows,
36
46
  isLoading,
37
47
  status,
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+
3
+ import { useFetchClient } from '@strapi/helper-plugin';
4
+ import { useQuery } from 'react-query';
5
+
6
+ export function useReviewWorkflowsStages({ id, layout } = {}, queryOptions = {}) {
7
+ const { kind, uid } = layout;
8
+ const slug = kind === 'collectionType' ? 'collection-types' : 'single-types';
9
+
10
+ const { get } = useFetchClient();
11
+
12
+ const { data, isLoading, refetch } = useQuery(
13
+ ['content-manager', slug, layout.uid, id, 'stages'],
14
+ async () => {
15
+ const { data } = await get(`/admin/content-manager/${slug}/${uid}/${id}/stages`);
16
+
17
+ return data;
18
+ },
19
+ queryOptions
20
+ );
21
+
22
+ // these return values need to be memoized, because the default value
23
+ // would lead to infinite rendering loops when used in a dependency array
24
+ // on an effect
25
+ const meta = React.useMemo(() => data?.meta ?? {}, [data?.meta]);
26
+ const stages = React.useMemo(() => data?.data ?? [], [data?.data]);
27
+
28
+ return {
29
+ // meta contains e.g. the total of all workflows. we can not use
30
+ // the pagination object here, because the list is not paginated.
31
+ meta,
32
+ stages,
33
+ isLoading,
34
+ refetch,
35
+ };
36
+ }