@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
@@ -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
 
@@ -3,7 +3,11 @@
3
3
  const { mapAsync } = require('@strapi/utils');
4
4
  const { getService } = require('../../../utils');
5
5
  const { validateUpdateStageOnEntity } = require('../../../validation/review-workflows');
6
- const { STAGE_MODEL_UID } = require('../../../constants/workflows');
6
+ const {
7
+ STAGE_MODEL_UID,
8
+ ENTITY_STAGE_ATTRIBUTE,
9
+ STAGE_TRANSITION_UID,
10
+ } = require('../../../constants/workflows');
7
11
 
8
12
  /**
9
13
  *
@@ -75,18 +79,36 @@ module.exports = {
75
79
  */
76
80
  async updateEntity(ctx) {
77
81
  const stagesService = getService('stages');
82
+ const stagePermissions = getService('stage-permissions');
78
83
  const workflowService = getService('workflows');
79
84
 
80
- const { model_uid: modelUID, id: entityIdString } = ctx.params;
85
+ const { model_uid: modelUID, id } = ctx.params;
81
86
  const { body } = ctx.request;
82
87
 
83
- const entityId = Number(entityIdString);
84
-
85
88
  const { sanitizeOutput } = strapi
86
89
  .plugin('content-manager')
87
90
  .service('permission-checker')
88
91
  .create({ userAbility: ctx.state.userAbility, model: modelUID });
89
92
 
93
+ // Load entity
94
+ const entity = await strapi.entityService.findOne(modelUID, Number(id), {
95
+ populate: [ENTITY_STAGE_ATTRIBUTE],
96
+ });
97
+
98
+ if (!entity) {
99
+ ctx.throw(404, 'Entity not found');
100
+ }
101
+
102
+ // Validate if entity stage can be updated
103
+ const canTransition = stagePermissions.can(
104
+ STAGE_TRANSITION_UID,
105
+ entity[ENTITY_STAGE_ATTRIBUTE]?.id
106
+ );
107
+
108
+ if (!canTransition) {
109
+ ctx.throw(403, 'Forbidden stage transition');
110
+ }
111
+
90
112
  const { id: stageId } = await validateUpdateStageOnEntity(
91
113
  { id: Number(body?.data?.id) },
92
114
  'You should pass an id to the body of the put request.'
@@ -95,8 +117,73 @@ module.exports = {
95
117
  const workflow = await workflowService.assertContentTypeBelongsToWorkflow(modelUID);
96
118
  workflowService.assertStageBelongsToWorkflow(stageId, workflow);
97
119
 
98
- const entity = await stagesService.updateEntity({ id: entityId, modelUID }, stageId);
120
+ const updatedEntity = await stagesService.updateEntity({ id: entity.id, modelUID }, stageId);
99
121
 
100
- ctx.body = { data: await sanitizeOutput(entity) };
122
+ ctx.body = { data: await sanitizeOutput(updatedEntity) };
123
+ },
124
+
125
+ /**
126
+ * List all the stages that are available for a user to transition an entity to.
127
+ * If the user has permission to change the current stage of the entity every other stage in the workflow is returned
128
+ * @async
129
+ * @param {*} ctx
130
+ * @param {string} ctx.params.model_uid - The model UID of the entity.
131
+ * @param {string} ctx.params.id - The ID of the entity.
132
+ * @throws {ApplicationError} If review workflows is not activated on the specified model UID.
133
+ */
134
+ async listAvailableStages(ctx) {
135
+ const stagePermissions = getService('stage-permissions');
136
+ const workflowService = getService('workflows');
137
+
138
+ const { model_uid: modelUID, id } = ctx.params;
139
+
140
+ if (
141
+ strapi
142
+ .plugin('content-manager')
143
+ .service('permission-checker')
144
+ .create({ userAbility: ctx.state.userAbility, model: modelUID })
145
+ .cannot.read()
146
+ ) {
147
+ return ctx.forbidden();
148
+ }
149
+
150
+ // Load entity
151
+ const entity = await strapi.entityService.findOne(modelUID, Number(id), {
152
+ populate: [ENTITY_STAGE_ATTRIBUTE],
153
+ });
154
+
155
+ if (!entity) {
156
+ ctx.throw(404, 'Entity not found');
157
+ }
158
+
159
+ const entityStageId = entity[ENTITY_STAGE_ATTRIBUTE]?.id;
160
+ const canTransition = stagePermissions.can(STAGE_TRANSITION_UID, entityStageId);
161
+
162
+ const [workflowCount, { stages: workflowStages }] = await Promise.all([
163
+ workflowService.count(),
164
+ workflowService.getAssignedWorkflow(modelUID, {
165
+ populate: 'stages',
166
+ }),
167
+ ]);
168
+
169
+ const meta = {
170
+ stageCount: workflowStages.length,
171
+ workflowCount,
172
+ };
173
+
174
+ if (!canTransition) {
175
+ ctx.body = {
176
+ data: [],
177
+ meta,
178
+ };
179
+
180
+ return;
181
+ }
182
+
183
+ const data = workflowStages.filter((stage) => stage.id !== entityStageId);
184
+ ctx.body = {
185
+ data,
186
+ meta,
187
+ };
101
188
  },
102
189
  };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const { STAGE_TRANSITION_UID, STAGE_MODEL_UID } = require('../constants/workflows');
4
+ const { getService } = require('../utils');
5
+
6
+ async function migrateReviewWorkflowStagesRoles({ oldContentTypes, contentTypes }) {
7
+ const stageUID = 'admin::workflow-stage';
8
+ const hadRolePermissions = !!oldContentTypes?.[stageUID]?.attributes?.permissions;
9
+ const hasRolePermissions = !!contentTypes?.[stageUID]?.attributes?.permissions;
10
+
11
+ // If the stage content type did not have permissions in the previous version
12
+ // then we set the permissions of every stage to be every current role in the app.
13
+ // This ensures consistent behaviour when upgrading to a strapi version with review workflows RBAC.
14
+ if (!hadRolePermissions && hasRolePermissions) {
15
+ const roleUID = 'admin::role';
16
+ strapi.log.info(
17
+ `Migrating all existing review workflow stages to have RBAC permissions for all ${roleUID}.`
18
+ );
19
+
20
+ const stagePermissionsService = getService('stage-permissions');
21
+
22
+ const stages = await strapi.query(stageUID).findMany();
23
+ const roles = await strapi.query(roleUID).findMany();
24
+
25
+ // Collect the permissions to add and group them by stage id.
26
+ const groupedPermissions = {};
27
+ roles
28
+ .map((role) => role.id)
29
+ .forEach((roleId) => {
30
+ stages
31
+ .map((stage) => stage.id)
32
+ .forEach((stageId) => {
33
+ if (!groupedPermissions[stageId]) {
34
+ groupedPermissions[stageId] = [];
35
+ }
36
+
37
+ groupedPermissions[stageId].push({
38
+ roleId,
39
+ fromStage: stageId,
40
+ action: STAGE_TRANSITION_UID,
41
+ });
42
+ });
43
+ });
44
+
45
+ for (const [stageId, permissions] of Object.entries(groupedPermissions)) {
46
+ const numericalStageId = Number(stageId);
47
+
48
+ if (Number.isNaN(numericalStageId)) {
49
+ strapi.log.warn(
50
+ `Unable to apply ${roleUID} migration for ${stageUID} with id ${stageId}. The stage does not have a numerical id.`
51
+ );
52
+ continue;
53
+ }
54
+
55
+ // Register the permissions for this stage
56
+ const stagePermissions = await stagePermissionsService.registerMany(permissions);
57
+
58
+ // Update the stage with its new permissions
59
+ await strapi.entityService.update(STAGE_MODEL_UID, numericalStageId, {
60
+ data: {
61
+ permissions: stagePermissions.flat().map((permission) => permission.id),
62
+ },
63
+ });
64
+ }
65
+ }
66
+ }
67
+
68
+ module.exports = migrateReviewWorkflowStagesRoles;
@@ -3,6 +3,10 @@
3
3
  const { WORKFLOW_MODEL_UID } = require('../constants/workflows');
4
4
  const defaultWorkflow = require('../constants/default-workflow.json');
5
5
 
6
+ /**
7
+ * Multiple workflows introduced the ability to name a workflow.
8
+ * This migration adds the default workflow name if the name attribute was added.
9
+ */
6
10
  async function migrateReviewWorkflowName({ oldContentTypes, contentTypes }) {
7
11
  // Look for RW name attribute
8
12
  const hadName = !!oldContentTypes?.[WORKFLOW_MODEL_UID]?.attributes?.name;
@@ -11,6 +15,9 @@ async function migrateReviewWorkflowName({ oldContentTypes, contentTypes }) {
11
15
  // Add the default workflow name if name attribute was added
12
16
  if (!hadName && hasName) {
13
17
  await strapi.query(WORKFLOW_MODEL_UID).updateMany({
18
+ where: {
19
+ name: { $null: true },
20
+ },
14
21
  data: {
15
22
  name: defaultWorkflow.name,
16
23
  },
@@ -4,6 +4,7 @@ const { features } = require('@strapi/strapi/lib/utils/ee');
4
4
  const executeCERegister = require('../../server/register');
5
5
  const migrateAuditLogsTable = require('./migrations/audit-logs-table');
6
6
  const migrateReviewWorkflowStagesColor = require('./migrations/review-workflows-stages-color');
7
+ const migrateReviewWorkflowStagesRoles = require('./migrations/review-workflows-stages-roles');
7
8
  const migrateReviewWorkflowName = require('./migrations/review-workflows-workflow-name');
8
9
  const migrateWorkflowsContentTypes = require('./migrations/review-workflows-content-types');
9
10
  const migrateStageAttribute = require('./migrations/review-workflows-stage-attribute');
@@ -26,6 +27,7 @@ module.exports = async ({ strapi }) => {
26
27
  strapi
27
28
  .hook('strapi::content-types.afterSync')
28
29
  .register(migrateReviewWorkflowStagesColor)
30
+ .register(migrateReviewWorkflowStagesRoles)
29
31
  .register(migrateReviewWorkflowName)
30
32
  .register(migrateWorkflowsContentTypes)
31
33
  .register(migrateDeletedCTInWorkflows);
@@ -131,15 +131,16 @@ module.exports = {
131
131
  handler: 'stages.updateEntity',
132
132
  config: {
133
133
  middlewares: [enableFeatureMiddleware('review-workflows')],
134
- policies: [
135
- 'admin::isAuthenticatedAdmin',
136
- {
137
- name: 'admin::hasPermissions',
138
- config: {
139
- actions: ['admin::review-workflows.update'],
140
- },
141
- },
142
- ],
134
+ policies: ['admin::isAuthenticatedAdmin'],
135
+ },
136
+ },
137
+ {
138
+ method: 'GET',
139
+ path: '/content-manager/(collection|single)-types/:model_uid/:id/stages',
140
+ handler: 'stages.listAvailableStages',
141
+ config: {
142
+ middlewares: [enableFeatureMiddleware('review-workflows')],
143
+ policies: ['admin::isAuthenticatedAdmin'],
143
144
  },
144
145
  },
145
146
  {
@@ -8,6 +8,7 @@ module.exports = {
8
8
  'seat-enforcement': require('./seat-enforcement'),
9
9
  workflows: require('./review-workflows/workflows'),
10
10
  stages: require('./review-workflows/stages'),
11
+ 'stage-permissions': require('./review-workflows/stage-permissions'),
11
12
  assignees: require('./review-workflows/assignees'),
12
13
  'review-workflows': require('./review-workflows/review-workflows'),
13
14
  'review-workflows-validation': require('./review-workflows/validation'),
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const { prop } = require('lodash/fp');
4
+ const {
5
+ mapAsync,
6
+ errors: { ApplicationError },
7
+ } = require('@strapi/utils');
8
+ const { getService } = require('../../utils');
9
+ const { STAGE_TRANSITION_UID } = require('../../constants/workflows');
10
+
11
+ const validActions = [STAGE_TRANSITION_UID];
12
+
13
+ module.exports = ({ strapi }) => {
14
+ const roleService = getService('role');
15
+ const permissionService = getService('permission');
16
+
17
+ return {
18
+ async register({ roleId, action, fromStage }) {
19
+ if (!validActions.includes(action)) {
20
+ throw new ApplicationError(`Invalid action ${action}`);
21
+ }
22
+ const permissions = await roleService.addPermissions(roleId, [
23
+ {
24
+ action,
25
+ actionParameters: {
26
+ from: fromStage,
27
+ },
28
+ },
29
+ ]);
30
+
31
+ // TODO: Filter response
32
+ return permissions;
33
+ },
34
+ async registerMany(permissions) {
35
+ return mapAsync(permissions, this.register);
36
+ },
37
+ async unregister(permissions) {
38
+ const permissionIds = permissions.map(prop('id'));
39
+ await permissionService.deleteByIds(permissionIds);
40
+ },
41
+ can(action, fromStage) {
42
+ const requestState = strapi.requestContext.get()?.state;
43
+
44
+ if (!requestState) {
45
+ return false;
46
+ }
47
+
48
+ // Override permissions for super admin
49
+ const userRoles = requestState.user?.roles;
50
+ if (userRoles?.some((role) => role.code === 'strapi-super-admin')) {
51
+ return true;
52
+ }
53
+
54
+ return requestState.userAbility.can({
55
+ name: action,
56
+ params: { from: fromStage },
57
+ });
58
+ },
59
+ };
60
+ };
@@ -2,15 +2,20 @@
2
2
 
3
3
  const {
4
4
  mapAsync,
5
+ reduceAsync,
5
6
  errors: { ApplicationError, ValidationError },
6
7
  } = require('@strapi/utils');
7
- const { map } = require('lodash/fp');
8
+ const { map, pick, isEqual } = require('lodash/fp');
8
9
 
9
10
  const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE, ERRORS } = require('../../constants/workflows');
10
11
  const { getService } = require('../../utils');
11
12
 
13
+ const sanitizedStageFields = ['id', 'name', 'workflow', 'color'];
14
+ const sanitizeStageFields = pick(sanitizedStageFields);
15
+
12
16
  module.exports = ({ strapi }) => {
13
17
  const metrics = getService('review-workflows-metrics', { strapi });
18
+ const stagePermissionsService = getService('stage-permissions', { strapi });
14
19
  const workflowsValidationService = getService('review-workflows-validation', { strapi });
15
20
 
16
21
  return {
@@ -32,20 +37,72 @@ module.exports = ({ strapi }) => {
32
37
  async createMany(stagesList, { fields } = {}) {
33
38
  const params = { select: fields ?? '*' };
34
39
 
40
+ // TODO: pick the fields from the stage
35
41
  const stages = await Promise.all(
36
42
  stagesList.map((stage) =>
37
- strapi.entityService.create(STAGE_MODEL_UID, { data: stage, ...params })
43
+ strapi.entityService.create(STAGE_MODEL_UID, {
44
+ data: sanitizeStageFields(stage),
45
+ ...params,
46
+ })
38
47
  )
39
48
  );
40
49
 
50
+ // Create stage permissions
51
+ await reduceAsync(stagesList)(async (_, stage, idx) => {
52
+ // Ignore stages without permissions
53
+ if (!stage.permissions || stage.permissions.length === 0) {
54
+ return;
55
+ }
56
+
57
+ const stagePermissions = stage.permissions;
58
+ const stageId = stages[idx].id;
59
+
60
+ const permissions = await mapAsync(
61
+ stagePermissions,
62
+ // Register each stage permission
63
+ (permission) =>
64
+ stagePermissionsService.register({
65
+ roleId: permission.role,
66
+ action: permission.action,
67
+ fromStage: stageId,
68
+ })
69
+ );
70
+
71
+ // Update stage with the new permissions
72
+ await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
73
+ data: {
74
+ permissions: permissions.flat().map((p) => p.id),
75
+ },
76
+ });
77
+ }, []);
78
+
41
79
  metrics.sendDidCreateStage();
42
80
 
43
81
  return stages;
44
82
  },
45
83
 
46
- async update(stageId, stageData) {
84
+ async update(srcStage, destStage) {
85
+ let stagePermissions = srcStage?.permissions ?? [];
86
+ const stageId = destStage.id;
87
+
88
+ if (destStage.permissions) {
89
+ await this.deleteStagePermissions([srcStage]);
90
+
91
+ const permissions = await mapAsync(destStage.permissions, (permission) =>
92
+ stagePermissionsService.register({
93
+ roleId: permission.role,
94
+ action: permission.action,
95
+ fromStage: stageId,
96
+ })
97
+ );
98
+ stagePermissions = permissions.flat().map((p) => p.id);
99
+ }
100
+
47
101
  const stage = await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
48
- data: stageData,
102
+ data: {
103
+ ...destStage,
104
+ permissions: stagePermissions,
105
+ },
49
106
  });
50
107
 
51
108
  metrics.sendDidEditStage();
@@ -53,20 +110,31 @@ module.exports = ({ strapi }) => {
53
110
  return stage;
54
111
  },
55
112
 
56
- async delete(stageId) {
57
- const stage = await strapi.entityService.delete(STAGE_MODEL_UID, stageId);
113
+ async delete(stage) {
114
+ // Unregister all permissions related to this stage id
115
+ await this.deleteStagePermissions([stage]);
116
+
117
+ const deletedStage = await strapi.entityService.delete(STAGE_MODEL_UID, stage.id);
58
118
 
59
119
  metrics.sendDidDeleteStage();
60
120
 
61
- return stage;
121
+ return deletedStage;
62
122
  },
63
123
 
64
- async deleteMany(stagesId) {
124
+ async deleteMany(stages) {
125
+ await this.deleteStagePermissions(stages);
126
+
65
127
  return strapi.entityService.deleteMany(STAGE_MODEL_UID, {
66
- filters: { id: { $in: stagesId } },
128
+ filters: { id: { $in: stages.map((s) => s.id) } },
67
129
  });
68
130
  },
69
131
 
132
+ async deleteStagePermissions(stages) {
133
+ // TODO: Find another way to do this for when we use the "to" parameter.
134
+ const permissions = stages.map((s) => s.permissions || []).flat();
135
+ await stagePermissionsService.unregister(permissions || []);
136
+ },
137
+
70
138
  count({ workflowId } = {}) {
71
139
  const opts = {};
72
140
 
@@ -91,7 +159,11 @@ module.exports = ({ strapi }) => {
91
159
  const createdStagesIds = map('id', createdStages);
92
160
 
93
161
  // Update the workflow stages
94
- await mapAsync(updated, (stage) => this.update(stage.id, stage));
162
+ await mapAsync(updated, (destStage) => {
163
+ const srcStage = srcStages.find((s) => s.id === destStage.id);
164
+
165
+ return this.update(srcStage, destStage);
166
+ });
95
167
 
96
168
  // Delete the stages that are not in the new stages list
97
169
  await mapAsync(deleted, async (stage) => {
@@ -114,7 +186,7 @@ module.exports = ({ strapi }) => {
114
186
  });
115
187
  });
116
188
 
117
- return this.delete(stage.id);
189
+ return this.delete(stage);
118
190
  });
119
191
 
120
192
  return destStages.map((stage) => ({ ...stage, id: stage.id ?? createdStagesIds.shift() }));
@@ -241,12 +313,19 @@ module.exports = ({ strapi }) => {
241
313
  */
242
314
  function getDiffBetweenStages(sourceStages, comparisonStages) {
243
315
  const result = comparisonStages.reduce(
316
+ // ...
317
+
244
318
  (acc, stageToCompare) => {
245
319
  const srcStage = sourceStages.find((stage) => stage.id === stageToCompare.id);
246
320
 
247
321
  if (!srcStage) {
248
322
  acc.created.push(stageToCompare);
249
- } else if (srcStage.name !== stageToCompare.name || srcStage.color !== stageToCompare.color) {
323
+ } else if (
324
+ !isEqual(
325
+ pick(['name', 'color', 'permissions'], srcStage),
326
+ pick(['name', 'color', 'permissions'], stageToCompare)
327
+ )
328
+ ) {
250
329
  acc.updated.push(stageToCompare);
251
330
  }
252
331
  return acc;