@strapi/admin 4.13.2 → 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 (73) hide show
  1. package/admin/src/components/NpsSurvey/index.js +5 -2
  2. package/admin/src/hooks/useAdminRoles/index.js +17 -7
  3. package/admin/src/hooks/useAdminUsers/useAdminUsers.js +16 -7
  4. package/admin/src/hooks/useContentTypes/useContentTypes.js +18 -7
  5. package/build/1227.ec336799.chunk.js +1 -0
  6. package/build/{3483.19381b40.chunk.js → 3483.f6b2439f.chunk.js} +1 -1
  7. package/build/4174.4587c7f6.chunk.js +1 -0
  8. package/build/6266.53be9ea3.chunk.js +124 -0
  9. package/build/7897.eac204a4.chunk.js +6 -0
  10. package/build/{Admin-authenticatedApp.5a6b7544.chunk.js → Admin-authenticatedApp.d200a4ee.chunk.js} +2 -2
  11. package/build/{admin-app.2a8615ab.chunk.js → admin-app.582877a3.chunk.js} +11 -11
  12. package/build/admin-edit-roles-page.0aa65505.chunk.js +267 -0
  13. package/build/admin-edit-users.9215912a.chunk.js +10 -0
  14. package/build/admin-roles-list.824a50de.chunk.js +22 -0
  15. package/build/admin-users.f6b3c643.chunk.js +11 -0
  16. package/build/audit-logs-settings-page.be2cb4dd.chunk.js +1 -0
  17. package/build/{content-manager.f448efdf.chunk.js → content-manager.06a2f7ec.chunk.js} +39 -39
  18. package/build/index.html +1 -1
  19. package/build/review-workflows-settings-create-view.604cffa0.chunk.js +1 -0
  20. package/build/review-workflows-settings-edit-view.73c57f07.chunk.js +1 -0
  21. package/build/review-workflows-settings-list-view.7e300ecb.chunk.js +56 -0
  22. package/build/{runtime~main.ec4717bd.js → runtime~main.9de029f4.js} +2 -2
  23. package/build/sso-settings-page.94373f78.chunk.js +1 -0
  24. package/ee/admin/content-manager/pages/EditView/InformationBox/components/StageSelect/StageSelect.js +65 -53
  25. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +50 -5
  26. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +227 -19
  27. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js +8 -23
  28. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +6 -0
  29. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +17 -7
  30. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflowsStages.js +36 -0
  31. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +68 -19
  32. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +105 -35
  33. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +68 -27
  34. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/selectors.js +45 -0
  35. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/validateWorkflow.js +20 -0
  36. package/ee/server/config/admin-actions.js +6 -0
  37. package/ee/server/constants/workflows.js +13 -0
  38. package/ee/server/content-types/workflow-stage/index.js +6 -0
  39. package/ee/server/controllers/workflows/index.js +41 -16
  40. package/ee/server/controllers/workflows/stages/index.js +93 -6
  41. package/ee/server/routes/review-workflows.js +10 -9
  42. package/ee/server/services/index.js +1 -0
  43. package/ee/server/services/review-workflows/stage-permissions.js +60 -0
  44. package/ee/server/services/review-workflows/stages.js +83 -12
  45. package/ee/server/services/review-workflows/workflows/index.js +20 -7
  46. package/ee/server/validation/review-workflows.js +11 -0
  47. package/package.json +8 -8
  48. package/scripts/build.js +2 -3
  49. package/scripts/create-dev-plugins-file.js +2 -3
  50. package/server/content-types/Permission.js +6 -0
  51. package/server/domain/permission/index.js +11 -2
  52. package/server/services/role.js +12 -4
  53. package/server/validation/action-provider.js +1 -1
  54. package/server/validation/common-validators.js +92 -100
  55. package/server/validation/permission.js +0 -3
  56. package/utils/create-cache-dir.js +5 -102
  57. package/utils/plugins.js +217 -0
  58. package/webpack.config.js +2 -2
  59. package/build/1227.9f37e1dc.chunk.js +0 -1
  60. package/build/2237.b832ae6e.chunk.js +0 -114
  61. package/build/4174.f1f39e40.chunk.js +0 -1
  62. package/build/4724.aea5c8c1.chunk.js +0 -6
  63. package/build/admin-edit-roles-page.38a6c863.chunk.js +0 -267
  64. package/build/admin-edit-users.545fc882.chunk.js +0 -10
  65. package/build/admin-roles-list.1e2e814d.chunk.js +0 -22
  66. package/build/admin-users.b8ea5677.chunk.js +0 -11
  67. package/build/audit-logs-settings-page.96f9d608.chunk.js +0 -1
  68. package/build/review-workflows-settings-create-view.4a156a19.chunk.js +0 -1
  69. package/build/review-workflows-settings-edit-view.ce984d1f.chunk.js +0 -1
  70. package/build/review-workflows-settings-list-view.419b8deb.chunk.js +0 -56
  71. package/build/sso-settings-page.45153df5.chunk.js +0 -1
  72. package/utils/create-plugins-exclude-path.js +0 -20
  73. package/utils/get-plugins.js +0 -110
