@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
@@ -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
  )}
@@ -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
  };