@strapi/admin 4.11.4 → 4.12.0-beta.1

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 (213) hide show
  1. package/admin/src/constants.js +83 -83
  2. package/admin/src/content-manager/components/CollectionTypeFormWrapper/index.js +8 -5
  3. package/admin/src/content-manager/components/Inputs/index.js +3 -47
  4. package/admin/src/content-manager/components/SingleTypeFormWrapper/index.js +34 -37
  5. package/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js +0 -27
  6. package/admin/src/content-manager/pages/EditView/Information/index.js +1 -1
  7. package/admin/src/content-manager/pages/EditView/InformationBox/InformationBoxCE.js +1 -2
  8. package/admin/src/content-manager/pages/EditView/InformationBox/index.js +1 -3
  9. package/admin/src/content-manager/pages/EditView/index.js +14 -2
  10. package/admin/src/content-manager/pages/ListView/components/TableRows/index.js +93 -14
  11. package/admin/src/content-manager/pages/ListView/index.js +65 -59
  12. package/admin/src/content-manager/pages/ListView/utils/buildValidGetParams.js +30 -0
  13. package/admin/src/content-manager/pages/ListView/utils/index.js +1 -1
  14. package/admin/src/content-manager/utils/mergeMetasWithSchema.js +5 -1
  15. package/admin/src/hooks/index.js +0 -1
  16. package/admin/src/hooks/useAdminUsers/useAdminUsers.js +3 -3
  17. package/admin/src/hooks/useEnterprise/useEnterprise.js +4 -4
  18. package/admin/src/hooks/useSettingsMenu/index.js +35 -21
  19. package/admin/src/pages/App/index.js +28 -23
  20. package/admin/src/pages/AuthPage/components/Login/index.js +3 -5
  21. package/admin/src/pages/AuthPage/components/Register/index.js +5 -1
  22. package/admin/src/pages/AuthPage/constants.js +3 -2
  23. package/admin/src/pages/AuthPage/index.js +18 -1
  24. package/admin/src/pages/HomePage/index.js +19 -7
  25. package/admin/src/pages/ProfilePage/index.js +12 -12
  26. package/admin/src/pages/SettingsPage/components/SettingsNav/index.js +13 -11
  27. package/admin/src/pages/SettingsPage/components/Tokens/TokenBox/index.js +1 -1
  28. package/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/index.js +17 -1
  29. package/admin/src/pages/SettingsPage/pages/Users/EditPage/index.js +16 -3
  30. package/admin/src/pages/SettingsPage/pages/Users/ListPage/CreateAction/index.js +2 -4
  31. package/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/index.js +15 -1
  32. package/admin/src/pages/SettingsPage/pages/Users/ListPage/index.js +36 -5
  33. package/admin/src/pages/SettingsPage/pages/Users/components/MagicLink/index.js +3 -5
  34. package/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventTable/index.js +1 -3
  35. package/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/WebhookForm/index.js +16 -1
  36. package/admin/src/translations/zh-Hans.json +1 -1
  37. package/build/0cd5f8915b265d5b1856.png +0 -0
  38. package/build/1049.758a01f5.chunk.js +1 -0
  39. package/build/{3528.4845cf92.chunk.js → 1386.762d6eb8.chunk.js} +1 -1
  40. package/build/1727.b49f0713.chunk.js +1 -0
  41. package/build/190.66d89241.chunk.js +117 -0
  42. package/build/{5563.86f9aa9c.chunk.js → 2225.15d1df72.chunk.js} +3 -3
  43. package/build/2379.d33a2e16.chunk.js +1 -0
  44. package/build/2395.b0419a54.chunk.js +26 -0
  45. package/build/2801.18ac397d.chunk.js +1 -0
  46. package/build/{7394.423886bd.chunk.js → 3100.21c343fa.chunk.js} +1 -1
  47. package/build/311.cb0884bb.chunk.js +1 -0
  48. package/build/3483.e182b190.chunk.js +1 -0
  49. package/build/3984.ea7b8036.chunk.js +1 -0
  50. package/build/4546.ff9fdf30.chunk.js +1 -0
  51. package/build/502.ccb38223.chunk.js +1 -0
  52. package/build/5483.ed2c7efa.chunk.js +6 -0
  53. package/build/{5542.c62d0daf.chunk.js → 5542.2415a393.chunk.js} +6 -6
  54. package/build/6158.f9d82db9.chunk.js +1 -0
  55. package/build/7030.b98dcedf.chunk.js +1 -0
  56. package/build/7464.c6d0565c.chunk.js +1 -0
  57. package/build/8276.23e0763b.chunk.js +26 -0
  58. package/build/918.54414509.chunk.js +1 -0
  59. package/build/{9932.7e2b71de.chunk.js → 9932.b5a3bb3a.chunk.js} +81 -81
  60. package/build/9944.7af075a5.chunk.js +26 -0
  61. package/build/{Admin-authenticatedApp.cb649fc1.chunk.js → Admin-authenticatedApp.2ffa318a.chunk.js} +5 -5
  62. package/build/Admin_InternalErrorPage.f45f2462.chunk.js +1 -0
  63. package/build/{Admin_homePage.be30ef4e.chunk.js → Admin_homePage.ac9dfb86.chunk.js} +23 -15
  64. package/build/{Admin_marketplace.74a58e20.chunk.js → Admin_marketplace.f0b87fce.chunk.js} +1 -1
  65. package/build/{Admin_pluginsPage.ce464189.chunk.js → Admin_pluginsPage.8728ff6e.chunk.js} +1 -1
  66. package/build/{Admin_profilePage.2131eb68.chunk.js → Admin_profilePage.a968035f.chunk.js} +2 -2
  67. package/build/Admin_settingsPage.3ad19487.chunk.js +79 -0
  68. package/build/Upload_ConfigureTheView.345ac1e0.chunk.js +1 -0
  69. package/build/admin-app.088bcd33.chunk.js +36 -0
  70. package/build/{admin-edit-roles-page.3fdd6b9d.chunk.js → admin-edit-roles-page.a49b9f4f.chunk.js} +4 -4
  71. package/build/admin-edit-users.67704088.chunk.js +10 -0
  72. package/build/{admin-roles-list.e17b00d7.chunk.js → admin-roles-list.0c129e98.chunk.js} +1 -1
  73. package/build/admin-users.3279ffb0.chunk.js +11 -0
  74. package/build/api-tokens-create-page.46c2ea84.chunk.js +1 -0
  75. package/build/{api-tokens-edit-page.9a1dd2fa.chunk.js → api-tokens-edit-page.58139df9.chunk.js} +1 -1
  76. package/build/{api-tokens-list-page.a103f526.chunk.js → api-tokens-list-page.505bf7e0.chunk.js} +2 -2
  77. package/build/audit-logs-settings-page.4b422831.chunk.js +1 -0
  78. package/build/content-manager.9b569036.chunk.js +1094 -0
  79. package/build/{content-type-builder-list-view.a200a358.chunk.js → content-type-builder-list-view.bf9be456.chunk.js} +9 -9
  80. package/build/content-type-builder-translation-en-json.38e20391.chunk.js +1 -0
  81. package/build/{content-type-builder-translation-zh-json.ad24dbeb.chunk.js → content-type-builder-translation-zh-json.958d90e1.chunk.js} +1 -1
  82. package/build/content-type-builder.3963fb2d.chunk.js +166 -0
  83. package/build/{email-settings-page.45695daa.chunk.js → email-settings-page.d494d1eb.chunk.js} +2 -2
  84. package/build/{i18n-settings-page.29308d0b.chunk.js → i18n-settings-page.47f78016.chunk.js} +1 -1
  85. package/build/index.html +1 -1
  86. package/build/main.98c989b0.js +2908 -0
  87. package/build/review-workflows-settings-create-view.60bc516c.chunk.js +1 -0
  88. package/build/review-workflows-settings-edit-view.898ea409.chunk.js +1 -0
  89. package/build/review-workflows-settings-list-view.240cacdf.chunk.js +56 -0
  90. package/build/runtime~main.44bf2a37.js +2 -0
  91. package/build/sso-settings-page.ed6f3f15.chunk.js +1 -0
  92. package/build/transfer-tokens-create-page.1597e6ab.chunk.js +1 -0
  93. package/build/transfer-tokens-edit-page.8741529f.chunk.js +1 -0
  94. package/build/{transfer-tokens-list-page.7237443d.chunk.js → transfer-tokens-list-page.22147d2c.chunk.js} +2 -2
  95. package/build/upload-settings.cac210a0.chunk.js +14 -0
  96. package/build/upload.8d01c525.chunk.js +26 -0
  97. package/build/{users-advanced-settings-page.750b1f76.chunk.js → users-advanced-settings-page.18379a56.chunk.js} +1 -1
  98. package/build/users-email-settings-page.a87978e5.chunk.js +9 -0
  99. package/build/users-providers-settings-page.8876c1ee.chunk.js +14 -0
  100. package/build/{users-roles-settings-page.1f505119.chunk.js → users-roles-settings-page.0431f48c.chunk.js} +2 -2
  101. package/build/webhook-edit-page.a91f27a1.chunk.js +33 -0
  102. package/build/{webhook-list-page.940a40f1.chunk.js → webhook-list-page.65e1b5bb.chunk.js} +1 -1
  103. package/build/{zh-Hans-json.4cfef87d.chunk.js → zh-Hans-json.fada6f40.chunk.js} +1 -1
  104. package/ee/admin/constants.js +14 -14
  105. package/ee/admin/content-manager/pages/EditView/InformationBox/InformationBoxEE.js +88 -31
  106. package/ee/admin/content-manager/pages/EditView/InformationBox/index.js +1 -3
  107. package/ee/admin/content-manager/{components/DynamicTable/CellContent/ReviewWorkflowsStage → pages/ListView/ReviewWorkflowsColumn}/ReviewWorkflowsStageEE.js +7 -2
  108. package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/constants.js +24 -0
  109. package/ee/admin/content-manager/pages/ListView/ReviewWorkflowsColumn/index.js +1 -0
  110. package/ee/admin/hooks/useLicenseLimitNotification/index.js +17 -6
  111. package/ee/admin/hooks/useLicenseLimits/index.js +1 -32
  112. package/ee/admin/hooks/useLicenseLimits/useLicenseLimits.js +44 -0
  113. package/ee/admin/pages/AuthPage/components/Login/index.js +3 -5
  114. package/ee/admin/pages/HomePage/index.js +11 -0
  115. package/ee/admin/pages/SettingsPage/constants.js +25 -1
  116. package/ee/admin/pages/SettingsPage/pages/ApplicationInfosPage/components/AdminSeatInfo/index.js +7 -7
  117. package/ee/admin/pages/SettingsPage/pages/AuditLogs/ListView/hooks/useAuditLogsData.js +6 -4
  118. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/actions/index.js +19 -4
  119. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Layout/Layout.js +65 -0
  120. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Layout/index.js +1 -0
  121. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/LimitsModal.js +111 -0
  122. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/assets/balloon.png +0 -0
  123. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal/index.js +3 -0
  124. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/ProtectedPage/ProtectedPage.js +21 -0
  125. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/ProtectedPage/index.js +1 -0
  126. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/Stages/Stage/Stage.js +5 -5
  127. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/WorkflowAttributes.js +110 -0
  128. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/components/WorkflowAttributes/index.js +1 -0
  129. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/constants.js +3 -1
  130. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/hooks/useReviewWorkflows.js +13 -19
  131. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/CreateView.js +246 -0
  132. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/CreateView/index.js +13 -0
  133. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/EditView.js +287 -0
  134. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/EditView/index.js +13 -0
  135. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/ListView.js +382 -0
  136. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/pages/ListView/index.js +13 -0
  137. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/reducer/index.js +53 -23
  138. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/utils/getWorkflowValidationSchema.js +43 -28
  139. package/ee/admin/pages/SettingsPage/pages/Users/ListPage/CreateAction/index.js +11 -6
  140. package/ee/admin/pages/SettingsPage/pages/Users/ListPage/index.js +13 -0
  141. package/ee/admin/pages/SettingsPage/pages/Users/components/MagicLink/index.js +3 -5
  142. package/ee/admin/pages/SettingsPage/pages/Webhooks/EditView/components/EventTable/index.js +1 -3
  143. package/ee/server/config/admin-actions.js +24 -0
  144. package/ee/server/constants/default-stages.json +8 -4
  145. package/ee/server/constants/default-workflow.json +3 -1
  146. package/ee/server/constants/workflows.js +10 -1
  147. package/ee/server/content-types/workflow/index.js +10 -0
  148. package/ee/server/content-types/workflow-stage/index.js +3 -1
  149. package/ee/server/controllers/admin.js +1 -0
  150. package/ee/server/controllers/workflows/index.js +135 -8
  151. package/ee/server/controllers/workflows/stages/index.js +38 -38
  152. package/ee/server/migrations/review-workflows-content-types.js +29 -0
  153. package/ee/server/migrations/review-workflows-deleted-ct-in-workflows.js +39 -0
  154. package/ee/server/migrations/review-workflows-stage-attribute.js +49 -0
  155. package/ee/server/migrations/review-workflows-stages-color.js +2 -2
  156. package/ee/server/migrations/review-workflows-workflow-name.js +21 -0
  157. package/ee/server/register.js +12 -2
  158. package/ee/server/routes/review-workflows.js +44 -10
  159. package/ee/server/services/index.js +1 -0
  160. package/ee/server/services/review-workflows/entity-service-decorator.js +8 -13
  161. package/ee/server/services/review-workflows/review-workflows.js +45 -53
  162. package/ee/server/services/review-workflows/stages.js +84 -46
  163. package/ee/server/services/review-workflows/validation.js +60 -0
  164. package/ee/server/services/review-workflows/workflows/content-types.js +80 -0
  165. package/ee/server/services/review-workflows/workflows/index.js +207 -0
  166. package/ee/server/utils/review-workflows.js +30 -25
  167. package/ee/server/validation/review-workflows.js +49 -10
  168. package/index.js +0 -14
  169. package/package.json +12 -21
  170. package/webpack.alias.js +0 -3
  171. package/webpack.config.js +1 -75
  172. package/admin/src/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +0 -2
  173. package/admin/src/content-manager/pages/ListView/utils/buildQueryString.js +0 -36
  174. package/admin/src/content-manager/pages/ListView/utils/createPluginsFilter.js +0 -4
  175. package/admin/src/hooks/useLicenseLimits/index.js +0 -3
  176. package/admin/src/pages/App/utils/index.js +0 -3
  177. package/admin/src/pages/App/utils/unique-identifier.js +0 -12
  178. package/admin/src/pages/SettingsPage/pages/ApplicationInfosPage/components/AdminSeatInfo/index.js +0 -5
  179. package/build/1386.3b2aa6a7.chunk.js +0 -3
  180. package/build/1799.44d2e264.chunk.js +0 -33
  181. package/build/1970.39a2d75e.chunk.js +0 -1
  182. package/build/3269.1ea0f5a6.chunk.js +0 -1
  183. package/build/5932.6a23b88c.chunk.js +0 -1
  184. package/build/7018.98feed67.chunk.js +0 -1
  185. package/build/7259.fb69d4bf.chunk.js +0 -1
  186. package/build/Admin_InternalErrorPage.8911cb49.chunk.js +0 -1
  187. package/build/Admin_settingsPage.4069bb8a.chunk.js +0 -79
  188. package/build/Upload_ConfigureTheView.7a1cb9c9.chunk.js +0 -1
  189. package/build/admin-app.fea867af.chunk.js +0 -61
  190. package/build/admin-edit-users.200551e3.chunk.js +0 -10
  191. package/build/admin-users.3b12dca2.chunk.js +0 -11
  192. package/build/api-tokens-create-page.3dd4e921.chunk.js +0 -1
  193. package/build/audit-logs-settings-page.f538490f.chunk.js +0 -1
  194. package/build/content-manager.c40f5ff9.chunk.js +0 -1088
  195. package/build/content-type-builder-translation-en-json.f592325b.chunk.js +0 -1
  196. package/build/content-type-builder.bd1bbff1.chunk.js +0 -166
  197. package/build/main.ee36abd9.js +0 -2927
  198. package/build/review-workflows-settings.93808ae0.chunk.js +0 -110
  199. package/build/runtime~main.efd966f6.js +0 -2
  200. package/build/sso-settings-page.0cdb96a6.chunk.js +0 -1
  201. package/build/transfer-tokens-create-page.de14cad4.chunk.js +0 -1
  202. package/build/transfer-tokens-edit-page.4f5e39af.chunk.js +0 -1
  203. package/build/upload-settings.cb6c14c3.chunk.js +0 -14
  204. package/build/upload.7e629643.chunk.js +0 -26
  205. package/build/users-email-settings-page.e9bcd865.chunk.js +0 -9
  206. package/build/users-providers-settings-page.a94253e9.chunk.js +0 -14
  207. package/build/webhook-edit-page.77ef4f1a.chunk.js +0 -33
  208. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/getTableColumn.js +0 -58
  209. package/ee/admin/content-manager/components/DynamicTable/CellContent/ReviewWorkflowsStage/index.js +0 -3
  210. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ProtectedPage.js +0 -20
  211. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/ReviewWorkflows.js +0 -204
  212. package/ee/admin/pages/SettingsPage/pages/ReviewWorkflows/index.js +0 -3
  213. package/ee/server/services/review-workflows/workflows.js +0 -25