@@ -16,11 +16,19 @@ import { useMutation } from 'react-query';
16
16
  import { useSelector, useDispatch } from 'react-redux';
17
17
  import { useParams } from 'react-router-dom';
18
18
 
19
+ import { useAdminRoles } from '../../../../../../../../admin/src/hooks/useAdminRoles';
19
20
  import { useContentTypes } from '../../../../../../../../admin/src/hooks/useContentTypes';
20
21
  import { useInjectReducer } from '../../../../../../../../admin/src/hooks/useInjectReducer';
21
22
  import { selectAdminPermissions } from '../../../../../../../../admin/src/pages/App/selectors';
22
23
  import { useLicenseLimits } from '../../../../../../hooks/useLicenseLimits';
23
- import { resetWorkflow, setWorkflow } from '../../actions';
24
+ import {
25
+ resetWorkflow,
26
+ setIsLoading,
27
+ setWorkflow,
28
+ setContentTypes,
29
+ setRoles,
30
+ setWorkflows,
31
+ } from '../../actions';
24
32
  import * as Layout from '../../components/Layout';
25
33
  import * as LimitsModal from '../../components/LimitsModal';
26
34
  import { Stages } from '../../components/Stages';
@@ -31,7 +39,15 @@ import {
31
39
  REDUX_NAMESPACE,
32
40
  } from '../../constants';
33
41
  import { useReviewWorkflows } from '../../hooks/useReviewWorkflows';
34
- import { reducer, initialState } from '../../reducer';
42
+ import { reducer } from '../../reducer';
43
+ import {
44
+ selectIsWorkflowDirty,
45
+ selectCurrentWorkflow,
46
+ selectHasDeletedServerStages,
47
+ selectIsLoading,
48
+ selectRoles,
49
+ selectServerState,
50
+ } from '../../selectors';
35
51
  import { validateWorkflow } from '../../utils/validateWorkflow';
36
52
 
