@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
@@ -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
  };
@@ -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,64 @@ 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(permission.role, permission.action, stageId)
65
+ );
66
+
67
+ // Update stage with the new permissions
68
+ await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
69
+ data: {
70
+ permissions: permissions.flat().map((p) => p.id),
71
+ },
72
+ });
73
+ }, []);
74
+
41
75
  metrics.sendDidCreateStage();
42
76
 
43
77
  return stages;
44
78
  },
45
79
 
46
- async update(stageId, stageData) {
80
+ async update(srcStage, destStage) {
81
+ let stagePermissions = srcStage?.permissions ?? [];
82
+ const stageId = destStage.id;
83
+
84
+ if (destStage.permissions) {
85
+ await this.deleteStagePermissions([srcStage]);
86
+
87
+ const permissions = await mapAsync(destStage.permissions, (permission) =>
88
+ stagePermissionsService.register(permission.role, permission.action, stageId)
89
+ );
90
+ stagePermissions = permissions.flat().map((p) => p.id);
91
+ }
92
+
47
93
  const stage = await strapi.entityService.update(STAGE_MODEL_UID, stageId, {
48
- data: stageData,
94
+ data: {
95
+ ...destStage,
96
+ permissions: stagePermissions,
97
+ },
49
98
  });
50
99
 
51
100
  metrics.sendDidEditStage();
@@ -53,20 +102,31 @@ module.exports = ({ strapi }) => {
53
102
  return stage;
54
103
  },
55
104
 
56
- async delete(stageId) {
57
- const stage = await strapi.entityService.delete(STAGE_MODEL_UID, stageId);
105
+ async delete(stage) {
106
+ // Unregister all permissions related to this stage id
107
+ await this.deleteStagePermissions([stage]);
108
+
109
+ const deletedStage = await strapi.entityService.delete(STAGE_MODEL_UID, stage.id);
58
110
 
59
111
  metrics.sendDidDeleteStage();
60
112
 
61
- return stage;
113
+ return deletedStage;
62
114
  },
63
115
 
64
- async deleteMany(stagesId) {
116
+ async deleteMany(stages) {
117
+ await this.deleteStagePermissions(stages);
118
+
65
119
  return strapi.entityService.deleteMany(STAGE_MODEL_UID, {
66
- filters: { id: { $in: stagesId } },
120
+ filters: { id: { $in: stages.map((s) => s.id) } },
67
121
  });
68
122
  },
69
123
 
124
+ async deleteStagePermissions(stages) {
125
+ // TODO: Find another way to do this for when we use the "to" parameter.
126
+ const permissions = stages.map((s) => s.permissions || []).flat();
127
+ await stagePermissionsService.unregister(permissions || []);
128
+ },
129
+
70
130
  count({ workflowId } = {}) {
71
131
  const opts = {};
72
132
 
@@ -91,7 +151,11 @@ module.exports = ({ strapi }) => {
91
151
  const createdStagesIds = map('id', createdStages);
92
152
 
93
153
  // Update the workflow stages
94
- await mapAsync(updated, (stage) => this.update(stage.id, stage));
154
+ await mapAsync(updated, (destStage) => {
155
+ const srcStage = srcStages.find((s) => s.id === destStage.id);
156
+
157
+ return this.update(srcStage, destStage);
158
+ });
95
159
 
96
160
  // Delete the stages that are not in the new stages list
97
161
  await mapAsync(deleted, async (stage) => {
@@ -114,7 +178,7 @@ module.exports = ({ strapi }) => {
114
178
  });
115
179
  });
116
180
 
117
- return this.delete(stage.id);
181
+ return this.delete(stage);
118
182
  });
119
183
 
120
184
  return destStages.map((stage) => ({ ...stage, id: stage.id ?? createdStagesIds.shift() }));
@@ -241,12 +305,19 @@ module.exports = ({ strapi }) => {
241
305
  */
242
306
  function getDiffBetweenStages(sourceStages, comparisonStages) {
243
307
  const result = comparisonStages.reduce(
308
+ // ...
309
+
244
310
  (acc, stageToCompare) => {
245
311
  const srcStage = sourceStages.find((stage) => stage.id === stageToCompare.id);
246
312
 
247
313
  if (!srcStage) {
248
314
  acc.created.push(stageToCompare);
249
- } else if (srcStage.name !== stageToCompare.name || srcStage.color !== stageToCompare.color) {
315
+ } else if (
316
+ !isEqual(
317
+ pick(['name', 'color', 'permissions'], srcStage),
318
+ pick(['name', 'color', 'permissions'], stageToCompare)
319
+ )
320
+ ) {
250
321
  acc.updated.push(stageToCompare);
251
322
  }
252
323
  return acc;
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { set, isString, map, get } = require('lodash/fp');
4
4
  const { ApplicationError } = require('@strapi/utils').errors;
5
- const { WORKFLOW_MODEL_UID } = require('../../../constants/workflows');
5
+ const { WORKFLOW_MODEL_UID, WORKFLOW_POPULATE } = require('../../../constants/workflows');
6
6
  const { getService } = require('../../../utils');
7
7
  const { getWorkflowContentTypeFilter } = require('../../../utils/review-workflows');
8
8
  const workflowsContentTypesFactory = require('./content-types');
@@ -17,6 +17,16 @@ const processFilters = ({ strapi }, filters = {}) => {
17
17
  return processedFilters;
18
18
  };
19
19
 
20
+ // TODO: How can we improve this? Maybe using traversePopulate?
21
+ const processPopulate = (populate) => {
22
+ // If it does not exist or it's not an object (like an array) return the default populate
23
+ if (!populate) {
24
+ return populate;
25
+ }
26
+
27
+ return WORKFLOW_POPULATE;
28
+ };
29
+
20
30
  module.exports = ({ strapi }) => {
21
31
  const workflowsContentTypes = workflowsContentTypesFactory({ strapi });
22
32
  const workflowsValidationService = getService('review-workflows-validation', { strapi });
@@ -31,7 +41,9 @@ module.exports = ({ strapi }) => {
31
41
  */
32
42
  async find(opts = {}) {
33
43
  const filters = processFilters({ strapi }, opts.filters);
34
- return strapi.entityService.findMany(WORKFLOW_MODEL_UID, { ...opts, filters });
44
+ const populate = processPopulate(opts.populate);
45
+
46
+ return strapi.entityService.findMany(WORKFLOW_MODEL_UID, { ...opts, filters, populate });
35
47
  },
36
48
 
37
49
  /**
@@ -41,7 +53,8 @@ module.exports = ({ strapi }) => {
41
53
  * @returns {Promise<object>} - Workflow object matching the requested ID.
42
54
  */
43
55
  findById(id, opts) {
44
- return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, opts);
56
+ const populate = processPopulate(opts.populate);
57
+ return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, { ...opts, populate });
45
58
  },
46
59
 
47
60
  /**
@@ -51,7 +64,7 @@ module.exports = ({ strapi }) => {
51
64
  * @throws {ValidationError} - If the workflow has no stages.
52
65
  */
53
66
  async create(opts) {
54
- let createOpts = { ...opts, populate: { stages: true } };
67
+ let createOpts = { ...opts, populate: WORKFLOW_POPULATE };
55
68
 
56
69
  workflowsValidationService.validateWorkflowStages(opts.data.stages);
57
70
  await workflowsValidationService.validateWorkflowCount(1);
@@ -87,7 +100,7 @@ module.exports = ({ strapi }) => {
87
100
  */
88
101
  async update(workflow, opts) {
89
102
  const stageService = getService('stages', { strapi });
90
- let updateOpts = { ...opts, populate: { stages: true } };
103
+ let updateOpts = { ...opts, populate: { ...WORKFLOW_POPULATE } };
91
104
  let updatedStageIds;
92
105
 
93
106
  await workflowsValidationService.validateWorkflowCount();
@@ -104,7 +117,7 @@ module.exports = ({ strapi }) => {
104
117
  .replaceStages(workflow.stages, opts.data.stages, workflow.contentTypes)
105
118
  .then((stages) => stages.map((stage) => stage.id));
106
119
 
107
- updateOpts = set('data.stages', updatedStageIds, opts);
120
+ updateOpts = set('data.stages', updatedStageIds, updateOpts);
108
121
  }
109
122
 
110
123
  // Update (un)assigned Content Types
@@ -141,7 +154,7 @@ module.exports = ({ strapi }) => {
141
154
 
142
155
  return strapi.db.transaction(async () => {
143
156
  // Delete stages
144
- await stageService.deleteMany(workflow.stages.map((stage) => stage.id));
157
+ await stageService.deleteMany(workflow.stages);
145
158
 
146
159
  // Unassign all content types, this will migrate the content types to null
147
160
  await workflowsContentTypes.migrate({
@@ -4,11 +4,22 @@
4
4
 
5
5
  const { yup, validateYupSchema } = require('@strapi/utils');
6
6
  const { hasStageAttribute } = require('../utils/review-workflows');
7
+ const { STAGE_TRANSITION_UID } = require('../constants/workflows');
7
8
 
8
9
  const stageObject = yup.object().shape({
9
10
  id: yup.number().integer().min(1),
10
11
  name: yup.string().max(255).required(),
11
12
  color: yup.string().matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i), // hex color
13
+ permissions: yup.array().of(
14
+ yup.object().shape({
15
+ role: yup.number().integer().min(1).required(),
16
+ action: yup.string().oneOf([STAGE_TRANSITION_UID]).required(),
17
+ actionParameters: yup.object().shape({
18
+ from: yup.number().integer().min(1).required(),
19
+ to: yup.number().integer().min(1),
20
+ }),
21
+ })
22
+ ),
12
23
  });
13
24
 
14
25
  const validateUpdateStageOnEntity = yup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/admin",
3
- "version": "4.13.2",
3
+ "version": "4.14.0-alpha.0",
4
4
  "description": "Strapi Admin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,14 +42,14 @@
42
42
  "dependencies": {
43
43
  "@casl/ability": "^5.4.3",
44
44
  "@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
45
- "@strapi/data-transfer": "4.13.2",
45
+ "@strapi/data-transfer": "4.14.0-alpha.0",
46
46
  "@strapi/design-system": "1.9.0",
47
- "@strapi/helper-plugin": "4.13.2",
47
+ "@strapi/helper-plugin": "4.14.0-alpha.0",
48
48
  "@strapi/icons": "1.9.0",
49
- "@strapi/permissions": "4.13.2",
50
- "@strapi/provider-audit-logs-local": "4.13.2",
51
- "@strapi/typescript-utils": "4.13.2",
52
- "@strapi/utils": "4.13.2",
49
+ "@strapi/permissions": "4.14.0-alpha.0",
50
+ "@strapi/provider-audit-logs-local": "4.14.0-alpha.0",
51
+ "@strapi/typescript-utils": "4.14.0-alpha.0",
52
+ "@strapi/utils": "4.14.0-alpha.0",
53
53
  "axios": "1.5.0",
54
54
  "bcryptjs": "2.4.3",
55
55
  "browserslist": "^4.17.3",
@@ -154,5 +154,5 @@
154
154
  }
155
155
  }
156
156
  },
157
- "gitHead": "989fc49fa863563de26161544d21716c9ee805b5"
157
+ "gitHead": "1c3c286e2481cd8cbbb50e0e0bb8fbd0b824d2e4"
158
158
  }
package/scripts/build.js CHANGED
@@ -7,8 +7,7 @@ const { isObject } = require('lodash');
7
7
  const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
8
8
 
9
9
  const webpackConfig = require('../webpack.config');
10
- const { getPlugins } = require('../utils/get-plugins');
11
- const { createPluginsJs } = require('../utils/create-cache-dir');
10
+ const { getPlugins, createPluginFile } = require('../utils/plugins');
12
11
 
13
12
  // Wrapper that outputs the webpack speed
14
13
  const smp = new SpeedMeasurePlugin();
@@ -30,7 +29,7 @@ const buildAdmin = async () => {
30
29
  '@strapi/plugin-users-permissions',
31
30
  ]);
32
31
 
33
- await createPluginsJs(plugins, path.join(__dirname, '..'));
32
+ await createPluginFile(plugins, path.join(__dirname, '..'));
34
33
 
35
34
  const args = {
36
35
  entry,
@@ -2,8 +2,7 @@
2
2
 
3
3
  const { join } = require('path');
4
4
  const fs = require('fs-extra');
5
- const { getPlugins } = require('../utils/get-plugins');
6
- const { createPluginsJs } = require('../utils/create-cache-dir');
5
+ const { getPlugins, createPluginFile } = require('../utils/plugins');
7
6
 
8
7
  /**
9
8
  * Write the plugins.js file or copy the plugins-dev.js file if it exists
@@ -20,7 +19,7 @@ const createFile = async () => {
20
19
 
21
20
  const plugins = getPlugins();
22
21
 
23
- return createPluginsJs(plugins, join(__dirname, '..'));
22
+ return createPluginFile(plugins, join(__dirname, '..'));
24
23
  };
25
24
 
26
25
  createFile()
@@ -29,6 +29,12 @@ module.exports = {
29
29
  configurable: false,
30
30
  required: true,
31
31
  },
32
+ actionParameters: {
33
+ type: 'json',
34
+ configurable: false,
35
+ required: false,
36
+ default: {},
37
+ },
32
38
  subject: {
33
39
  type: 'string',
34
40
  minLength: 1,
@@ -26,8 +26,16 @@ const {
26
26
  * @property {string} subject - The subject on which the permission should applies
27
27
  */
28
28
 
29
- const permissionFields = ['id', 'action', 'subject', 'properties', 'conditions', 'role'];
30
- const sanitizedPermissionFields = ['id', 'action', 'subject', 'properties', 'conditions'];
29
+ const permissionFields = [
30
+ 'id',
31
+ 'action',
32
+ 'actionParameters',
33
+ 'subject',
34
+ 'properties',
35
+ 'conditions',
36
+ 'role',
37
+ ];
38
+ const sanitizedPermissionFields = ['id', 'action', 'actionParameters', 'subject', 'properties', 'conditions'];
31
39
 
32
40
  const sanitizePermissionFields = pick(sanitizedPermissionFields);
33
41
 
@@ -36,6 +44,7 @@ const sanitizePermissionFields = pick(sanitizedPermissionFields);
36
44
  * @return {Permission}
37
45
  */
38
46
  const getDefaultPermission = () => ({
47
+ actionParameters: {},
39
48
  conditions: [],
40
49
  properties: {},
41
50
  subject: null,
@@ -24,7 +24,7 @@ const ACTIONS = {
24
24
 
25
25
  const sanitizeRole = omit(['users', 'permissions']);
26
26
 
27
- const COMPARABLE_FIELDS = ['conditions', 'properties', 'subject', 'action'];
27
+ const COMPARABLE_FIELDS = ['conditions', 'properties', 'subject', 'action', 'actionParameters'];
28
28
  const pickComparableFields = pick(COMPARABLE_FIELDS);
29
29
 
30
30
  const jsonClean = (data) => JSON.parse(JSON.stringify(data));
@@ -323,6 +323,13 @@ const displayWarningIfNoSuperAdmin = async () => {
323
323
  const assignPermissions = async (roleId, permissions = []) => {
324
324
  await validatePermissionsExist(permissions);
325
325
 
326
+ // Internal actions are not handled by the role service, so any permission
327
+ // with an internal action is filtered out
328
+ const internalActions = getService('permission')
329
+ .actionProvider.values()
330
+ .filter((action) => action.section === 'internal')
331
+ .map((action) => action.actionId);
332
+
326
333
  const superAdmin = await getService('role').getSuperAdmin();
327
334
  const isSuperAdmin = superAdmin && superAdmin.id === roleId;
328
335
  const assignRole = set('role', roleId);
@@ -342,13 +349,13 @@ const assignPermissions = async (roleId, permissions = []) => {
342
349
  arePermissionsEqual,
343
350
  permissionsWithRole,
344
351
  existingPermissions
345
- );
352
+ ).filter((permission) => !internalActions.includes(permission.action));
346
353
 
347
354
  const permissionsToDelete = differenceWith(
348
355
  arePermissionsEqual,
349
356
  existingPermissions,
350
357
  permissionsWithRole
351
- );
358
+ ).filter((permission) => !internalActions.includes(permission.action));
352
359
 
353
360
  const permissionsToReturn = differenceBy('id', permissionsToDelete, existingPermissions);
354
361
 
@@ -374,7 +381,8 @@ const addPermissions = async (roleId, permissions) => {
374
381
 
375
382
  const permissionsWithRole = permissions
376
383
  .map(set('role', roleId))
377
- .map(sanitizeConditions(conditionProvider));
384
+ .map(sanitizeConditions(conditionProvider))
385
+ .map(permissionDomain.create);
378
386
 
379
387
  return createMany(permissionsWithRole);
380
388
  };
@@ -17,7 +17,7 @@ const registerProviderActionSchema = yup
17
17
  (v) => `${v.path}: The id can only contain lowercase letters, dots and hyphens.`
18
18
  )
19
19
  .required(),
20
- section: yup.string().oneOf(['contentTypes', 'plugins', 'settings']).required(),
20
+ section: yup.string().oneOf(['contentTypes', 'plugins', 'settings', 'internal']).required(),
21
21
  pluginName: yup.mixed().when('section', {
22
22
  is: 'plugins',
23
23
  then: validators.isAPluginName.required(),