@@ -2,17 +2,16 @@
2
2
 
3
3
  const {
4
4
  mapAsync,
5
- errors: { ApplicationError },
5
+ errors: { ApplicationError, ValidationError },
6
6
  } = require('@strapi/utils');
7
7
  const { map } = require('lodash/fp');
8
8
 
9
- const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE } = require('../../constants/workflows');
9
+ const { STAGE_MODEL_UID, ENTITY_STAGE_ATTRIBUTE, ERRORS } = require('../../constants/workflows');
10
10
  const { getService } = require('../../utils');
11
- const { getContentTypeUIDsWithActivatedReviewWorkflows } = require('../../utils/review-workflows');
12
11
 
13
12
  module.exports = ({ strapi }) => {
14
- const workflowsService = getService('workflows', { strapi });
15
13
  const metrics = getService('review-workflows-metrics', { strapi });
14
+ const workflowsValidationService = getService('review-workflows-validation', { strapi });
16
15
 
17
16
  return {
18
17
  find({ workflowId, populate }) {
@@ -30,8 +29,8 @@ module.exports = ({ strapi }) => {
30
29
  return strapi.entityService.findOne(STAGE_MODEL_UID, id, params);
31
30
  },
32
31
 
33
- async createMany(stagesList, { fields }) {
34
- const params = { select: fields };
32
+ async createMany(stagesList, { fields } = {}) {
33
+ const params = { select: fields ?? '*' };
35
34
 
36
35
  const stages = await Promise.all(
37
36
  stagesList.map((stage) =>
@@ -62,24 +61,34 @@ module.exports = ({ strapi }) => {
62
61
  return stage;
63
62
  },
64
63
 
65
- count() {
66
- return strapi.entityService.count(STAGE_MODEL_UID);
64
+ async deleteMany(stagesId) {
65
+ return strapi.entityService.deleteMany(STAGE_MODEL_UID, {
66
+ filters: { id: { $in: stagesId } },
67
+ });
67
68
  },
68
69
 
69
- async replaceWorkflowStages(workflowId, stages) {
70
- const workflow = await workflowsService.findById(workflowId, { populate: ['stages'] });
70
+ count({ workflowId } = {}) {
71
+ const opts = {};
71
72
 
72
- const { created, updated, deleted } = getDiffBetweenStages(workflow.stages, stages);
73
+ if (workflowId) {
74
+ opts.where = {
75
+ workflow: workflowId,
76
+ };
77
+ }
78
+ return strapi.entityService.count(STAGE_MODEL_UID, opts);
79
+ },
73
80
 
74
- assertAtLeastOneStageRemain(workflow.stages, { created, deleted });
81
+ async replaceStages(srcStages, destStages, contentTypesToMigrate = []) {
82
+ const { created, updated, deleted } = getDiffBetweenStages(srcStages, destStages);
75
83
 
84
+ assertAtLeastOneStageRemain(srcStages || [], { created, deleted });
85
+
86
+ // Update stages and assign entity stages
76
87
  return strapi.db.transaction(async ({ trx }) => {
77
88
  // Create the new stages
78
89
  const createdStages = await this.createMany(created, { fields: ['id'] });
79
90
  // Put all the newly created stages ids
80
91
  const createdStagesIds = map('id', createdStages);
81
- const stagesIds = stages.map((stage) => stage.id ?? createdStagesIds.shift());
82
- const contentTypes = getContentTypeUIDsWithActivatedReviewWorkflows(strapi.contentTypes);
83
92
 
84
93
  // Update the workflow stages
85
94
  await mapAsync(updated, (stage) => this.update(stage.id, stage));
@@ -89,15 +98,15 @@ module.exports = ({ strapi }) => {
89
98
  // Find the nearest stage in the workflow and newly created stages
90
99
  // that is not deleted, prioritizing the previous stages
91
100
  const nearestStage = findNearestMatchingStage(
92
- [...workflow.stages, ...createdStages],
93
- workflow.stages.findIndex((s) => s.id === stage.id),
101
+ [...srcStages, ...createdStages],
102
+ srcStages.findIndex((s) => s.id === stage.id),
94
103
  (targetStage) => {
95
104
  return !deleted.find((s) => s.id === targetStage.id);
96
105
  }
97
106
  );
98
107
 
99
108
  // Assign the new stage to entities that had the deleted stage
100
- await mapAsync(contentTypes, (contentTypeUID) => {
109
+ await mapAsync(contentTypesToMigrate, (contentTypeUID) => {
101
110
  this.updateEntitiesStage(contentTypeUID, {
102
111
  fromStageId: stage.id,
103
112
  toStageId: nearestStage.id,
@@ -108,9 +117,7 @@ module.exports = ({ strapi }) => {
108
117
  return this.delete(stage.id);
109
118
  });
110
119
 
111
- return workflowsService.update(workflowId, {
112
- stages: stagesIds,
113
- });
120
+ return destStages.map((stage) => ({ ...stage, id: stage.id ?? createdStagesIds.shift() }));
114
121
  });
115
122
  },
116
123
 
@@ -125,6 +132,8 @@ module.exports = ({ strapi }) => {
125
132
  async updateEntity(entityInfo, stageId) {
126
133
  const stage = await this.findById(stageId);
127
134
 
135
+ await workflowsValidationService.validateWorkflowCount();
136
+
128
137
  if (!stage) {
129
138
  throw new ApplicationError(`Selected stage does not exist`);
130
139
  }
@@ -140,43 +149,72 @@ module.exports = ({ strapi }) => {
140
149
  },
141
150
 
142
151
  /**
143
- * Updates the stage of all entities of a content type that are in a specific stage
152
+ * Updates entity stages of a content type:
153
+ * - If fromStageId is undefined, all entities with an existing stage will be assigned the new stage
154
+ * - If fromStageId is null, all entities without a stage will be assigned the new stage
155
+ * - If fromStageId is a number, all entities with that stage will be assigned the new stage
156
+ *
157
+ * For performance reasons we use knex queries directly.
158
+ *
144
159
  * @param {string} contentTypeUID
145
- * @param {number} fromStageId
160
+ * @param {number | undefined | null} fromStageId
146
161
  * @param {number} toStageId
147
162
  * @param {import('knex').Knex.Transaction} trx
148
163
  * @returns
149
164
  */
150
- async updateEntitiesStage(contentTypeUID, { fromStageId, toStageId, trx = null }) {
165
+ async updateEntitiesStage(contentTypeUID, { fromStageId, toStageId }) {
151
166
  const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
152
167
  const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
153
168
  const joinColumn = joinTable.joinColumn.name;
154
169
  const invJoinColumn = joinTable.inverseJoinColumn.name;
155
170
 
156
- const selectStatement = strapi.db
157
- .getConnection()
158
- .select({ [joinColumn]: 't1.id', [invJoinColumn]: toStageId })
159
- .from(`${tableName} as t1`)
160
- .leftJoin(`${joinTable.name} as t2`, `t1.id`, `t2.${joinColumn}`)
161
- .where(`t2.${invJoinColumn}`, fromStageId)
162
- .toSQL();
163
-
164
- // Insert rows for all entries of the content type that do not have a
165
- // default stage
166
- const query = strapi.db
167
- .getConnection(joinTable.name)
168
- .insert(
169
- strapi.db.connection.raw(
170
- `(${joinColumn}, ${invJoinColumn}) ${selectStatement.sql}`,
171
- selectStatement.bindings
171
+ await workflowsValidationService.validateWorkflowCount();
172
+
173
+ return strapi.db.transaction(async ({ trx }) => {
174
+ // Update all already existing links to the new stage
175
+ if (fromStageId === undefined) {
176
+ return strapi.db
177
+ .getConnection()
178
+ .from(joinTable.name)
179
+ .update({ [invJoinColumn]: toStageId })
180
+ .transacting(trx);
181
+ }
182
+
183
+ // Update all links from the specified stage to the new stage
184
+ const selectStatement = strapi.db
185
+ .getConnection()
186
+ .select({ [joinColumn]: 't1.id', [invJoinColumn]: toStageId })
187
+ .from(`${tableName} as t1`)
188
+ .leftJoin(`${joinTable.name} as t2`, `t1.id`, `t2.${joinColumn}`)
189
+ .where(`t2.${invJoinColumn}`, fromStageId)
190
+ .toSQL();
191
+
192
+ // Insert rows for all entries of the content type that have the specified stage
193
+ return strapi.db
194
+ .getConnection(joinTable.name)
195
+ .insert(
196
+ strapi.db.connection.raw(
197
+ `(${joinColumn}, ${invJoinColumn}) ${selectStatement.sql}`,
198
+ selectStatement.bindings
199
+ )
172
200
  )
173
- );
201
+ .transacting(trx);
202
+ });
203
+ },
174
204
 
175
- if (trx) {
176
- query.transacting(trx);
177
- }
205
+ /**
206
+ * Deletes all entity stages of a content type
207
+ * @param {string} contentTypeUID
208
+ * @returns
209
+ */
210
+ async deleteAllEntitiesStage(contentTypeUID) {
211
+ const { attributes } = strapi.db.metadata.get(contentTypeUID);
212
+ const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
178
213
 
179
- return query;
214
+ // Delete all stage links for the content type
215
+ return strapi.db.transaction(async ({ trx }) =>
216
+ strapi.db.getConnection().from(joinTable.name).delete().transacting(trx)
217
+ );
180
218
  },
181
219
  };
182
220
  };
@@ -231,13 +269,13 @@ function getDiffBetweenStages(sourceStages, comparisonStages) {
231
269
  * @param {Array} diffStages.deleted - An array of stages that are planned to be deleted from the workflow.
232
270
  * @param {Array} diffStages.created - An array of stages that are planned to be created in the workflow.
233
271
  *
234
- * @throws {ApplicationError} If the number of remaining stages in the workflow after applying deletions and additions is less than 1.
272
+ * @throws {ValidationError} If the number of remaining stages in the workflow after applying deletions and additions is less than 1.
235
273
  */
236
274
  function assertAtLeastOneStageRemain(workflowStages, diffStages) {
237
275
  const remainingStagesCount =
238
276
  workflowStages.length - diffStages.deleted.length + diffStages.created.length;
239
277
  if (remainingStagesCount < 1) {
240
- throw new ApplicationError('At least one stage must remain in the workflow.');
278
+ throw new ValidationError(ERRORS.WORKFLOW_WITHOUT_STAGES);
241
279
  }
242
280
  }
243
281
 
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const { ValidationError } = require('@strapi/utils').errors;
4
+ const { getService } = require('../../utils');
5
+ const { ERRORS, MAX_WORKFLOWS, MAX_STAGES_PER_WORKFLOW } = require('../../constants/workflows');
6
+ const { clampMaxWorkflows, clampMaxStagesPerWorkflow } = require('../../utils/review-workflows');
7
+
8
+ module.exports = ({ strapi }) => {
9
+ return {
10
+ limits: {
11
+ workflows: MAX_WORKFLOWS,
12
+ stagesPerWorkflow: MAX_STAGES_PER_WORKFLOW,
13
+ },
14
+ register({ workflows, stagesPerWorkflow }) {
15
+ if (!Object.isFrozen(this.limits)) {
16
+ this.limits.workflows = clampMaxWorkflows(workflows || this.limits.workflows);
17
+ this.limits.stagesPerWorkflow = clampMaxStagesPerWorkflow(
18
+ stagesPerWorkflow || this.limits.stagesPerWorkflow
19
+ );
20
+ Object.freeze(this.limits);
21
+ }
22
+ },
23
+ /**
24
+ * Validates the stages of a workflow.
25
+ * @param {Array} stages - Array of stages to be validated.
26
+ * @throws {ValidationError} - If the workflow has no stages or exceeds the limit.
27
+ */
28
+ validateWorkflowStages(stages) {
29
+ if (!stages || stages.length === 0) {
30
+ throw new ValidationError(ERRORS.WORKFLOW_WITHOUT_STAGES);
31
+ }
32
+ if (stages.length > this.limits.stagesPerWorkflow) {
33
+ throw new ValidationError(ERRORS.STAGES_LIMIT);
34
+ }
35
+ },
36
+
37
+ async validateWorkflowCountStages(workflowId, countAddedStages = 0) {
38
+ const stagesService = getService('stages', { strapi });
39
+ const countWorkflowStages = await stagesService.count({ workflowId });
40
+
41
+ if (countWorkflowStages + countAddedStages > this.limits.stagesPerWorkflow) {
42
+ throw new ValidationError(ERRORS.STAGES_LIMIT);
43
+ }
44
+ },
45
+
46
+ /**
47
+ * Validates the count of existing and added workflows.
48
+ * @param {number} [countAddedWorkflows=0] - The count of workflows to be added.
49
+ * @throws {ValidationError} - If the total count of workflows exceeds the limit.
50
+ * @returns {Promise<void>} - A Promise that resolves when the validation is completed.
51
+ */
52
+ async validateWorkflowCount(countAddedWorkflows = 0) {
53
+ const workflowsService = getService('workflows', { strapi });
54
+ const countWorkflows = await workflowsService.count();
55
+ if (countWorkflows + countAddedWorkflows > this.limits.workflows) {
56
+ throw new ValidationError(ERRORS.WORKFLOWS_LIMIT);
57
+ }
58
+ },
59
+ };
60
+ };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const { mapAsync } = require('@strapi/utils');
4
+ const { difference, merge } = require('lodash/fp');
5
+ const { getService } = require('../../../utils');
6
+ const { WORKFLOW_MODEL_UID } = require('../../../constants/workflows');
7
+
8
+ module.exports = ({ strapi }) => {
9
+ const contentManagerContentTypeService = strapi
10
+ .plugin('content-manager')
11
+ .service('content-types');
12
+ const stagesService = getService('stages', { strapi });
13
+
14
+ const updateContentTypeConfig = async (uid, reviewWorkflowOption) => {
15
+ // Merge options in the configuration as the configuration service use a destructuration merge which doesn't include nested objects
16
+ const modelConfig = await contentManagerContentTypeService.findConfiguration(uid);
17
+
18
+ await contentManagerContentTypeService.updateConfiguration(
19
+ { uid },
20
+ { options: merge(modelConfig.options, { reviewWorkflows: reviewWorkflowOption }) }
21
+ );
22
+ };
23
+
24
+ return {
25
+ /**
26
+ * Migrates entities stages. Used when a content type is assigned to a workflow.
27
+ * @param {*} options
28
+ * @param {Array<string>} options.srcContentTypes - The content types assigned to the previous workflow
29
+ * @param {Array<string>} options.destContentTypes - The content types assigned to the new workflow
30
+ * @param {Workflow.Stage} options.stageId - The new stage to assign the entities to
31
+ */
32
+ async migrate({ srcContentTypes = [], destContentTypes, stageId }) {
33
+ // Workflows service is using this content-types service, to avoid an infinite loop, we need to get the service in the method
34
+ const workflowsService = getService('workflows', { strapi });
35
+ const { created, deleted } = diffContentTypes(srcContentTypes, destContentTypes);
36
+
37
+ await mapAsync(created, async (uid) => {
38
+ // If it was assigned to another workflow, transfer it from the previous workflow
39
+ const srcWorkflow = await workflowsService.getAssignedWorkflow(uid);
40
+ if (srcWorkflow) {
41
+ // Updates all existing entities stages links to the new stage
42
+ await stagesService.updateEntitiesStage(uid, { toStageId: stageId });
43
+ return this.transferContentType(srcWorkflow, uid);
44
+ }
45
+ await updateContentTypeConfig(uid, true);
46
+
47
+ // Create new stages links to the new stage
48
+ return stagesService.updateEntitiesStage(uid, {
49
+ fromStageId: null,
50
+ toStageId: stageId,
51
+ });
52
+ });
53
+
54
+ await mapAsync(deleted, async (uid) => {
55
+ await updateContentTypeConfig(uid, false);
56
+ await stagesService.deleteAllEntitiesStage(uid, {});
57
+ });
58
+ },
59
+
60
+ /**
61
+ * Filters the content types assigned to the previous workflow.
62
+ * @param {Workflow} srcWorkflow - The workflow to transfer from
63
+ * @param {string} uid - The content type uid
64
+ */
65
+ async transferContentType(srcWorkflow, uid) {
66
+ // Update assignedContentTypes of the previous workflow
67
+ await strapi.entityService.update(WORKFLOW_MODEL_UID, srcWorkflow.id, {
68
+ data: {
69
+ contentTypes: srcWorkflow.contentTypes.filter((ct) => ct !== uid),
70
+ },
71
+ });
72
+ },
73
+ };
74
+ };
75
+
76
+ const diffContentTypes = (srcContentTypes, destContentTypes) => {
77
+ const created = difference(destContentTypes, srcContentTypes);
78
+ const deleted = difference(srcContentTypes, destContentTypes);
79
+ return { created, deleted };
80
+ };
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+
3
+ const { set, isString, map, get } = require('lodash/fp');
4
+ const { ApplicationError } = require('@strapi/utils').errors;
5
+ const { WORKFLOW_MODEL_UID } = require('../../../constants/workflows');
6
+ const { getService } = require('../../../utils');
7
+ const { getWorkflowContentTypeFilter } = require('../../../utils/review-workflows');
8
+ const workflowsContentTypesFactory = require('./content-types');
9
+
10
+ const processFilters = ({ strapi }, filters = {}) => {
11
+ const processedFilters = { ...filters };
12
+
13
+ if (isString(filters.contentTypes)) {
14
+ processedFilters.contentTypes = getWorkflowContentTypeFilter({ strapi }, filters.contentTypes);
15
+ }
16
+
17
+ return processedFilters;
18
+ };
19
+
20
+ module.exports = ({ strapi }) => {
21
+ const workflowsContentTypes = workflowsContentTypesFactory({ strapi });
22
+ const workflowsValidationService = getService('review-workflows-validation', { strapi });
23
+
24
+ return {
25
+ /**
26
+ * Returns all the workflows matching the user-defined filters.
27
+ * @param {object} opts - Options for the query.
28
+ * @param {object} opts.filters - Filters object.
29
+ * @returns {Promise<object[]>} - List of workflows that match the user's filters.
30
+ */
31
+ async find(opts = {}) {
32
+ const filters = processFilters({ strapi }, opts.filters);
33
+ return strapi.entityService.findMany(WORKFLOW_MODEL_UID, { ...opts, filters });
34
+ },
35
+
36
+ /**
37
+ * Returns the workflow with the specified ID.
38
+ * @param {string} id - ID of the requested workflow.
39
+ * @param {object} opts - Options for the query.
40
+ * @returns {Promise<object>} - Workflow object matching the requested ID.
41
+ */
42
+ findById(id, opts) {
43
+ return strapi.entityService.findOne(WORKFLOW_MODEL_UID, id, opts);
44
+ },
45
+
46
+ /**
47
+ * Creates a new workflow.
48
+ * @param {object} opts - Options for creating the new workflow.
49
+ * @returns {Promise<object>} - Workflow object that was just created.
50
+ * @throws {ValidationError} - If the workflow has no stages.
51
+ */
52
+ async create(opts) {
53
+ let createOpts = { ...opts, populate: { stages: true } };
54
+
55
+ workflowsValidationService.validateWorkflowStages(opts.data.stages);
56
+ await workflowsValidationService.validateWorkflowCount(1);
57
+
58
+ return strapi.db.transaction(async () => {
59
+ // Create stages
60
+ const stages = await getService('stages', { strapi }).createMany(opts.data.stages);
61
+ const mapIds = map(get('id'));
62
+
63
+ createOpts = set('data.stages', mapIds(stages), createOpts);
64
+
65
+ // Update (un)assigned Content Types
66
+ if (opts.data.contentTypes) {
67
+ await workflowsContentTypes.migrate({
68
+ destContentTypes: opts.data.contentTypes,
69
+ stageId: stages[0].id,
70
+ });
71
+ }
72
+
73
+ // Create Workflow
74
+ return strapi.entityService.create(WORKFLOW_MODEL_UID, createOpts);
75
+ });
76
+ },
77
+
78
+ /**
79
+ * Updates an existing workflow.
80
+ * @param {object} workflow - The existing workflow to update.
81
+ * @param {object} opts - Options for updating the workflow.
82
+ * @returns {Promise<object>} - Workflow object that was just updated.
83
+ * @throws {ApplicationError} - If the supplied stage ID does not belong to the workflow.
84
+ */
85
+ async update(workflow, opts) {
86
+ const stageService = getService('stages', { strapi });
87
+ let updateOpts = { ...opts, populate: { stages: true } };
88
+ let updatedStageIds;
89
+
90
+ await workflowsValidationService.validateWorkflowCount();
91
+
92
+ return strapi.db.transaction(async () => {
93
+ // Update stages
94
+ if (opts.data.stages) {
95
+ workflowsValidationService.validateWorkflowStages(opts.data.stages);
96
+ opts.data.stages.forEach((stage) =>
97
+ this.assertStageBelongsToWorkflow(stage.id, workflow)
98
+ );
99
+
100
+ updatedStageIds = await stageService
101
+ .replaceStages(workflow.stages, opts.data.stages, workflow.contentTypes)
102
+ .then((stages) => stages.map((stage) => stage.id));
103
+
104
+ updateOpts = set('data.stages', updatedStageIds, opts);
105
+ }
106
+
107
+ // Update (un)assigned Content Types
108
+ if (opts.data.contentTypes) {
109
+ await workflowsContentTypes.migrate({
110
+ srcContentTypes: workflow.contentTypes,
111
+ destContentTypes: opts.data.contentTypes,
112
+ stageId: updatedStageIds ? updatedStageIds[0] : workflow.stages[0].id,
113
+ });
114
+ }
115
+
116
+ // Update Workflow
117
+ return strapi.entityService.update(WORKFLOW_MODEL_UID, workflow.id, updateOpts);
118
+ });
119
+ },
120
+
121
+ /**
122
+ * Deletes an existing workflow.
123
+ * Also deletes all the workflow stages and migrate all assigned the content types.
124
+ * @param {*} workflow
125
+ * @param {*} opts
126
+ * @returns
127
+ */
128
+ async delete(workflow, opts) {
129
+ const stageService = getService('stages', { strapi });
130
+
131
+ const workflowCount = await this.count();
132
+
133
+ if (workflowCount <= 1) {
134
+ throw new ApplicationError('Can not delete the last workflow');
135
+ }
136
+
137
+ return strapi.db.transaction(async () => {
138
+ // Delete stages
139
+ await stageService.deleteMany(workflow.stages.map((stage) => stage.id));
140
+
141
+ // Unassign all content types, this will migrate the content types to null
142
+ await workflowsContentTypes.migrate({
143
+ srcContentTypes: workflow.contentTypes,
144
+ destContentTypes: [],
145
+ });
146
+
147
+ // Delete Workflow
148
+ return strapi.entityService.delete(WORKFLOW_MODEL_UID, workflow.id, opts);
149
+ });
150
+ },
151
+ /**
152
+ * Returns the total count of workflows.
153
+ * @returns {Promise<number>} - Total count of workflows.
154
+ */
155
+ count() {
156
+ return strapi.entityService.count(WORKFLOW_MODEL_UID);
157
+ },
158
+
159
+ /**
160
+ * Finds the assigned workflow for a given content type ID.
161
+ * @param {string} uid - Content type ID to find the assigned workflow for.
162
+ * @param {object} opts - Options for the query.
163
+ * @returns {Promise<object|null>} - Assigned workflow object if found, or null.
164
+ */
165
+ async getAssignedWorkflow(uid, opts = {}) {
166
+ const workflows = await this.find({
167
+ ...opts,
168
+ filters: { contentTypes: getWorkflowContentTypeFilter({ strapi }, uid) },
169
+ });
170
+ return workflows.length > 0 ? workflows[0] : null;
171
+ },
172
+
173
+ /**
174
+ * Asserts that a content type has an assigned workflow.
175
+ * @param {string} uid - Content type ID to verify the assignment of.
176
+ * @returns {Promise<object>} - Workflow object associated with the content type ID.
177
+ * @throws {ApplicationError} - If no assigned workflow is found for the content type ID.
178
+ */
179
+ async assertContentTypeBelongsToWorkflow(uid) {
180
+ const workflow = await this.getAssignedWorkflow(uid, {
181
+ populate: 'stages',
182
+ });
183
+ if (!workflow) {
184
+ throw new ApplicationError(`Review workflows is not activated on Content Type ${uid}.`);
185
+ }
186
+ return workflow;
187
+ },
188
+
189
+ /**
190
+ * Asserts that a stage belongs to a given workflow.
191
+ * @param {string} stageId - ID of stage to check.
192
+ * @param {object} workflow - Workflow object to check against.
193
+ * @returns
194
+ * @throws {ApplicationError} - If the stage does not belong to the specified workflow.
195
+ */
196
+ assertStageBelongsToWorkflow(stageId, workflow) {
197
+ if (!stageId) {
198
+ return;
199
+ }
200
+
201
+ const belongs = workflow.stages.some((stage) => stage.id === stageId);
202
+ if (!belongs) {
203
+ throw new ApplicationError(`Stage does not belong to workflow "${workflow.name}"`);
204
+ }
205
+ },
206
+ };
207
+ };
@@ -1,34 +1,39 @@
1
1
  'use strict';
2
2
 
3
- const { get, keys, pickBy, pipe } = require('lodash/fp');
4
- const { WORKFLOW_MODEL_UID } = require('../constants/workflows');
3
+ const { getOr, keys, pickBy, pipe, has, clamp } = require('lodash/fp');
4
+ const {
5
+ ENTITY_STAGE_ATTRIBUTE,
6
+ MAX_WORKFLOWS,
7
+ MAX_STAGES_PER_WORKFLOW,
8
+ } = require('../constants/workflows');
5
9
 
6
- /**
7
- * Checks if a content type has review workflows enabled.
8
- * @param {string|Object} contentType - Either the modelUID of the content type, or the content type object.
9
- * @returns {boolean} Whether review workflows are enabled for the specified content type.
10
- */
11
- function hasReviewWorkflow({ strapi }, contentType) {
12
- if (typeof contentType === 'string') {
13
- // If the input is a string, assume it's the modelUID of the content type and retrieve the corresponding object.
14
- return hasReviewWorkflow({ strapi }, strapi.getModel(contentType));
15
- }
16
- // Otherwise, assume it's the content type object itself and return its `reviewWorkflows` option if it exists.
17
- return contentType?.options?.reviewWorkflows || false;
18
- }
19
- // TODO To be refactored when multiple workflows are added
20
- const getDefaultWorkflow = async ({ strapi }) =>
21
- strapi.query(WORKFLOW_MODEL_UID).findOne({ populate: ['stages'] });
22
-
23
- const getContentTypeUIDsWithActivatedReviewWorkflows = pipe([
24
- // Pick only content-types with reviewWorkflows options set to true
25
- pickBy(get('options.reviewWorkflows')),
10
+ const getVisibleContentTypesUID = pipe([
11
+ // Pick only content-types visible in the content-manager and option is not false
12
+ pickBy(
13
+ (value) =>
14
+ getOr(true, 'pluginOptions.content-manager.visible', value) &&
15
+ !getOr(false, 'options.noStageAttribute', value)
16
+ ),
26
17
  // Get UIDs
27
18
  keys,
28
19
  ]);
29
20
 
21
+ const hasStageAttribute = has(['attributes', ENTITY_STAGE_ATTRIBUTE]);
22
+
23
+ const getWorkflowContentTypeFilter = ({ strapi }, contentType) => {
24
+ if (strapi.db.dialect.supportsOperator('$jsonSupersetOf')) {
25
+ return { $jsonSupersetOf: JSON.stringify([contentType]) };
26
+ }
27
+ return { $contains: `"${contentType}"` };
28
+ };
29
+
30
+ const clampMaxWorkflows = clamp(1, MAX_WORKFLOWS);
31
+ const clampMaxStagesPerWorkflow = clamp(1, MAX_STAGES_PER_WORKFLOW);
32
+
30
33
  module.exports = {
31
- hasReviewWorkflow,
32
- getDefaultWorkflow,
33
- getContentTypeUIDsWithActivatedReviewWorkflows,
34
+ clampMaxWorkflows,
35
+ clampMaxStagesPerWorkflow,
36
+ getVisibleContentTypesUID,
37
+ hasStageAttribute,
38
+ getWorkflowContentTypeFilter,
34
39
  };