37
53
  export function ReviewWorkflowsEditView() {
@@ -42,29 +58,22 @@ export function ReviewWorkflowsEditView() {
42
58
  const { put } = useFetchClient();
43
59
  const { formatAPIError } = useAPIErrorHandler();
44
60
  const toggleNotification = useNotification();
45
- const {
46
- isLoading: isWorkflowLoading,
47
- meta,
48
- workflows,
49
- status: workflowStatus,
50
- refetch,
51
- } = useReviewWorkflows();
52
- const { collectionTypes, singleTypes, isLoading: isLoadingModels } = useContentTypes();
53
- const {
54
- status,
55
- clientState: {
56
- currentWorkflow: {
57
- data: currentWorkflow,
58
- isDirty: currentWorkflowIsDirty,
59
- hasDeletedServerStages,
60
- },
61
- },
62
- } = useSelector((state) => state?.[REDUX_NAMESPACE] ?? initialState);
61
+ const { isLoading: isLoadingWorkflow, meta, workflows, refetch } = useReviewWorkflows();
62
+ const { collectionTypes, singleTypes, isLoading: isLoadingContentTypes } = useContentTypes();
63
+ const serverState = useSelector(selectServerState);
64
+ const currentWorkflowIsDirty = useSelector(selectIsWorkflowDirty);
65
+ const currentWorkflow = useSelector(selectCurrentWorkflow);
66
+ const hasDeletedServerStages = useSelector(selectHasDeletedServerStages);
67
+ const roles = useSelector(selectRoles);
68
+ const isLoading = useSelector(selectIsLoading);
63
69
  const {
64
70
  allowedActions: { canDelete, canUpdate },
65
71
  } = useRBAC(permissions.settings['review-workflows']);
66
72
  const [savePrompts, setSavePrompts] = React.useState({});
67
73
  const { getFeature, isLoading: isLicenseLoading } = useLicenseLimits();
74
+ const { isLoading: isLoadingRoles, roles: serverRoles } = useAdminRoles(undefined, {
75
+ retry: false,
76
+ });
68
77
  const [showLimitModal, setShowLimitModal] = React.useState(false);
69
78
  const [initialErrors, setInitialErrors] = React.useState(null);
70
79
 
@@ -73,7 +82,7 @@ export function ReviewWorkflowsEditView() {
73
82
  .filter((workflow) => workflow.id !== parseInt(workflowId, 10))
74
83
  .flatMap((workflow) => workflow.contentTypes);
75
84
 
76
- const { mutateAsync, isLoading } = useMutation(
85
+ const { mutateAsync, isLoading: isLoadingMutation } = useMutation(
77
86
  async ({ workflow }) => {
78
87
  const {
79
88
  data: { data },
@@ -98,7 +107,37 @@ export function ReviewWorkflowsEditView() {
98
107
  setInitialErrors(null);
99
108
 
100
109
  try {
101
- const res = await mutateAsync({ workflow });
110
+ const res = await mutateAsync({
111
+ workflow: {
112
+ ...workflow,
113
+
114
+ // compare permissions of stages and only submit them if at least one has
115
+ // changed; this enables partial updates e.g. for users who don't have
116
+ // permissions to see roles
117
+ stages: workflow.stages.map((stage) => {
118
+ let hasUpdatedPermissions = true;
119
+ const serverStage = serverState.workflow.stages.find(
120
+ (serverStage) => serverStage.id === stage?.id
121
+ );
122
+
123
+ if (serverStage) {
124
+ hasUpdatedPermissions =
125
+ serverStage.permissions?.length !== stage.permission?.length ||
126
+ !serverStage.permissions.every(
127
+ (serverPermission) =>
128
+ !!stage.permissions.find(
129
+ (permission) => permission.role === serverPermission.role
130
+ )
131
+ );
132
+ }
133
+
134
+ return {
135
+ ...stage,
136
+ permissions: hasUpdatedPermissions ? stage.permissions : undefined,
137
+ };
138
+ }),
139
+ },
140
+ });
102
141
 
103
142
  return res;
104
143
  } catch (error) {
@@ -194,14 +233,37 @@ export function ReviewWorkflowsEditView() {
194
233
  const limits = getFeature('review-workflows');
195
234
 
196
235
  React.useEffect(() => {
197
- dispatch(setWorkflow({ status: workflowStatus, data: workflow }));
236
+ if (!isLoadingWorkflow) {
237
+ dispatch(setWorkflow({ workflow }));
238
+ dispatch(setWorkflows({ workflows }));
239
+ }
240
+
241
+ if (!isLoadingContentTypes) {
242
+ dispatch(setContentTypes({ collectionTypes, singleTypes }));
243
+ }
244
+
245
+ if (!isLoadingRoles) {
246
+ dispatch(setRoles(serverRoles));
247
+ }
248
+
249
+ dispatch(setIsLoading(isLoadingWorkflow || isLoadingContentTypes || isLoadingRoles));
198
250
 
199
251
  // reset the state to the initial state to avoid flashes if a user
200
252
  // navigates from an edit-view to a create-view
201
253
  return () => {
202
254
  dispatch(resetWorkflow());
203
255
  };
204
- }, [workflowStatus, workflow, dispatch]);
256
+ }, [
257
+ collectionTypes,
258
+ dispatch,
259
+ isLoadingContentTypes,
260
+ isLoadingWorkflow,
261
+ isLoadingRoles,
262
+ serverRoles,
263
+ singleTypes,
264
+ workflow,
265
+ workflows,
266
+ ]);
205
267
 
206
268
  /**
207
269
  * If the current license has a limit:
@@ -217,7 +279,7 @@ export function ReviewWorkflowsEditView() {
217
279
  */
218
280
 
219
281
  React.useEffect(() => {
220
- if (!isWorkflowLoading && !isLicenseLoading) {
282
+ if (!isLoadingWorkflow && !isLicenseLoading) {
221
283
  if (
222
284
  limits?.[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME] &&
223
285
  meta?.workflowCount > parseInt(limits[CHARGEBEE_WORKFLOW_ENTITLEMENT_NAME], 10)
@@ -234,12 +296,25 @@ export function ReviewWorkflowsEditView() {
234
296
  }, [
235
297
  currentWorkflow.stages.length,
236
298
  isLicenseLoading,
237
- isWorkflowLoading,
299
+ isLoadingWorkflow,
238
300
  limits,
239
301
  meta?.workflowCount,
240
302
  meta.workflowsTotal,
241
303
  ]);
242
304
 
305
+ React.useEffect(() => {
306
+ if (!isLoading && roles.length === 0) {
307
+ toggleNotification({
308
+ blockTransition: true,
309
+ type: 'warning',
310
+ message: formatMessage({
311
+ id: 'Settings.review-workflows.stage.permissions.noPermissions.description',
312
+ defaultMessage: 'You don’t have the permission to see roles',
313
+ }),
314
+ });
315
+ }
316
+ }, [formatMessage, isLoading, roles, toggleNotification]);
317
+
243
318
  // TODO: redirect back to list-view if workflow is not found?
244
319
 
245
320
  return (
@@ -259,7 +334,7 @@ export function ReviewWorkflowsEditView() {
259
334
  disabled={!currentWorkflowIsDirty}
260
335
  // if the confirm dialog is open the loading state is on
261
336
  // the confirm button already
262
- loading={!Object.keys(savePrompts).length > 0 && isLoading}
337
+ loading={!Object.keys(savePrompts).length > 0 && isLoadingMutation}
263
338
  >
264
339
  {formatMessage({
265
340
  id: 'global.save',
@@ -269,7 +344,7 @@ export function ReviewWorkflowsEditView() {
269
344
  )
270
345
  }
271
346
  subtitle={
272
- currentWorkflow.stages.length > 0 &&
347
+ !isLoading &&
273
348
  formatMessage(
274
349
  {
275
350
  id: 'Settings.review-workflows.page.subtitle',
@@ -282,7 +357,7 @@ export function ReviewWorkflowsEditView() {
282
357
  />
283
358
 
284
359
  <Layout.Root>
285
- {isLoadingModels || status === 'loading' ? (
360
+ {isLoading ? (
286
361
  <Flex justifyContent="center">
287
362
  <Loader>
288
363
  {formatMessage({
@@ -293,12 +368,7 @@ export function ReviewWorkflowsEditView() {
293
368
  </Flex>
294
369
  ) : (
295
370
  <Flex alignItems="stretch" direction="column" gap={7}>
296
- <WorkflowAttributes
297
- canUpdate={canUpdate}
298
- contentTypes={{ collectionTypes, singleTypes }}
299
- currentWorkflow={currentWorkflow}
300
- workflows={workflows}
301
- />
371
+ <WorkflowAttributes canUpdate={canUpdate} />
302
372
  <Stages
303
373
  canDelete={canDelete}
304
374
  canUpdate={canUpdate}
@@ -1,21 +1,31 @@
1
- import { current, produce } from 'immer';
2
- import isEqual from 'lodash/isEqual';
1
+ import { produce } from 'immer';
3
2
 
4
3
  import {
5
4
  ACTION_ADD_STAGE,
5
+ ACTION_CLONE_STAGE,
6
6
  ACTION_DELETE_STAGE,
7
7
  ACTION_RESET_WORKFLOW,
8
+ ACTION_SET_CONTENT_TYPES,
9
+ ACTION_SET_IS_LOADING,
10
+ ACTION_SET_ROLES,
8
11
  ACTION_SET_WORKFLOW,
12
+ ACTION_SET_WORKFLOWS,
9
13
  ACTION_UPDATE_STAGE,
14
+ ACTION_UPDATE_STAGES,
10
15
  ACTION_UPDATE_STAGE_POSITION,
11
16
  ACTION_UPDATE_WORKFLOW,
12
17
  STAGE_COLOR_DEFAULT,
13
18
  } from '../constants';
14
19
 
15
20
  export const initialState = {
16
- status: 'loading',
17
21
  serverState: {
22
+ contentTypes: {
23
+ collectionTypes: [],
24
+ singleTypes: [],
25
+ },
26
+ roles: [],
18
27
  workflow: null,
28
+ workflows: [],
19
29
  },
20
30
  clientState: {
21
31
  currentWorkflow: {
@@ -23,10 +33,10 @@ export const initialState = {
23
33
  name: '',
24
34
  contentTypes: [],
25
35
  stages: [],
36
+ permissions: undefined,
26
37
  },
27
- isDirty: false,
28
- hasDeletedServerStages: false,
29
38
  },
39
+ isLoading: true,
30
40
  },
31
41
  };
32
42
 
@@ -35,10 +45,23 @@ export function reducer(state = initialState, action) {
35
45
  const { payload } = action;
36
46
 
37
47
  switch (action.type) {
38
- case ACTION_SET_WORKFLOW: {
39
- const { status, workflow } = payload;
48
+ case ACTION_SET_CONTENT_TYPES: {
49
+ draft.serverState.contentTypes = payload;
50
+ break;
51
+ }
40
52
 
41
- draft.status = status;
53
+ case ACTION_SET_IS_LOADING: {
54
+ draft.clientState.isLoading = payload;
55
+ break;
56
+ }
57
+
58
+ case ACTION_SET_ROLES: {
59
+ draft.serverState.roles = payload;
60
+ break;
61
+ }
62
+
63
+ case ACTION_SET_WORKFLOW: {
64
+ const workflow = payload;
42
65
 
43
66
  if (workflow) {
44
67
  draft.serverState.workflow = workflow;
@@ -47,18 +70,21 @@ export function reducer(state = initialState, action) {
47
70
  stages: workflow.stages.map((stage) => ({
48
71
  ...stage,
49
72
  // A safety net in case a stage does not have a color assigned;
50
- // this normallly should not happen
73
+ // this should not happen
51
74
  color: stage?.color ?? STAGE_COLOR_DEFAULT,
52
75
  })),
53
76
  };
54
77
  }
78
+ break;
79
+ }
55
80
 
56
- draft.clientState.currentWorkflow.hasDeletedServerStages = false;
81
+ case ACTION_SET_WORKFLOWS: {
82
+ draft.serverState.workflows = payload;
57
83
  break;
58
84
  }
59
85
 
60
86
  case ACTION_RESET_WORKFLOW: {
61
- draft.clientState.currentWorkflow.data = initialState.clientState.currentWorkflow.data;
87
+ draft.clientState = initialState.clientState;
62
88
  draft.serverState = initialState.serverState;
63
89
  break;
64
90
  }
@@ -71,12 +97,6 @@ export function reducer(state = initialState, action) {
71
97
  (stage) => (stage?.id ?? stage.__temp_key__) !== stageId
72
98
  );
73
99
 
74
- if (!currentWorkflow.hasDeletedServerStages) {
75
- draft.clientState.currentWorkflow.hasDeletedServerStages = !!(
76
- state.serverState.workflow?.stages ?? []
77
- ).find((stage) => stage.id === stageId);
78
- }
79
-
80
100
  break;
81
101
  }
82
102
 
@@ -100,6 +120,24 @@ export function reducer(state = initialState, action) {
100
120
  break;
101
121
  }
102
122
 
123
+ case ACTION_CLONE_STAGE: {
124
+ const { currentWorkflow } = state.clientState;
125
+ const { id } = payload;
126
+
127
+ const sourceStageIndex = currentWorkflow.data.stages.findIndex(
128
+ (stage) => (stage?.id ?? stage?.__temp_key__) === id
129
+ );
130
+ const sourceStage = currentWorkflow.data.stages[sourceStageIndex];
131
+
132
+ draft.clientState.currentWorkflow.data.stages.splice(sourceStageIndex + 1, 0, {
133
+ ...sourceStage,
134
+ id: undefined,
135
+ __temp_key__: getMaxTempKey(draft.clientState.currentWorkflow.data.stages),
136
+ });
137
+
138
+ break;
139
+ }
140
+
103
141
  case ACTION_UPDATE_STAGE: {
104
142
  const { currentWorkflow } = state.clientState;
105
143
  const { stageId, ...modified } = payload;
@@ -116,6 +154,19 @@ export function reducer(state = initialState, action) {
116
154
  break;
117
155
  }
118
156
 
157
+ case ACTION_UPDATE_STAGES: {
158
+ const { currentWorkflow } = state.clientState;
159
+
160
+ draft.clientState.currentWorkflow.data.stages = currentWorkflow.data.stages.map(
161
+ (stage) => ({
162
+ ...stage,
163
+ ...payload,
164
+ })
165
+ );
166
+
167
+ break;
168
+ }
169
+
119
170
  case ACTION_UPDATE_STAGE_POSITION: {
120
171
  const {
121
172
  currentWorkflow: {
@@ -149,16 +200,6 @@ export function reducer(state = initialState, action) {
149
200
  default:
150
201
  break;
151
202
  }
152
-
153
- if (state.clientState.currentWorkflow.data && draft.serverState.workflow) {
154
- draft.clientState.currentWorkflow.isDirty = !isEqual(
155
- current(draft.clientState.currentWorkflow).data,
156
- draft.serverState.workflow
157
- );
158
- } else {
159
- // if there is no workflow on the server, the workflow is awalys considered dirty
160
- draft.clientState.currentWorkflow.isDirty = true;
161
- }
162
203
  });
163
204
  }
164
205
 
@@ -0,0 +1,45 @@
1
+ import isEqual from 'lodash/isEqual';
2
+ import { createSelector } from 'reselect';
3
+
4
+ import { REDUX_NAMESPACE } from './constants';
5
+ import { initialState } from './reducer';
6
+
7
+ export const selectNamespace = (state) => state[REDUX_NAMESPACE] ?? initialState;
8
+
9
+ export const selectContentTypes = createSelector(
10
+ selectNamespace,
11
+ ({ serverState: { contentTypes } }) => contentTypes
12
+ );
13
+
14
+ export const selectRoles = createSelector(selectNamespace, ({ serverState: { roles } }) => roles);
15
+
16
+ export const selectCurrentWorkflow = createSelector(
17
+ selectNamespace,
18
+ ({ clientState: { currentWorkflow } }) => currentWorkflow.data
19
+ );
20
+
21
+ export const selectWorkflows = createSelector(
22
+ selectNamespace,
23
+ ({ serverState: { workflows } }) => workflows
24
+ );
25
+
26
+ export const selectIsWorkflowDirty = createSelector(
27
+ selectNamespace,
28
+ ({ serverState, clientState: { currentWorkflow } }) =>
29
+ !isEqual(serverState.workflow, currentWorkflow.data)
30
+ );
31
+
32
+ export const selectHasDeletedServerStages = createSelector(
33
+ selectNamespace,
34
+ ({ serverState, clientState: { currentWorkflow } }) =>
35
+ !(serverState.workflow?.stages ?? []).every(
36
+ (stage) => !!currentWorkflow.data.stages.find(({ id }) => id === stage.id)
37
+ )
38
+ );
39
+
40
+ export const selectIsLoading = createSelector(
41
+ selectNamespace,
42
+ ({ clientState: { isLoading } }) => isLoading
43
+ );
44
+
45
+ export const selectServerState = createSelector(selectNamespace, ({ serverState }) => serverState);
@@ -57,6 +57,26 @@ export async function validateWorkflow({ values, formatMessage }) {
57
57
  })
58
58
  )
59
59
  .matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i),
60
+
61
+ permissions: yup
62
+ .array(
63
+ yup.object({
64
+ role: yup
65
+ .number()
66
+ .strict()
67
+ .typeError(
68
+ formatMessage({
69
+ id: 'Settings.review-workflows.validation.stage.permissions.role.number',
70
+ defaultMessage: 'Role must be of type number',
71
+ })
72
+ ).required,
73
+ action: yup.string().required({
74
+ id: 'Settings.review-workflows.validation.stage.permissions.action.required',
75
+ defaultMessage: 'Action is a required argument',
76
+ }),
77
+ })
78
+ )
79
+ .strict(),
60
80
  })
61
81
  )
62
82
  .min(1),
@@ -62,5 +62,11 @@ module.exports = {
62
62
  category: 'review workflows',
63
63
  subCategory: 'options',
64
64
  },
65
+ {
66
+ uid: 'review-workflows.stage.transition',
67
+ displayName: 'Change stage',
68
+ pluginName: 'admin',
69
+ section: 'internal',
70
+ },
65
71
  ],
66
72
  };
@@ -4,6 +4,7 @@
4
4
  module.exports = {
5
5
  WORKFLOW_MODEL_UID: 'admin::workflow',
6
6
  STAGE_MODEL_UID: 'admin::workflow-stage',
7
+ STAGE_TRANSITION_UID: 'admin::review-workflows.stage.transition',
7
8
  STAGE_DEFAULT_COLOR: '#4945FF',
8
9
  ENTITY_STAGE_ATTRIBUTE: 'strapi_stage',
9
10
  ENTITY_ASSIGNEE_ATTRIBUTE: 'strapi_assignee',
@@ -17,4 +18,16 @@ module.exports = {
17
18
  'You’ve reached the limit of stages for this workflow in your plan. Try deleting some stages or contact Sales to enable more stages.',
18
19
  DUPLICATED_STAGE_NAME: 'Stage names must be unique.',
19
20
  },
21
+ WORKFLOW_POPULATE: {
22
+ stages: {
23
+ populate: {
24
+ permissions: {
25
+ fields: ['action', 'actionParameters'],
26
+ populate: {
27
+ role: { fields: ['id', 'name'] },
28
+ },
29
+ },
30
+ },
31
+ },
32
+ },
20
33
  };
@@ -40,6 +40,12 @@ module.exports = {
40
40
  inversedBy: 'stages',
41
41
  configurable: false,
42
42
  },
43
+ permissions: {
44
+ type: 'relation',
45
+ target: 'admin::permission',
46
+ relation: 'manyToMany',
47
+ configurable: false,
48
+ }
43
49
  },
44
50
  },
45
51
  };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { update, map, property } = require('lodash/fp');
3
4
  const { mapAsync } = require('@strapi/utils');
4
5
  const { getService } = require('../../utils');
5
6
 
@@ -7,7 +8,7 @@ const {
7
8
  validateWorkflowCreate,
8
9
  validateWorkflowUpdate,
9
10
  } = require('../../validation/review-workflows');
10
- const { WORKFLOW_MODEL_UID } = require('../../constants/workflows');
11
+ const { WORKFLOW_MODEL_UID, WORKFLOW_POPULATE } = require('../../constants/workflows');
11
12
 
12
13
  /**
13
14
  *
@@ -22,6 +23,21 @@ function getWorkflowsPermissionChecker({ strapi }, userAbility) {
22
23
  .create({ userAbility, model: WORKFLOW_MODEL_UID });
23
24
  }
24
25
 
26
+ /**
27
+ * Transforms workflow to an admin UI format.
28
+ * Some attributes (like permissions) are presented in a different format in the admin UI.
29
+ * @param {Workflow} workflow
30
+ */
31
+ function formatWorkflowToAdmin(workflow) {
32
+ if (!workflow) return;
33
+ if (!workflow.stages) return workflow;
34
+
35
+ // Transform permissions roles to be the id string instead of an object
36
+ const transformPermissions = map(update('role', property('id')));
37
+ const transformStages = map(update('permissions', transformPermissions));
38
+ return update('stages', transformStages, workflow);
39
+ }
40
+
25
41
  module.exports = {
26
42
  /**
27
43
  * Create a new workflow
@@ -38,10 +54,12 @@ module.exports = {
38
54
  const workflowBody = await validateWorkflowCreate(body.data);
39
55
 
40
56
  const workflowService = getService('workflows');
41
- const createdWorkflow = await workflowService.create({
42
- data: await sanitizeCreateInput(workflowBody),
43
- populate,
44
- });
57
+ const createdWorkflow = await workflowService
58
+ .create({
59
+ data: await sanitizeCreateInput(workflowBody),
60
+ populate,
61
+ })
62
+ .then(formatWorkflowToAdmin);
45
63
 
46
64
  ctx.body = {
47
65
  data: await sanitizeOutput(createdWorkflow),
@@ -61,22 +79,27 @@ module.exports = {
61
79
  ctx.state.userAbility
62
80
  );
63
81
  const { populate } = await sanitizedQuery.update(query);
64
-
65
82
  const workflowBody = await validateWorkflowUpdate(body.data);
66
83
 
67
- const workflow = await workflowService.findById(id, { populate: ['stages'] });
84
+ // Find if workflow exists
85
+ const workflow = await workflowService.findById(id, { populate: WORKFLOW_POPULATE });
68
86
  if (!workflow) {
69
87
  return ctx.notFound();
70
88
  }
71
- const getPermittedFieldToUpdate = sanitizeUpdateInput(workflow);
72
89
 
90
+ // Sanitize input data
91
+ const getPermittedFieldToUpdate = sanitizeUpdateInput(workflow);
73
92
  const dataToUpdate = await getPermittedFieldToUpdate(workflowBody);
74
93
 
75
- const updatedWorkflow = await workflowService.update(workflow, {
76
- data: dataToUpdate,
77
- populate,
78
- });
94
+ // Update workflow
95
+ const updatedWorkflow = await workflowService
96
+ .update(workflow, {
97
+ data: dataToUpdate,
98
+ populate,
99
+ })
100
+ .then(formatWorkflowToAdmin);
79
101
 
102
+ // Send sanitized response
80
103
  ctx.body = {
81
104
  data: await sanitizeOutput(updatedWorkflow),
82
105
  };
@@ -96,12 +119,14 @@ module.exports = {
96
119
  );
97
120
  const { populate } = await sanitizedQuery.delete(query);
98
121
 
99
- const workflow = await workflowService.findById(id, { populate: ['stages'] });
122
+ const workflow = await workflowService.findById(id, { populate: WORKFLOW_POPULATE });
100
123
  if (!workflow) {
101
124
  return ctx.notFound("Workflow doesn't exist");
102
125
  }
103
126
 
104
- const deletedWorkflow = await workflowService.delete(workflow, { populate });
127
+ const deletedWorkflow = await workflowService
128
+ .delete(workflow, { populate })
129
+ .then(formatWorkflowToAdmin);
105
130
 
106
131
  ctx.body = {
107
132
  data: await sanitizeOutput(deletedWorkflow),
@@ -122,7 +147,7 @@ module.exports = {
122
147
  const { populate, filters, sort } = await sanitizedQuery.read(query);
123
148
 
124
149
  const [workflows, workflowCount] = await Promise.all([
125
- workflowService.find({ populate, filters, sort }),
150
+ workflowService.find({ populate, filters, sort }).then(map(formatWorkflowToAdmin)),
126
151
  workflowService.count(),
127
152
  ]);
128
153
 
@@ -151,7 +176,7 @@ module.exports = {
151
176
  const workflowService = getService('workflows');
152
177
 
153
178
  const [workflow, workflowCount] = await Promise.all([
154
- workflowService.findById(id, { populate }),
179
+ workflowService.findById(id, { populate }).then(formatWorkflowToAdmin),
155
180
  workflowService.count(),
156
181
  ]);
157
182