@strapi/admin 4.13.3 → 4.14.0-alpha.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 (72) hide show
  1. package/admin/src/hooks/useAdminRoles/index.js +17 -7
  2. package/admin/src/hooks/useAdminUsers/useAdminUsers.js +16 -7
  3. package/admin/src/hooks/useContentTypes/useContentTypes.js +18 -7
  4. package/build/1227.ec336799.chunk.js +1 -0
  5. package/build/{3483.19381b40.chunk.js → 3483.f6b2439f.chunk.js} +1 -1
  6. package/build/4174.4587c7f6.chunk.js +1 -0
  7. package/build/6266.53be9ea3.chunk.js +124 -0
  8. package/build/7897.eac204a4.chunk.js +6 -0
  9. package/build/{Admin-authenticatedApp.796792a8.chunk.js → Admin-authenticatedApp.d200a4ee.chunk.js} +1 -1
  10. package/build/{admin-app.2a8615ab.chunk.js → admin-app.582877a3.chunk.js} +11 -11
  11. package/build/admin-edit-roles-page.0aa65505.chunk.js +267 -0
  12. package/build/admin-edit-users.9215912a.chunk.js +10 -0
  13. package/build/admin-roles-list.824a50de.chunk.js +22 -0
  14. package/build/admin-users.f6b3c643.chunk.js +11 -0
  15. package/build/audit-logs-settings-page.be2cb4dd.chunk.js +1 -0
  16. package/build/{content-manager.f448efdf.chunk.js → content-manager.06a2f7ec.chunk.js} +39 -39
  17. package/build/index.html +1 -1
  18. package/build/review-workflows-settings-create-view.604cffa0.chunk.js +1 -0
  19. package/build/review-workflows-settings-edit-view.73c57f07.chunk.js +1 -0
  20. package/build/review-workflows-settings-list-view.7e300ecb.chunk.js +56 -0
  21. package/build/{runtime~main.d2b8d4a1.js → runtime~main.9de029f4.js} +2 -2
  22. package/build/sso-settings-page.94373f78.chunk.js +1 -0
  23. package/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js +65 -53
  24. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +50 -5
  25. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +227 -19
  26. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js +8 -23
  27. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
  28. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +17 -7
  29. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages.js +36 -0
  30. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +68 -19
  31. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +105 -35
  32. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +68 -27
  33. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/selectors.js +45 -0
  34. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/validateWorkflow.js +20 -0
  35. package/ee/server/config/admin-actions.js +6 -0
  36. package/ee/server/constants/workflows.js +13 -0
  37. package/ee/server/content-types/workflow-stage/index.js +6 -0
  38. package/ee/server/controllers/workflows/index.js +41 -16
  39. package/ee/server/controllers/workflows/stages/index.js +93 -6
  40. package/ee/server/routes/review-workflows.js +10 -9
  41. package/ee/server/services/index.js +1 -0
  42. package/ee/server/services/review-workflows/stage-permissions.js +60 -0
  43. package/ee/server/services/review-workflows/stages.js +83 -12
  44. package/ee/server/services/review-workflows/workflows/index.js +20 -7
  45. package/ee/server/validation/review-workflows.js +11 -0
  46. package/package.json +8 -8
  47. package/scripts/build.js +2 -3
  48. package/scripts/create-dev-plugins-file.js +2 -3
  49. package/server/content-types/Permission.js +6 -0
  50. package/server/domain/permission/index.js +11 -2
  51. package/server/services/role.js +12 -4
  52. package/server/validation/action-provider.js +1 -1
  53. package/server/validation/common-validators.js +92 -100
  54. package/server/validation/permission.js +0 -3
  55. package/utils/create-cache-dir.js +5 -102
  56. package/utils/plugins.js +217 -0
  57. package/webpack.config.js +2 -2
  58. package/build/1227.9f37e1dc.chunk.js +0 -1
  59. package/build/2237.b832ae6e.chunk.js +0 -114
  60. package/build/4174.f1f39e40.chunk.js +0 -1
  61. package/build/4724.aea5c8c1.chunk.js +0 -6
  62. package/build/admin-edit-roles-page.38a6c863.chunk.js +0 -267
  63. package/build/admin-edit-users.545fc882.chunk.js +0 -10
  64. package/build/admin-roles-list.1e2e814d.chunk.js +0 -22
  65. package/build/admin-users.b8ea5677.chunk.js +0 -11
  66. package/build/audit-logs-settings-page.96f9d608.chunk.js +0 -1
  67. package/build/review-workflows-settings-create-view.4a156a19.chunk.js +0 -1
  68. package/build/review-workflows-settings-edit-view.ce984d1f.chunk.js +0 -1
  69. package/build/review-workflows-settings-list-view.419b8deb.chunk.js +0 -56
  70. package/build/sso-settings-page.45153df5.chunk.js +0 -1
  71. package/utils/create-plugins-exclude-path.js +0 -20
  72. package/utils/get-plugins.js +0 -110
@@ -5,29 +5,69 @@ import {
5
5
  AccordionContent,
6
6
  AccordionToggle,
7
7
  Box,
8
+ Button,
8
9
  Flex,
9
10
  Grid,
10
11
  GridItem,
11
12
  IconButton,
13
+ MultiSelect,
14
+ MultiSelectGroup,
15
+ MultiSelectOption,
12
16
  SingleSelect,
13
17
  SingleSelectOption,
14
18
  TextInput,
19
+ Typography,
15
20
  VisuallyHidden,
16
21
  } from '@strapi/design-system';
17
- import { useTracking } from '@strapi/helper-plugin';
18
- import { Drag, Trash } from '@strapi/icons';
22
+ import { Menu, MenuItem } from '@strapi/design-system/v2';
23
+ import {
24
+ ConfirmDialog,
25
+ useNotification,
26
+ NotAllowedInput,
27
+ useTracking,
28
+ } from '@strapi/helper-plugin';
29
+ import { Drag, More } from '@strapi/icons';
19
30
  import { useField } from 'formik';
20
31
  import PropTypes from 'prop-types';
21
32
  import { getEmptyImage } from 'react-dnd-html5-backend';
22
33
  import { useIntl } from 'react-intl';
23
- import { useDispatch } from 'react-redux';
34
+ import { useDispatch, useSelector } from 'react-redux';
35
+ import styled from 'styled-components';
24
36
 
25
37
  import { useDragAndDrop } from '../../../../../../../../../admin/src/content-manager/hooks';
26
38
  import { composeRefs } from '../../../../../../../../../admin/src/content-manager/utils';
27
- import { deleteStage, updateStage, updateStagePosition } from '../../../actions';
39
+ import {
40
+ cloneStage,
41
+ deleteStage,
42
+ updateStage,
43
+ updateStagePosition,
44
+ updateStages,
45
+ } from '../../../actions';
28
46
  import { DRAG_DROP_TYPES } from '../../../constants';
47
+ import { selectRoles } from '../../../selectors';
29
48
  import { getAvailableStageColors, getStageColorByHex } from '../../../utils/colors';
30
49
 
50
+ const NestedOption = styled(MultiSelectOption)`
51
+ padding-left: ${({ theme }) => theme.spaces[7]};
52
+ `;
53
+
54
+ // Grow the size of the permission Select
55
+ const PermissionWrapper = styled(Flex)`
56
+ > * {
57
+ flex-grow: 1;
58
+ }
59
+ `;
60
+
61
+ // Make sure the apply to all stages button doesn't collapse, when the Select
62
+ // contains more tags than it can fit into one line
63
+ const ApplyToAllStages = styled(Button)`
64
+ flex-shrink: 0;
65
+ `;
66
+
67
+ const DeleteMenuItem = styled(MenuItem)`
68
+ color: ${({ theme }) => theme.colors.danger600};
69
+ `;
70
+
31
71
  const AVAILABLE_COLORS = getAvailableStageColors();
32
72
 
33
73
  function StageDropPreview() {
@@ -137,13 +177,23 @@ export function Stage({
137
177
  dispatch(updateStagePosition(oldIndex, newIndex));
138
178
  };
139
179
 
180
+ const handleApplyPermissionsToAllStages = () => {
181
+ setIsApplyAllConfirmationOpen(true);
182
+ };
183
+
140
184
  const [liveText, setLiveText] = React.useState(null);
141
185
  const { formatMessage } = useIntl();
142
186
  const { trackUsage } = useTracking();
143
187
  const dispatch = useDispatch();
188
+ const toggleNotification = useNotification();
144
189
  const [isOpen, setIsOpen] = React.useState(isOpenDefault);
190
+ const [isApplyAllConfirmationOpen, setIsApplyAllConfirmationOpen] = React.useState(false);
145
191
  const [nameField, nameMeta, nameHelper] = useField(`stages.${index}.name`);
146
192
  const [colorField, colorMeta, colorHelper] = useField(`stages.${index}.color`);
193
+ const [permissionsField, permissionsMeta, permissionsHelper] = useField(
194
+ `stages.${index}.permissions`
195
+ );
196
+ const roles = useSelector(selectRoles);
147
197
  const [{ handlerId, isDragging, handleKeyDown }, stageRef, dropRef, dragRef, dragPreviewRef] =
148
198
  useDragAndDrop(canReorder, {
149
199
  index,
@@ -171,12 +221,17 @@ export function Stage({
171
221
  color: hex,
172
222
  }));
173
223
 
224
+ const { themeColorName } = getStageColorByHex(colorField.value) ?? {};
225
+
226
+ const filteredRoles = roles
227
+ // Super admins always have permissions to do everything and therefore
228
+ // there is no point for this role to show up in the role combobox
229
+ .filter((role) => role.code !== 'strapi-super-admin');
230
+
174
231
  React.useEffect(() => {
175
232
  dragPreviewRef(getEmptyImage(), { captureDraggingState: false });
176
233
  }, [dragPreviewRef, index]);
177
234
 
178
- const { themeColorName } = getStageColorByHex(colorField.value) ?? {};
179
-
180
235
  return (
181
236
  <Box ref={composedRef}>
182
237
  {liveText && <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>}
@@ -196,7 +251,7 @@ export function Stage({
196
251
  }}
197
252
  expanded={isOpen}
198
253
  shadow="tableShadow"
199
- error={nameMeta.error ?? colorMeta?.error ?? false}
254
+ error={nameMeta.error ?? colorMeta?.error ?? permissionsMeta?.error ?? false}
200
255
  hasErrorMessage={false}
201
256
  >
202
257
  <AccordionToggle
@@ -205,18 +260,41 @@ export function Stage({
205
260
  action={
206
261
  (canDelete || canUpdate) && (
207
262
  <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
- )}
263
+ <Menu.Root>
264
+ <Menu.Trigger size="S" endIcon={null} paddingLeft={2} paddingRight={2}>
265
+ <More aria-hidden focusable={false} />
266
+ <VisuallyHidden as="span">
267
+ {formatMessage({
268
+ id: '[tbdb].components.DynamicZone.more-actions',
269
+ defaultMessage: 'More actions',
270
+ })}
271
+ </VisuallyHidden>
272
+ </Menu.Trigger>
273
+ {/* z-index needs to be as big as the one defined for the wrapper in Stages, otherwise the menu
274
+ * disappears behind the accordion
275
+ */}
276
+ <Menu.Content popoverPlacement="bottom-end" zIndex={2}>
277
+ <Menu.SubRoot>
278
+ {canUpdate && (
279
+ <MenuItem onClick={() => dispatch(cloneStage(id))}>
280
+ {formatMessage({
281
+ id: 'Settings.review-workflows.stage.delete',
282
+ defaultMessage: 'Duplicate stage',
283
+ })}
284
+ </MenuItem>
285
+ )}
286
+
287
+ {canDelete && (
288
+ <DeleteMenuItem onClick={() => dispatch(deleteStage(id))}>
289
+ {formatMessage({
290
+ id: 'Settings.review-workflows.stage.delete',
291
+ defaultMessage: 'Delete',
292
+ })}
293
+ </DeleteMenuItem>
294
+ )}
295
+ </Menu.SubRoot>
296
+ </Menu.Content>
297
+ </Menu.Root>
220
298
 
221
299
  {canUpdate && (
222
300
  <IconButton
@@ -315,10 +393,140 @@ export function Stage({
315
393
  })}
316
394
  </SingleSelect>
317
395
  </GridItem>
396
+
397
+ <GridItem col={6}>
398
+ {filteredRoles.length === 0 ? (
399
+ <NotAllowedInput
400
+ description={{
401
+ id: 'Settings.review-workflows.stage.permissions.noPermissions.description',
402
+ defaultMessage: 'You don’t have the permission to see roles',
403
+ }}
404
+ intlLabel={{
405
+ id: 'Settings.review-workflows.stage.permissions.label',
406
+ defaultMessage: 'Roles that can change this stage',
407
+ }}
408
+ name={permissionsField.name}
409
+ />
410
+ ) : (
411
+ <Flex alignItems="flex-end" gap={3}>
412
+ <PermissionWrapper grow={1}>
413
+ <MultiSelect
414
+ {...permissionsField}
415
+ disabled={!canUpdate}
416
+ error={permissionsMeta.error ?? false}
417
+ id={permissionsField.name}
418
+ label={formatMessage({
419
+ id: 'Settings.review-workflows.stage.permissions.label',
420
+ defaultMessage: 'Roles that can change this stage',
421
+ })}
422
+ onChange={(values) => {
423
+ // Because the select components expects strings for values, but
424
+ // the yup schema validates we are sending full permission objects to the API,
425
+ // we must coerce the string value back to an object
426
+ const permissions = values.map((value) => ({
427
+ role: parseInt(value, 10),
428
+ action: 'admin::review-workflows.stage.transition',
429
+ }));
430
+
431
+ permissionsHelper.setValue(permissions);
432
+ dispatch(updateStage(id, { permissions }));
433
+ }}
434
+ placeholder={formatMessage({
435
+ id: 'Settings.review-workflows.stage.permissions.placeholder',
436
+ defaultMessage: 'Select a role',
437
+ })}
438
+ required
439
+ // The Select component expects strings for values
440
+ value={(permissionsField.value ?? []).map(
441
+ (permission) => `${permission.role}`
442
+ )}
443
+ withTags
444
+ >
445
+ {[
446
+ {
447
+ label: formatMessage({
448
+ id: 'Settings.review-workflows.stage.permissions.allRoles.label',
449
+ defaultMessage: 'All roles',
450
+ }),
451
+
452
+ children: filteredRoles.map((role) => ({
453
+ value: `${role.id}`,
454
+ label: role.name,
455
+ })),
456
+ },
457
+ ].map((role) => {
458
+ if ('children' in role) {
459
+ return (
460
+ <MultiSelectGroup
461
+ key={role.label}
462
+ label={role.label}
463
+ values={role.children.map((child) => child.value)}
464
+ >
465
+ {role.children.map((role) => {
466
+ return (
467
+ <NestedOption key={role.value} value={role.value}>
468
+ {role.label}
469
+ </NestedOption>
470
+ );
471
+ })}
472
+ </MultiSelectGroup>
473
+ );
474
+ }
475
+
476
+ return (
477
+ <MultiSelectOption key={role.value} value={role.value}>
478
+ {role.label}
479
+ </MultiSelectOption>
480
+ );
481
+ })}
482
+ </MultiSelect>
483
+ </PermissionWrapper>
484
+
485
+ <ApplyToAllStages
486
+ disabled={!canUpdate}
487
+ size="L"
488
+ type="button"
489
+ variant="secondary"
490
+ onClick={() => handleApplyPermissionsToAllStages(permissionsField.value)}
491
+ >
492
+ {formatMessage({
493
+ id: 'Settings.review-workflows.stage.permissions.apply.label',
494
+ defaultMessage: 'Apply to all stages',
495
+ })}
496
+ </ApplyToAllStages>
497
+ </Flex>
498
+ )}
499
+ </GridItem>
318
500
  </Grid>
319
501
  </AccordionContent>
320
502
  </Accordion>
321
503
  )}
504
+
505
+ <ConfirmDialog.Root
506
+ isOpen={isApplyAllConfirmationOpen}
507
+ onToggleDialog={() => setIsApplyAllConfirmationOpen(false)}
508
+ onConfirm={() => {
509
+ dispatch(updateStages({ permissions: permissionsField.value }));
510
+ setIsApplyAllConfirmationOpen(false);
511
+ toggleNotification({
512
+ type: 'success',
513
+ message: formatMessage({
514
+ id: 'Settings.review-workflows.page.edit.confirm.stages.permissions.copy.success',
515
+ defaultMessage: 'Applied roles to all other stages of the workflow',
516
+ }),
517
+ });
518
+ }}
519
+ >
520
+ <ConfirmDialog.Body>
521
+ <Typography textAlign="center" variant="omega">
522
+ {formatMessage({
523
+ id: 'Settings.review-workflows.page.edit.confirm.stages.permissions.copy',
524
+ defaultMessage:
525
+ 'Roles that can change that stage will be applied to all the other stages.',
526
+ })}
527
+ </Typography>
528
+ </ConfirmDialog.Body>
529
+ </ConfirmDialog.Root>
322
530
  </Box>
323
531
  );
324
532
  }
@@ -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
+ }
@@ -15,10 +15,18 @@ import { useMutation } from 'react-query';
15
15
  import { useDispatch, useSelector } from 'react-redux';
16
16
  import { useHistory } from 'react-router-dom';
17
17
 
18
+ import { useAdminRoles } from '../../../../../../../../admin/src/hooks/useAdminRoles';
18
19
  import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes';
19
20
  import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer';
20
21
  import { useLicenseLimits } from '../../../../../../hooks/useLicenseLimits';
21
- import { addStage, resetWorkflow } from '../../actions';
22
+ import {
23
+ addStage,
24
+ resetWorkflow,
25
+ setContentTypes,
26
+ setIsLoading,
27
+ setRoles,
28
+ setWorkflows,
29
+ } from '../../actions';
22
30
  import * as Layout from '../../components/Layout';
23
31
  import * as LimitsModal from '../../components/LimitsModal';
24
32
  import { Stages } from '../../components/Stages';
@@ -29,7 +37,13 @@ import {
29
37
  REDUX_NAMESPACE,
30
38
  } from '../../constants';
31
39
  import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
32
- import { reducer, initialState } from '../../reducer';
40
+ import { reducer } from '../../reducer';
41
+ import {
42
+ selectIsLoading,
43
+ selectIsWorkflowDirty,
44
+ selectCurrentWorkflow,
45
+ selectRoles,
46
+ } from '../../selectors';
33
47
  import { validateWorkflow } from '../../utils/validateWorkflow';
34
48
 
35
49
  export function ReviewWorkflowsCreateView() {
@@ -39,13 +53,15 @@ export function ReviewWorkflowsCreateView() {
39
53
  const { formatAPIError } = useAPIErrorHandler();
40
54
  const dispatch = useDispatch();
41
55
  const toggleNotification = useNotification();
42
- const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes();
43
- const { isLoading: isWorkflowLoading, meta, workflows } = useReviewWorkflows();
44
- const {
45
- clientState: {
46
- currentWorkflow: { data: currentWorkflow, isDirty: currentWorkflowIsDirty },
47
- },
48
- } = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState);
56
+ const { collectionTypes, singleTypes, isLoading: isLoadingContentTypes } = useContentTypes();
57
+ const { isLoading: isLoadingWorkflow, meta, workflows } = useReviewWorkflows();
58
+ const { isLoading: isLoadingRoles, roles: serverRoles } = useAdminRoles(undefined, {
59
+ retry: false,
60
+ });
61
+ const isLoading = useSelector(selectIsLoading);
62
+ const currentWorkflowIsDirty = useSelector(selectIsWorkflowDirty);
63
+ const currentWorkflow = useSelector(selectCurrentWorkflow);
64
+ const roles = useSelector(selectRoles);
49
65
  const [showLimitModal, setShowLimitModal] = React.useState(false);
50
66
  const { isLoading: isLicenseLoading, getFeature } = useLicenseLimits();
51
67
  const [initialErrors, setInitialErrors] = React.useState(null);
@@ -54,7 +70,7 @@ export function ReviewWorkflowsCreateView() {
54
70
  const limits = getFeature('review-workflows');
55
71
  const contentTypesFromOtherWorkflows = workflows.flatMap((workflow) => workflow.contentTypes);
56
72
 
57
- const { mutateAsync, isLoading } = useMutation(
73
+ const { mutateAsync, isLoading: isLoadingMutation } = useMutation(
58
74
  async ({ workflow }) => {
59
75
  const {
60
76
  data: { data },
@@ -167,13 +183,36 @@ export function ReviewWorkflowsCreateView() {
167
183
  React.useEffect(() => {
168
184
  dispatch(resetWorkflow());
169
185
 
186
+ if (!isLoadingWorkflow) {
187
+ dispatch(setWorkflows({ workflows }));
188
+ }
189
+
190
+ if (!isLoadingContentTypes) {
191
+ dispatch(setContentTypes({ collectionTypes, singleTypes }));
192
+ }
193
+
194
+ if (!isLoadingRoles) {
195
+ dispatch(setRoles(serverRoles));
196
+ }
197
+
198
+ dispatch(setIsLoading(isLoadingContentTypes || isLoadingRoles));
199
+
170
200
  // Create an empty default stage
171
201
  dispatch(
172
202
  addStage({
173
203
  name: '',
174
204
  })
175
205
  );
176
- }, [dispatch]);
206
+ }, [
207
+ collectionTypes,
208
+ dispatch,
209
+ isLoadingContentTypes,
210
+ isLoadingRoles,
211
+ isLoadingWorkflow,
212
+ serverRoles,
213
+ singleTypes,
214
+ workflows,
215
+ ]);
177
216
 
178
217
  /**
179
218
  * If the current license has a limit:
@@ -189,7 +228,7 @@ export function ReviewWorkflowsCreateView() {
189
228
  */
190
229
 
191
230
  React.useEffect(() => {
192
- if (!isWorkflowLoading && !isLicenseLoading) {
231
+ if (!isLoadingWorkflow && !isLicenseLoading) {
193
232
  if (
194
233
  limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME] &&
195
234
  meta?.workflowsTotal >= parseInt(limits[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME], 10)
@@ -205,12 +244,25 @@ export function ReviewWorkflowsCreateView() {
205
244
  }
206
245
  }, [
207
246
  isLicenseLoading,
208
- isWorkflowLoading,
247
+ isLoadingWorkflow,
209
248
  limits,
210
249
  meta?.workflowsTotal,
211
250
  currentWorkflow.stages.length,
212
251
  ]);
213
252
 
253
+ React.useEffect(() => {
254
+ if (!isLoading && roles.length === 0) {
255
+ toggleNotification({
256
+ blockTransition: true,
257
+ type: 'warning',
258
+ message: formatMessage({
259
+ id: 'Settings.review-workflows.stage.permissions.noPermissions.description',
260
+ defaultMessage: 'You don’t have the permission to see roles',
261
+ }),
262
+ });
263
+ }
264
+ }, [formatMessage, isLoading, roles, toggleNotification]);
265
+
214
266
  return (
215
267
  <>
216
268
  <Layout.DragLayerRendered />
@@ -225,7 +277,7 @@ export function ReviewWorkflowsCreateView() {
225
277
  type="submit"
226
278
  size="M"
227
279
  disabled={!currentWorkflowIsDirty}
228
- isLoading={isLoading}
280
+ isLoading={isLoadingMutation}
229
281
  >
230
282
  {formatMessage({
231
283
  id: 'global.save',
@@ -247,7 +299,7 @@ export function ReviewWorkflowsCreateView() {
247
299
  />
248
300
  <Layout.Root>
249
301
  <Flex alignItems="stretch" direction="column" gap={7}>
250
- {isLoadingModels ? (
302
+ {isLoading ? (
251
303
  <Loader>
252
304
  {formatMessage({
253
305
  id: 'Settings.review-workflows.page.isLoading',
@@ -256,10 +308,7 @@ export function ReviewWorkflowsCreateView() {
256
308
  </Loader>
257
309
  ) : (
258
310
  <Flex alignItems="stretch" direction="column" gap={7}>
259
- <WorkflowAttributes
260
- contentTypes={{ collectionTypes, singleTypes }}
261
- workflows={workflows}
262
- />
311
+ <WorkflowAttributes />
263
312
  <Stages stages={formik.values?.stages} />
264
313
  </Flex>
265
314
  )}