@strapi/review-workflows 5.12.1 → 5.12.3

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 (327) hide show
  1. package/dist/admin/{chunks/index-DcEF47R4.mjs → assets/balloon.png.js} +3 -1078
  2. package/dist/admin/assets/balloon.png.js.map +1 -0
  3. package/dist/admin/{chunks/index-CzdEqFOm.js → assets/balloon.png.mjs} +2 -1113
  4. package/dist/admin/assets/balloon.png.mjs.map +1 -0
  5. package/dist/admin/assets/purchase-page-illustration-dark.svg.js +6 -0
  6. package/dist/admin/assets/purchase-page-illustration-dark.svg.js.map +1 -0
  7. package/dist/admin/assets/purchase-page-illustration-dark.svg.mjs +4 -0
  8. package/dist/admin/assets/purchase-page-illustration-dark.svg.mjs.map +1 -0
  9. package/dist/admin/{chunks/purchase-review-workflows-4n0KXAeo.mjs → assets/purchase-page-illustration-light.svg.js} +3 -197
  10. package/dist/admin/assets/purchase-page-illustration-light.svg.js.map +1 -0
  11. package/dist/admin/{chunks/purchase-review-workflows-BDLncDcz.js → assets/purchase-page-illustration-light.svg.mjs} +2 -200
  12. package/dist/admin/assets/purchase-page-illustration-light.svg.mjs.map +1 -0
  13. package/dist/admin/components/LimitsModal.js +122 -0
  14. package/dist/admin/components/LimitsModal.js.map +1 -0
  15. package/dist/admin/components/LimitsModal.mjs +120 -0
  16. package/dist/admin/components/LimitsModal.mjs.map +1 -0
  17. package/dist/admin/constants.js +18 -0
  18. package/dist/admin/constants.js.map +1 -0
  19. package/dist/admin/constants.mjs +12 -0
  20. package/dist/admin/constants.mjs.map +1 -0
  21. package/dist/admin/index.js +87 -13
  22. package/dist/admin/index.js.map +1 -1
  23. package/dist/admin/index.mjs +88 -12
  24. package/dist/admin/index.mjs.map +1 -1
  25. package/dist/admin/modules/hooks.js +8 -0
  26. package/dist/admin/modules/hooks.js.map +1 -0
  27. package/dist/admin/modules/hooks.mjs +6 -0
  28. package/dist/admin/modules/hooks.mjs.map +1 -0
  29. package/dist/admin/{chunks/router-ChVwf8TN.js → router.js} +3 -3
  30. package/dist/admin/router.js.map +1 -0
  31. package/dist/admin/{chunks/router-D-YCUzYy.mjs → router.mjs} +3 -3
  32. package/dist/admin/router.mjs.map +1 -0
  33. package/dist/admin/routes/content-manager/model/components/TableColumns.js +44 -0
  34. package/dist/admin/routes/content-manager/model/components/TableColumns.js.map +1 -0
  35. package/dist/admin/routes/content-manager/model/components/TableColumns.mjs +41 -0
  36. package/dist/admin/routes/content-manager/model/components/TableColumns.mjs.map +1 -0
  37. package/dist/admin/routes/content-manager/model/constants.js +60 -0
  38. package/dist/admin/routes/content-manager/model/constants.js.map +1 -0
  39. package/dist/admin/routes/content-manager/model/constants.mjs +58 -0
  40. package/dist/admin/routes/content-manager/model/constants.mjs.map +1 -0
  41. package/dist/admin/routes/content-manager/model/id/components/AssigneeSelect.js +169 -0
  42. package/dist/admin/routes/content-manager/model/id/components/AssigneeSelect.js.map +1 -0
  43. package/dist/admin/routes/content-manager/model/id/components/AssigneeSelect.mjs +148 -0
  44. package/dist/admin/routes/content-manager/model/id/components/AssigneeSelect.mjs.map +1 -0
  45. package/dist/admin/routes/content-manager/model/id/components/Header.js +31 -0
  46. package/dist/admin/routes/content-manager/model/id/components/Header.js.map +1 -0
  47. package/dist/admin/routes/content-manager/model/id/components/Header.mjs +29 -0
  48. package/dist/admin/routes/content-manager/model/id/components/Header.mjs.map +1 -0
  49. package/dist/admin/routes/content-manager/model/id/components/Panel.js +39 -0
  50. package/dist/admin/routes/content-manager/model/id/components/Panel.js.map +1 -0
  51. package/dist/admin/routes/content-manager/model/id/components/Panel.mjs +37 -0
  52. package/dist/admin/routes/content-manager/model/id/components/Panel.mjs.map +1 -0
  53. package/dist/admin/routes/content-manager/model/id/components/StageSelect.js +329 -0
  54. package/dist/admin/routes/content-manager/model/id/components/StageSelect.js.map +1 -0
  55. package/dist/admin/routes/content-manager/model/id/components/StageSelect.mjs +308 -0
  56. package/dist/admin/routes/content-manager/model/id/components/StageSelect.mjs.map +1 -0
  57. package/dist/admin/routes/content-manager/model/id/components/constants.js +8 -0
  58. package/dist/admin/routes/content-manager/model/id/components/constants.js.map +1 -0
  59. package/dist/admin/routes/content-manager/model/id/components/constants.mjs +5 -0
  60. package/dist/admin/routes/content-manager/model/id/components/constants.mjs.map +1 -0
  61. package/dist/admin/routes/purchase-review-workflows.js +194 -0
  62. package/dist/admin/routes/purchase-review-workflows.js.map +1 -0
  63. package/dist/admin/routes/purchase-review-workflows.mjs +192 -0
  64. package/dist/admin/routes/purchase-review-workflows.mjs.map +1 -0
  65. package/dist/admin/routes/settings/components/AddStage.js +51 -0
  66. package/dist/admin/routes/settings/components/AddStage.js.map +1 -0
  67. package/dist/admin/routes/settings/components/AddStage.mjs +49 -0
  68. package/dist/admin/routes/settings/components/AddStage.mjs.map +1 -0
  69. package/dist/admin/routes/settings/components/Layout.js +86 -0
  70. package/dist/admin/routes/settings/components/Layout.js.map +1 -0
  71. package/dist/admin/routes/settings/components/Layout.mjs +82 -0
  72. package/dist/admin/routes/settings/components/Layout.mjs.map +1 -0
  73. package/dist/admin/routes/settings/components/StageDragPreview.js +40 -0
  74. package/dist/admin/routes/settings/components/StageDragPreview.js.map +1 -0
  75. package/dist/admin/routes/settings/components/StageDragPreview.mjs +38 -0
  76. package/dist/admin/routes/settings/components/StageDragPreview.mjs.map +1 -0
  77. package/dist/admin/routes/settings/components/Stages.js +593 -0
  78. package/dist/admin/routes/settings/components/Stages.js.map +1 -0
  79. package/dist/admin/routes/settings/components/Stages.mjs +572 -0
  80. package/dist/admin/routes/settings/components/Stages.mjs.map +1 -0
  81. package/dist/admin/routes/settings/components/WorkflowAttributes.js +203 -0
  82. package/dist/admin/routes/settings/components/WorkflowAttributes.js.map +1 -0
  83. package/dist/admin/routes/settings/components/WorkflowAttributes.mjs +201 -0
  84. package/dist/admin/routes/settings/components/WorkflowAttributes.mjs.map +1 -0
  85. package/dist/admin/routes/settings/constants.js +8 -0
  86. package/dist/admin/routes/settings/constants.js.map +1 -0
  87. package/dist/admin/routes/settings/constants.mjs +6 -0
  88. package/dist/admin/routes/settings/constants.mjs.map +1 -0
  89. package/dist/admin/routes/settings/hooks/useDragAndDrop.js +193 -0
  90. package/dist/admin/routes/settings/hooks/useDragAndDrop.js.map +1 -0
  91. package/dist/admin/routes/settings/hooks/useDragAndDrop.mjs +170 -0
  92. package/dist/admin/routes/settings/hooks/useDragAndDrop.mjs.map +1 -0
  93. package/dist/admin/routes/settings/hooks/useKeyboardDragAndDrop.js +94 -0
  94. package/dist/admin/routes/settings/hooks/useKeyboardDragAndDrop.js.map +1 -0
  95. package/dist/admin/routes/settings/hooks/useKeyboardDragAndDrop.mjs +73 -0
  96. package/dist/admin/routes/settings/hooks/useKeyboardDragAndDrop.mjs.map +1 -0
  97. package/dist/admin/{chunks/Layout-C4ri_ldC.js → routes/settings/hooks/useReviewWorkflows.js} +6 -121
  98. package/dist/admin/routes/settings/hooks/useReviewWorkflows.js.map +1 -0
  99. package/dist/admin/{chunks/Layout-CF497D6H.mjs → routes/settings/hooks/useReviewWorkflows.mjs} +4 -115
  100. package/dist/admin/routes/settings/hooks/useReviewWorkflows.mjs.map +1 -0
  101. package/dist/admin/routes/settings/id.js +404 -0
  102. package/dist/admin/routes/settings/id.js.map +1 -0
  103. package/dist/admin/routes/settings/id.mjs +382 -0
  104. package/dist/admin/routes/settings/id.mjs.map +1 -0
  105. package/dist/admin/{chunks/index-CCx4kT-t.js → routes/settings/index.js} +15 -15
  106. package/dist/admin/routes/settings/index.js.map +1 -0
  107. package/dist/admin/{chunks/index-iChY7MsG.mjs → routes/settings/index.mjs} +7 -7
  108. package/dist/admin/routes/settings/index.mjs.map +1 -0
  109. package/dist/admin/services/admin.js +23 -0
  110. package/dist/admin/services/admin.js.map +1 -0
  111. package/dist/admin/services/admin.mjs +21 -0
  112. package/dist/admin/services/admin.mjs.map +1 -0
  113. package/dist/admin/services/api.js +15 -0
  114. package/dist/admin/services/api.js.map +1 -0
  115. package/dist/admin/services/api.mjs +13 -0
  116. package/dist/admin/services/api.mjs.map +1 -0
  117. package/dist/admin/services/content-manager.js +101 -0
  118. package/dist/admin/services/content-manager.js.map +1 -0
  119. package/dist/admin/services/content-manager.mjs +96 -0
  120. package/dist/admin/services/content-manager.mjs.map +1 -0
  121. package/dist/admin/services/settings.js +123 -0
  122. package/dist/admin/services/settings.js.map +1 -0
  123. package/dist/admin/services/settings.mjs +118 -0
  124. package/dist/admin/services/settings.mjs.map +1 -0
  125. package/dist/admin/{chunks/en-BNGiWajd.js → translations/en.json.js} +2 -2
  126. package/dist/admin/translations/en.json.js.map +1 -0
  127. package/dist/admin/{chunks/en-BrZXFtVv.mjs → translations/en.json.mjs} +1 -1
  128. package/dist/admin/translations/en.json.mjs.map +1 -0
  129. package/dist/admin/{chunks/uk-CbRUr1I7.js → translations/uk.json.js} +2 -2
  130. package/dist/admin/translations/uk.json.js.map +1 -0
  131. package/dist/admin/{chunks/uk-DLlzEBUF.mjs → translations/uk.json.mjs} +1 -1
  132. package/dist/admin/translations/uk.json.mjs.map +1 -0
  133. package/dist/admin/utils/api.js +22 -0
  134. package/dist/admin/utils/api.js.map +1 -0
  135. package/dist/admin/utils/api.mjs +19 -0
  136. package/dist/admin/utils/api.mjs.map +1 -0
  137. package/dist/admin/utils/cm-hooks.js +23 -0
  138. package/dist/admin/utils/cm-hooks.js.map +1 -0
  139. package/dist/admin/utils/cm-hooks.mjs +21 -0
  140. package/dist/admin/utils/cm-hooks.mjs.map +1 -0
  141. package/dist/admin/utils/colors.js +50 -0
  142. package/dist/admin/utils/colors.js.map +1 -0
  143. package/dist/admin/utils/colors.mjs +47 -0
  144. package/dist/admin/utils/colors.mjs.map +1 -0
  145. package/dist/admin/utils/translations.js +11 -0
  146. package/dist/admin/utils/translations.js.map +1 -0
  147. package/dist/admin/utils/translations.mjs +9 -0
  148. package/dist/admin/utils/translations.mjs.map +1 -0
  149. package/dist/admin/utils/users.js +17 -0
  150. package/dist/admin/utils/users.js.map +1 -0
  151. package/dist/admin/utils/users.mjs +15 -0
  152. package/dist/admin/utils/users.mjs.map +1 -0
  153. package/dist/server/bootstrap.js +54 -0
  154. package/dist/server/bootstrap.js.map +1 -0
  155. package/dist/server/bootstrap.mjs +52 -0
  156. package/dist/server/bootstrap.mjs.map +1 -0
  157. package/dist/server/config/actions.js +47 -0
  158. package/dist/server/config/actions.js.map +1 -0
  159. package/dist/server/config/actions.mjs +45 -0
  160. package/dist/server/config/actions.mjs.map +1 -0
  161. package/dist/server/constants/default-stages.json.js +23 -0
  162. package/dist/server/constants/default-stages.json.js.map +1 -0
  163. package/dist/server/constants/default-stages.json.mjs +21 -0
  164. package/dist/server/constants/default-stages.json.mjs.map +1 -0
  165. package/dist/server/constants/default-workflow.json.js +12 -0
  166. package/dist/server/constants/default-workflow.json.js.map +1 -0
  167. package/dist/server/constants/default-workflow.json.mjs +7 -0
  168. package/dist/server/constants/default-workflow.json.mjs.map +1 -0
  169. package/dist/server/constants/webhook-events.js +12 -0
  170. package/dist/server/constants/webhook-events.js.map +1 -0
  171. package/dist/server/constants/webhook-events.mjs +7 -0
  172. package/dist/server/constants/webhook-events.mjs.map +1 -0
  173. package/dist/server/constants/workflows.js +53 -0
  174. package/dist/server/constants/workflows.js.map +1 -0
  175. package/dist/server/constants/workflows.mjs +42 -0
  176. package/dist/server/constants/workflows.mjs.map +1 -0
  177. package/dist/server/content-types/index.js +12 -0
  178. package/dist/server/content-types/index.js.map +1 -0
  179. package/dist/server/content-types/index.mjs +10 -0
  180. package/dist/server/content-types/index.mjs.map +1 -0
  181. package/dist/server/content-types/workflow/index.js +50 -0
  182. package/dist/server/content-types/workflow/index.js.map +1 -0
  183. package/dist/server/content-types/workflow/index.mjs +48 -0
  184. package/dist/server/content-types/workflow/index.mjs.map +1 -0
  185. package/dist/server/content-types/workflow-stage/index.js +54 -0
  186. package/dist/server/content-types/workflow-stage/index.js.map +1 -0
  187. package/dist/server/content-types/workflow-stage/index.mjs +52 -0
  188. package/dist/server/content-types/workflow-stage/index.mjs.map +1 -0
  189. package/dist/server/controllers/assignees.js +57 -0
  190. package/dist/server/controllers/assignees.js.map +1 -0
  191. package/dist/server/controllers/assignees.mjs +55 -0
  192. package/dist/server/controllers/assignees.mjs.map +1 -0
  193. package/dist/server/controllers/index.js +14 -0
  194. package/dist/server/controllers/index.js.map +1 -0
  195. package/dist/server/controllers/index.mjs +12 -0
  196. package/dist/server/controllers/index.mjs.map +1 -0
  197. package/dist/server/controllers/stages.js +167 -0
  198. package/dist/server/controllers/stages.js.map +1 -0
  199. package/dist/server/controllers/stages.mjs +165 -0
  200. package/dist/server/controllers/stages.mjs.map +1 -0
  201. package/dist/server/controllers/workflows.js +136 -0
  202. package/dist/server/controllers/workflows.js.map +1 -0
  203. package/dist/server/controllers/workflows.mjs +134 -0
  204. package/dist/server/controllers/workflows.mjs.map +1 -0
  205. package/dist/server/destroy.js +6 -0
  206. package/dist/server/destroy.js.map +1 -0
  207. package/dist/server/destroy.mjs +4 -0
  208. package/dist/server/destroy.mjs.map +1 -0
  209. package/dist/server/index.js +12 -2333
  210. package/dist/server/index.js.map +1 -1
  211. package/dist/server/index.mjs +7 -2328
  212. package/dist/server/index.mjs.map +1 -1
  213. package/dist/server/middlewares/review-workflows.js +42 -0
  214. package/dist/server/middlewares/review-workflows.js.map +1 -0
  215. package/dist/server/middlewares/review-workflows.mjs +37 -0
  216. package/dist/server/middlewares/review-workflows.mjs.map +1 -0
  217. package/dist/server/migrations/handle-deleted-ct-in-workflows.js +40 -0
  218. package/dist/server/migrations/handle-deleted-ct-in-workflows.js.map +1 -0
  219. package/dist/server/migrations/handle-deleted-ct-in-workflows.mjs +38 -0
  220. package/dist/server/migrations/handle-deleted-ct-in-workflows.mjs.map +1 -0
  221. package/dist/server/migrations/multiple-workflows.js +41 -0
  222. package/dist/server/migrations/multiple-workflows.js.map +1 -0
  223. package/dist/server/migrations/multiple-workflows.mjs +39 -0
  224. package/dist/server/migrations/multiple-workflows.mjs.map +1 -0
  225. package/dist/server/migrations/set-stages-default-color.js +22 -0
  226. package/dist/server/migrations/set-stages-default-color.js.map +1 -0
  227. package/dist/server/migrations/set-stages-default-color.mjs +20 -0
  228. package/dist/server/migrations/set-stages-default-color.mjs.map +1 -0
  229. package/dist/server/migrations/set-stages-roles.js +56 -0
  230. package/dist/server/migrations/set-stages-roles.js.map +1 -0
  231. package/dist/server/migrations/set-stages-roles.mjs +54 -0
  232. package/dist/server/migrations/set-stages-roles.mjs.map +1 -0
  233. package/dist/server/migrations/set-workflow-default-name.js +29 -0
  234. package/dist/server/migrations/set-workflow-default-name.js.map +1 -0
  235. package/dist/server/migrations/set-workflow-default-name.mjs +27 -0
  236. package/dist/server/migrations/set-workflow-default-name.mjs.map +1 -0
  237. package/dist/server/migrations/shorten-stage-attribute.js +45 -0
  238. package/dist/server/migrations/shorten-stage-attribute.js.map +1 -0
  239. package/dist/server/migrations/shorten-stage-attribute.mjs +43 -0
  240. package/dist/server/migrations/shorten-stage-attribute.mjs.map +1 -0
  241. package/dist/server/register.js +116 -0
  242. package/dist/server/register.js.map +1 -0
  243. package/dist/server/register.mjs +114 -0
  244. package/dist/server/register.mjs.map +1 -0
  245. package/dist/server/routes/index.js +10 -0
  246. package/dist/server/routes/index.js.map +1 -0
  247. package/dist/server/routes/index.mjs +8 -0
  248. package/dist/server/routes/index.mjs.map +1 -0
  249. package/dist/server/routes/review-workflows.js +186 -0
  250. package/dist/server/routes/review-workflows.js.map +1 -0
  251. package/dist/server/routes/review-workflows.mjs +184 -0
  252. package/dist/server/routes/review-workflows.mjs.map +1 -0
  253. package/dist/server/routes/utils.js +11 -0
  254. package/dist/server/routes/utils.js.map +1 -0
  255. package/dist/server/routes/utils.mjs +9 -0
  256. package/dist/server/routes/utils.mjs.map +1 -0
  257. package/dist/server/services/assignees.js +68 -0
  258. package/dist/server/services/assignees.js.map +1 -0
  259. package/dist/server/services/assignees.mjs +66 -0
  260. package/dist/server/services/assignees.mjs.map +1 -0
  261. package/dist/server/services/document-service-middleware.js +130 -0
  262. package/dist/server/services/document-service-middleware.js.map +1 -0
  263. package/dist/server/services/document-service-middleware.mjs +128 -0
  264. package/dist/server/services/document-service-middleware.mjs.map +1 -0
  265. package/dist/server/services/index.js +24 -0
  266. package/dist/server/services/index.js.map +1 -0
  267. package/dist/server/services/index.mjs +22 -0
  268. package/dist/server/services/index.mjs.map +1 -0
  269. package/dist/server/services/metrics/index.js +67 -0
  270. package/dist/server/services/metrics/index.js.map +1 -0
  271. package/dist/server/services/metrics/index.mjs +55 -0
  272. package/dist/server/services/metrics/index.mjs.map +1 -0
  273. package/dist/server/services/metrics/weekly-metrics.js +84 -0
  274. package/dist/server/services/metrics/weekly-metrics.js.map +1 -0
  275. package/dist/server/services/metrics/weekly-metrics.mjs +82 -0
  276. package/dist/server/services/metrics/weekly-metrics.mjs.map +1 -0
  277. package/dist/server/services/stage-permissions.js +59 -0
  278. package/dist/server/services/stage-permissions.js.map +1 -0
  279. package/dist/server/services/stage-permissions.mjs +57 -0
  280. package/dist/server/services/stage-permissions.mjs.map +1 -0
  281. package/dist/server/services/stages.js +353 -0
  282. package/dist/server/services/stages.js.map +1 -0
  283. package/dist/server/services/stages.mjs +351 -0
  284. package/dist/server/services/stages.mjs.map +1 -0
  285. package/dist/server/services/validation.js +69 -0
  286. package/dist/server/services/validation.js.map +1 -0
  287. package/dist/server/services/validation.mjs +67 -0
  288. package/dist/server/services/validation.mjs.map +1 -0
  289. package/dist/server/services/workflow-content-types.js +90 -0
  290. package/dist/server/services/workflow-content-types.js.map +1 -0
  291. package/dist/server/services/workflow-content-types.mjs +88 -0
  292. package/dist/server/services/workflow-content-types.mjs.map +1 -0
  293. package/dist/server/services/workflows.js +279 -0
  294. package/dist/server/services/workflows.js.map +1 -0
  295. package/dist/server/services/workflows.mjs +277 -0
  296. package/dist/server/services/workflows.mjs.map +1 -0
  297. package/dist/server/utils/index.js +16 -0
  298. package/dist/server/utils/index.js.map +1 -0
  299. package/dist/server/utils/index.mjs +13 -0
  300. package/dist/server/utils/index.mjs.map +1 -0
  301. package/dist/server/utils/review-workflows.js +36 -0
  302. package/dist/server/utils/review-workflows.js.map +1 -0
  303. package/dist/server/utils/review-workflows.mjs +30 -0
  304. package/dist/server/utils/review-workflows.mjs.map +1 -0
  305. package/dist/server/validation/review-workflows.js +71 -0
  306. package/dist/server/validation/review-workflows.js.map +1 -0
  307. package/dist/server/validation/review-workflows.mjs +65 -0
  308. package/dist/server/validation/review-workflows.mjs.map +1 -0
  309. package/package.json +5 -5
  310. package/dist/admin/chunks/Layout-C4ri_ldC.js.map +0 -1
  311. package/dist/admin/chunks/Layout-CF497D6H.mjs.map +0 -1
  312. package/dist/admin/chunks/en-BNGiWajd.js.map +0 -1
  313. package/dist/admin/chunks/en-BrZXFtVv.mjs.map +0 -1
  314. package/dist/admin/chunks/id-DVOtqJqn.js +0 -1442
  315. package/dist/admin/chunks/id-DVOtqJqn.js.map +0 -1
  316. package/dist/admin/chunks/id-QD0V9dME.mjs +0 -1420
  317. package/dist/admin/chunks/id-QD0V9dME.mjs.map +0 -1
  318. package/dist/admin/chunks/index-CCx4kT-t.js.map +0 -1
  319. package/dist/admin/chunks/index-CzdEqFOm.js.map +0 -1
  320. package/dist/admin/chunks/index-DcEF47R4.mjs.map +0 -1
  321. package/dist/admin/chunks/index-iChY7MsG.mjs.map +0 -1
  322. package/dist/admin/chunks/purchase-review-workflows-4n0KXAeo.mjs.map +0 -1
  323. package/dist/admin/chunks/purchase-review-workflows-BDLncDcz.js.map +0 -1
  324. package/dist/admin/chunks/router-ChVwf8TN.js.map +0 -1
  325. package/dist/admin/chunks/router-D-YCUzYy.mjs.map +0 -1
  326. package/dist/admin/chunks/uk-CbRUr1I7.js.map +0 -1
  327. package/dist/admin/chunks/uk-DLlzEBUF.mjs.map +0 -1
@@ -1,2331 +1,10 @@
1
- import { getOr, pipe, pickBy, get, keys, has, clamp, difference, set, defaultsDeep, filter, merge, map, isString, pick, isEqual, prop, isNil, uniq, flow, size, mean, max, sum, defaultTo, update, property } from 'lodash/fp';
2
- import semver from 'semver';
3
- import { async, errors, yup, validateYupSchema } from '@strapi/utils';
4
- import '@strapi/types';
5
- import { add } from 'date-fns';
6
-
7
- const getAdminService = (name, { strapi } = {
8
- strapi: global.strapi
9
- })=>{
10
- return strapi.service(`admin::${name}`);
11
- };
12
- const getService = (name, { strapi } = {
13
- strapi: global.strapi
14
- })=>{
15
- return strapi.plugin('review-workflows').service(name);
16
- };
17
-
18
- const WORKFLOW_MODEL_UID = 'plugin::review-workflows.workflow';
19
- const STAGE_MODEL_UID = 'plugin::review-workflows.workflow-stage';
20
- /**
21
- * TODO: For V4 compatibility, the old UID was kept, when review workflows was in the admin package
22
- *
23
- * NOTE!: if you change this string you need to change it here too: strapi/packages/core/review-workflows/admin/src/routes/settings/components/Stages.tsx
24
- */ const STAGE_TRANSITION_UID = 'admin::review-workflows.stage.transition';
25
- const STAGE_DEFAULT_COLOR = '#4945FF';
26
- const ENTITY_STAGE_ATTRIBUTE = 'strapi_stage';
27
- const ENTITY_ASSIGNEE_ATTRIBUTE = 'strapi_assignee';
28
- const MAX_WORKFLOWS = 200;
29
- const MAX_STAGES_PER_WORKFLOW = 200;
30
- const ERRORS = {
31
- WORKFLOW_WITHOUT_STAGES: 'A workflow must have at least one stage.',
32
- WORKFLOWS_LIMIT: 'You’ve reached the limit of workflows in your plan. Delete a workflow or contact Sales to enable more workflows.',
33
- STAGES_LIMIT: 'You’ve reached the limit of stages for this workflow in your plan. Try deleting some stages or contact Sales to enable more stages.',
34
- DUPLICATED_STAGE_NAME: 'Stage names must be unique.'
35
- };
36
- const WORKFLOW_POPULATE = {
37
- stages: {
38
- populate: {
39
- permissions: {
40
- fields: [
41
- 'action',
42
- 'actionParameters'
43
- ],
44
- populate: {
45
- role: {
46
- fields: [
47
- 'id',
48
- 'name'
49
- ]
50
- }
51
- }
52
- }
53
- }
54
- },
55
- stageRequiredToPublish: true
56
- };
57
-
58
- function checkVersionThreshold(startVersion, currentVersion, thresholdVersion) {
59
- return semver.gte(currentVersion, thresholdVersion) && semver.lt(startVersion, thresholdVersion);
60
- }
61
- /**
62
- * Shorten strapi stage name
63
- */ async function migrateStageAttribute({ oldContentTypes, contentTypes }) {
64
- const getRWVersion = getOr('0.0.0', `${STAGE_MODEL_UID}.options.version`);
65
- const oldRWVersion = getRWVersion(oldContentTypes);
66
- const currentRWVersion = getRWVersion(contentTypes);
67
- checkVersionThreshold(oldRWVersion, currentRWVersion, '1.1.0');
68
- // TODO: Find tables with something else than `findTables` function
69
- // if (migrationNeeded) {
70
- // const oldAttributeTableName = 'strapi_review_workflows_stage';
71
- // const newAttributeTableName = 'strapi_stage';
72
- // // const tables = await findTables({ strapi }, new RegExp(oldAttributeTableName));
73
- // await async.map(tables, async (tableName: string) => {
74
- // const newTableName = tableName.replace(oldAttributeTableName, newAttributeTableName);
75
- // const alreadyHasNextTable = await strapi.db.connection.schema.hasTable(newTableName);
76
- // // The table can be already created but empty. In order to rename the old one, we need to drop the previously created empty one.
77
- // if (alreadyHasNextTable) {
78
- // const dataInTable = await strapi.db.connection(newTableName).select().limit(1);
79
- // if (!dataInTable.length) {
80
- // await strapi.db.connection.schema.dropTable(newTableName);
81
- // }
82
- // }
83
- // try {
84
- // await strapi.db.connection.schema.renameTable(tableName, newTableName);
85
- // } catch (e: any) {
86
- // strapi.log.warn(
87
- // `An error occurred during the migration of ${tableName} table to ${newTableName}.\nIf ${newTableName} already exists, migration can't be done automatically.`
88
- // );
89
- // strapi.log.warn(e.message);
90
- // }
91
- // });
92
- // }
93
- }
94
-
95
- /**
96
- * Set the default color for stages if the color attribute was added
97
- */ async function migrateReviewWorkflowStagesColor({ oldContentTypes, contentTypes }) {
98
- // Look for CT's color attribute
99
- const hadColor = !!oldContentTypes?.[STAGE_MODEL_UID]?.attributes?.color;
100
- const hasColor = !!contentTypes?.[STAGE_MODEL_UID]?.attributes?.color;
101
- // Add the default stage color if color attribute was added
102
- if (!hadColor && hasColor) {
103
- await strapi.db.query(STAGE_MODEL_UID).updateMany({
104
- data: {
105
- color: STAGE_DEFAULT_COLOR
106
- }
107
- });
108
- }
109
- }
110
-
111
- /**
112
- * Migrate review workflow stages to have RBAC permissions for all roles.
113
- */ async function migrateReviewWorkflowStagesRoles({ oldContentTypes, contentTypes }) {
114
- const hadRolePermissions = !!oldContentTypes?.[STAGE_MODEL_UID]?.attributes?.permissions;
115
- const hasRolePermissions = !!contentTypes?.[STAGE_MODEL_UID]?.attributes?.permissions;
116
- // If the stage content type did not have permissions in the previous version
117
- // then we set the permissions of every stage to be every current role in the app.
118
- // This ensures consistent behaviour when upgrading to a strapi version with review workflows RBAC.
119
- if (!hadRolePermissions && hasRolePermissions) {
120
- const roleUID = 'admin::role';
121
- strapi.log.info(`Migrating all existing review workflow stages to have RBAC permissions for all ${roleUID}.`);
122
- const stagePermissionsService = getService('stage-permissions');
123
- const stages = await strapi.db.query(STAGE_MODEL_UID).findMany();
124
- const roles = await strapi.db.query(roleUID).findMany();
125
- // Collect the permissions to add and group them by stage id.
126
- const groupedPermissions = {};
127
- roles.map((role)=>role.id).forEach((roleId)=>{
128
- stages.map((stage)=>stage.id).forEach((stageId)=>{
129
- if (!groupedPermissions[stageId]) {
130
- groupedPermissions[stageId] = [];
131
- }
132
- groupedPermissions[stageId].push({
133
- roleId,
134
- fromStage: stageId,
135
- action: STAGE_TRANSITION_UID
136
- });
137
- });
138
- });
139
- for (const [stageId, permissions] of Object.entries(groupedPermissions)){
140
- const numericalStageId = Number(stageId);
141
- if (Number.isNaN(numericalStageId)) {
142
- strapi.log.warn(`Unable to apply ${roleUID} migration for ${STAGE_MODEL_UID} with id ${stageId}. The stage does not have a numerical id.`);
143
- continue;
144
- }
145
- // Register the permissions for this stage
146
- const stagePermissions = await stagePermissionsService.registerMany(permissions);
147
- // Update the stage with its new permissions
148
- await strapi.db.query(STAGE_MODEL_UID).update({
149
- where: {
150
- id: numericalStageId
151
- },
152
- data: {
153
- permissions: stagePermissions.flat().map((permission)=>permission.id)
154
- }
155
- });
156
- }
157
- }
158
- }
159
-
160
- var name = "Default";
161
- var defaultWorkflow = {
162
- name: name
163
- };
164
-
165
- /**
166
- * Multiple workflows introduced the ability to name a workflow.
167
- * This migration adds the default workflow name if the name attribute was added.
168
- */ async function migrateReviewWorkflowName({ oldContentTypes, contentTypes }) {
169
- // Look for RW name attribute
170
- const hadName = !!oldContentTypes?.[WORKFLOW_MODEL_UID]?.attributes?.name;
171
- const hasName = !!contentTypes?.[WORKFLOW_MODEL_UID]?.attributes?.name;
172
- // Add the default workflow name if name attribute was added
173
- if (!hadName && hasName) {
174
- await strapi.db.query(WORKFLOW_MODEL_UID).updateMany({
175
- where: {
176
- name: {
177
- $null: true
178
- }
179
- },
180
- data: {
181
- name: defaultWorkflow.name
182
- }
183
- });
184
- }
185
- }
186
-
187
- async function migrateWorkflowsContentTypes({ oldContentTypes, contentTypes }) {
188
- // Look for RW contentTypes attribute
189
- const hadContentTypes = !!oldContentTypes?.[WORKFLOW_MODEL_UID]?.attributes?.contentTypes;
190
- const hasContentTypes = !!contentTypes?.[WORKFLOW_MODEL_UID]?.attributes?.contentTypes;
191
- if (!hadContentTypes && hasContentTypes) {
192
- // Initialize contentTypes with an empty array and assign only to one
193
- // workflow the Content Types which were using Review Workflow before.
194
- await strapi.db.query(WORKFLOW_MODEL_UID).updateMany({
195
- data: {
196
- contentTypes: []
197
- }
198
- });
199
- // Find Content Types which were using Review Workflow before
200
- const contentTypes = pipe([
201
- pickBy(get('options.reviewWorkflows')),
202
- keys
203
- ])(oldContentTypes);
204
- if (contentTypes.length) {
205
- // Update only one workflow with the contentTypes
206
- // Before this release there was only one workflow, so this operation is safe.
207
- await strapi.db.query(WORKFLOW_MODEL_UID).update({
208
- where: {
209
- id: {
210
- $notNull: true
211
- }
212
- },
213
- data: {
214
- contentTypes
215
- }
216
- });
217
- }
218
- }
219
- }
220
-
221
- const getVisibleContentTypesUID = pipe([
222
- // Pick only content-types visible in the content-manager and option is not false
223
- pickBy((value)=>getOr(true, 'pluginOptions.content-manager.visible', value) && !getOr(false, 'options.noStageAttribute', value)),
224
- // Get UIDs
225
- keys
226
- ]);
227
- const hasStageAttribute = has([
228
- 'attributes',
229
- ENTITY_STAGE_ATTRIBUTE
230
- ]);
231
- const getWorkflowContentTypeFilter = ({ strapi }, contentType)=>{
232
- if (strapi.db.dialect.supportsOperator('$jsonSupersetOf')) {
233
- return {
234
- $jsonSupersetOf: JSON.stringify([
235
- contentType
236
- ])
237
- };
238
- }
239
- return {
240
- $contains: `"${contentType}"`
241
- };
242
- };
243
- const clampMaxWorkflows = clamp(1, MAX_WORKFLOWS);
244
- const clampMaxStagesPerWorkflow = clamp(1, MAX_STAGES_PER_WORKFLOW);
245
-
246
- /**
247
- * Remove CT references from workflows if the CT is deleted
248
- */ async function migrateDeletedCTInWorkflows({ oldContentTypes, contentTypes }) {
249
- const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes)) ?? [];
250
- if (deletedContentTypes.length) {
251
- await async.map(deletedContentTypes, async (deletedContentTypeUID)=>{
252
- const workflow = await strapi.db.query(WORKFLOW_MODEL_UID).findOne({
253
- select: [
254
- 'id',
255
- 'contentTypes'
256
- ],
257
- where: {
258
- contentTypes: getWorkflowContentTypeFilter({
259
- strapi
260
- }, deletedContentTypeUID)
261
- }
262
- });
263
- if (workflow) {
264
- await strapi.db.query(WORKFLOW_MODEL_UID).update({
265
- where: {
266
- id: workflow.id
267
- },
268
- data: {
269
- contentTypes: workflow.contentTypes.filter((contentTypeUID)=>contentTypeUID !== deletedContentTypeUID)
270
- }
271
- });
272
- }
273
- });
274
- }
275
- }
276
-
277
- /**
278
- * A Strapi middleware function that adds support for review workflows.
279
- *
280
- * Why is it needed ?
281
- * For now, the admin panel cannot have anything but top-level attributes in the content-type for options.
282
- * But we need the CE part to be agnostics from Review Workflow (which is an EE feature).
283
- * CE handle the `options` object, that's why we move the reviewWorkflows boolean to the options object.
284
- *
285
- * @param {object} strapi - The Strapi instance.
286
- */ function contentTypeMiddleware(strapi) {
287
- /**
288
- * A middleware function that moves the `reviewWorkflows` attribute from the top level of
289
- * the request body to the `options` object within the request body.
290
- *
291
- * @param {object} ctx - The Koa context object.
292
- */ const moveReviewWorkflowOption = (ctx)=>{
293
- // Move reviewWorkflows to options.reviewWorkflows
294
- const { reviewWorkflows, ...contentType } = ctx.request.body.contentType;
295
- if (typeof reviewWorkflows === 'boolean') {
296
- ctx.request.body.contentType = set('options.reviewWorkflows', reviewWorkflows, contentType);
297
- }
298
- };
299
- strapi.server.router.use('/content-type-builder/content-types/:uid?', (ctx, next)=>{
300
- if (ctx.method === 'PUT' || ctx.method === 'POST') {
301
- moveReviewWorkflowOption(ctx);
302
- }
303
- return next();
304
- });
305
- }
306
- var reviewWorkflowsMiddlewares = {
307
- contentTypeMiddleware
308
- };
309
-
310
- const setRelation = (attributeName, target, contentType)=>{
311
- Object.assign(contentType.attributes, {
312
- [attributeName]: {
313
- writable: true,
314
- private: false,
315
- configurable: false,
316
- visible: false,
317
- useJoinTable: true,
318
- type: 'relation',
319
- relation: 'oneToOne',
320
- target
321
- }
322
- });
323
- return contentType;
324
- };
325
- /**
326
- * Add the stage and assignee attributes to content types
327
- */ function extendReviewWorkflowContentTypes({ strapi }) {
328
- const contentTypeToExtend = getVisibleContentTypesUID(strapi.contentTypes);
329
- for (const contentTypeUID of contentTypeToExtend){
330
- strapi.get('content-types').extend(contentTypeUID, (contentType)=>{
331
- // Set Stage attribute
332
- setRelation(ENTITY_STAGE_ATTRIBUTE, STAGE_MODEL_UID, contentType);
333
- // Set Assignee attribute
334
- setRelation(ENTITY_ASSIGNEE_ATTRIBUTE, 'admin::user', contentType);
335
- });
336
- }
337
- }
338
- /**
339
- * Persist the stage & assignee attributes so they are not removed when downgrading to CE.
340
- *
341
- * TODO: V6 - Instead of persisting the join tables, always create the stage & assignee attributes, even in CE mode
342
- * It was decided in V4 & V5 to not expose them in CE (as they pollute the CTs) but it's not worth given the complexity this needs
343
- */ function persistRWOnDowngrade({ strapi }) {
344
- const { removePersistedTablesWithSuffix, persistTables } = getAdminService('persist-tables');
345
- return async ({ contentTypes })=>{
346
- const getStageTableToPersist = (contentTypeUID)=>{
347
- // Persist the stage join table
348
- const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
349
- const joinTableName = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable.name;
350
- return {
351
- name: joinTableName,
352
- dependsOn: [
353
- {
354
- name: tableName
355
- }
356
- ]
357
- };
358
- };
359
- const getAssigneeTableToPersist = (contentTypeUID)=>{
360
- // Persist the assignee join table
361
- const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
362
- const joinTableName = attributes[ENTITY_ASSIGNEE_ATTRIBUTE].joinTable.name;
363
- return {
364
- name: joinTableName,
365
- dependsOn: [
366
- {
367
- name: tableName
368
- }
369
- ]
370
- };
371
- };
372
- const enabledRWContentTypes = pipe([
373
- getVisibleContentTypesUID,
374
- filter((uid)=>hasStageAttribute(contentTypes[uid]))
375
- ])(contentTypes);
376
- // Remove previously created join tables and persist the new ones
377
- const stageJoinTablesToPersist = enabledRWContentTypes.map(getStageTableToPersist);
378
- await removePersistedTablesWithSuffix('_strapi_stage_lnk');
379
- await persistTables(stageJoinTablesToPersist);
380
- // Remove previously created join tables and persist the new ones
381
- const assigneeJoinTablesToPersist = enabledRWContentTypes.map(getAssigneeTableToPersist);
382
- await removePersistedTablesWithSuffix('_strapi_assignee_lnk');
383
- await persistTables(assigneeJoinTablesToPersist);
384
- };
385
- }
386
- var register = (async ({ strapi })=>{
387
- // Data Migrations
388
- strapi.hook('strapi::content-types.beforeSync').register(migrateStageAttribute);
389
- strapi.hook('strapi::content-types.afterSync').register(persistRWOnDowngrade({
390
- strapi
391
- }));
392
- strapi.hook('strapi::content-types.afterSync').register(migrateReviewWorkflowStagesColor).register(migrateReviewWorkflowStagesRoles).register(migrateReviewWorkflowName).register(migrateWorkflowsContentTypes).register(migrateDeletedCTInWorkflows);
393
- // Middlewares
394
- reviewWorkflowsMiddlewares.contentTypeMiddleware(strapi);
395
- // Schema customization
396
- extendReviewWorkflowContentTypes({
397
- strapi
398
- });
399
- // License limits
400
- const reviewWorkflowsOptions = defaultsDeep({
401
- numberOfWorkflows: MAX_WORKFLOWS,
402
- stagesPerWorkflow: MAX_STAGES_PER_WORKFLOW
403
- }, strapi.ee.features.get('review-workflows'));
404
- const workflowsValidationService = getService('validation', {
405
- strapi
406
- });
407
- workflowsValidationService.register(reviewWorkflowsOptions);
408
- });
409
-
410
- var workflow = {
411
- schema: {
412
- collectionName: 'strapi_workflows',
413
- info: {
414
- name: 'Workflow',
415
- description: '',
416
- singularName: 'workflow',
417
- pluralName: 'workflows',
418
- displayName: 'Workflow'
419
- },
420
- options: {},
421
- pluginOptions: {
422
- 'content-manager': {
423
- visible: false
424
- },
425
- 'content-type-builder': {
426
- visible: false
427
- }
428
- },
429
- attributes: {
430
- name: {
431
- type: 'string',
432
- required: true,
433
- unique: true
434
- },
435
- stages: {
436
- type: 'relation',
437
- target: 'plugin::review-workflows.workflow-stage',
438
- relation: 'oneToMany',
439
- mappedBy: 'workflow'
440
- },
441
- stageRequiredToPublish: {
442
- type: 'relation',
443
- target: 'plugin::review-workflows.workflow-stage',
444
- relation: 'oneToOne',
445
- required: false
446
- },
447
- contentTypes: {
448
- type: 'json',
449
- required: true,
450
- default: '[]'
451
- }
452
- }
453
- }
454
- };
455
-
456
- var workflowStage = {
457
- schema: {
458
- collectionName: 'strapi_workflows_stages',
459
- info: {
460
- name: 'Workflow Stage',
461
- description: '',
462
- singularName: 'workflow-stage',
463
- pluralName: 'workflow-stages',
464
- displayName: 'Stages'
465
- },
466
- options: {
467
- version: '1.1.0'
468
- },
469
- pluginOptions: {
470
- 'content-manager': {
471
- visible: false
472
- },
473
- 'content-type-builder': {
474
- visible: false
475
- }
476
- },
477
- attributes: {
478
- name: {
479
- type: 'string',
480
- configurable: false
481
- },
482
- color: {
483
- type: 'string',
484
- configurable: false,
485
- default: STAGE_DEFAULT_COLOR
486
- },
487
- workflow: {
488
- type: 'relation',
489
- target: 'plugin::review-workflows.workflow',
490
- relation: 'manyToOne',
491
- inversedBy: 'stages',
492
- configurable: false
493
- },
494
- permissions: {
495
- type: 'relation',
496
- target: 'admin::permission',
497
- relation: 'manyToMany',
498
- configurable: false
499
- }
500
- }
501
- }
502
- };
503
-
504
- var contentTypes = {
505
- workflow,
506
- 'workflow-stage': workflowStage
507
- };
508
-
509
- var actions = {
510
- reviewWorkflows: [
511
- {
512
- uid: 'review-workflows.create',
513
- displayName: 'Create',
514
- pluginName: 'admin',
515
- section: 'settings',
516
- category: 'review workflows',
517
- subCategory: 'options'
518
- },
519
- {
520
- uid: 'review-workflows.read',
521
- displayName: 'Read',
522
- pluginName: 'admin',
523
- section: 'settings',
524
- category: 'review workflows',
525
- subCategory: 'options'
526
- },
527
- {
528
- uid: 'review-workflows.update',
529
- displayName: 'Update',
530
- pluginName: 'admin',
531
- section: 'settings',
532
- category: 'review workflows',
533
- subCategory: 'options'
534
- },
535
- {
536
- uid: 'review-workflows.delete',
537
- displayName: 'Delete',
538
- pluginName: 'admin',
539
- section: 'settings',
540
- category: 'review workflows',
541
- subCategory: 'options'
542
- },
543
- {
544
- uid: 'review-workflows.stage.transition',
545
- displayName: 'Change stage',
546
- pluginName: 'admin',
547
- section: 'internal'
548
- }
549
- ]
550
- };
551
-
552
- var defaultStages = [
553
- {
554
- name: "To do",
555
- color: "#4945FF"
556
- },
557
- {
558
- name: "Ready to review",
559
- color: "#9736E8"
560
- },
561
- {
562
- name: "In progress",
563
- color: "#EE5E52"
564
- },
565
- {
566
- name: "Reviewed",
567
- color: "#328048"
568
- }
569
- ];
570
-
571
- const WORKFLOW_UPDATE_STAGE = 'review-workflows.updateEntryStage';
572
- var webhookEvents = {
573
- WORKFLOW_UPDATE_STAGE
574
- };
575
-
576
- /**
577
- * Initialize the default workflow if there is no workflow in the database
578
- */ async function initDefaultWorkflow() {
579
- const workflowsService = getService('workflows', {
580
- strapi
581
- });
582
- const stagesService = getService('stages', {
583
- strapi
584
- });
585
- const wfCount = await workflowsService.count();
586
- const stagesCount = await stagesService.count();
587
- // Check if there is nothing about review-workflow in DB
588
- // If any, the feature has already been initialized with a workflow and stages
589
- if (wfCount === 0 && stagesCount === 0) {
590
- const workflow = {
591
- ...defaultWorkflow,
592
- contentTypes: [],
593
- stages: defaultStages
594
- };
595
- await workflowsService.create({
596
- data: workflow
597
- });
598
- }
599
- }
600
- /**
601
- * Webhook store limits the events that can be triggered,
602
- * this function extends it with the events review workflows can trigger
603
- */ const registerWebhookEvents = async ()=>Object.entries(webhookEvents).forEach(([eventKey, event])=>strapi.get('webhookStore').addAllowedEvent(eventKey, event));
604
- var bootstrap = (async (args)=>{
605
- // Permissions
606
- const { actionProvider } = getAdminService('permission');
607
- await actionProvider.registerMany(actions.reviewWorkflows);
608
- // Webhooks and events
609
- await registerWebhookEvents();
610
- await getService('workflow-weekly-metrics').registerCron();
611
- // Data initialization
612
- await initDefaultWorkflow();
613
- // Document service middleware
614
- const docsMiddlewares = getService('document-service-middlewares');
615
- strapi.documents.use(docsMiddlewares.assignStageOnCreate);
616
- strapi.documents.use(docsMiddlewares.handleStageOnUpdate);
617
- strapi.documents.use(docsMiddlewares.checkStageBeforePublish);
618
- });
619
-
620
- var destroy = (async ({ strapi })=>{});
621
-
622
- const enableFeatureMiddleware = (featureName)=>(ctx, next)=>{
623
- if (strapi.ee.features.isEnabled(featureName)) {
624
- return next();
625
- }
626
- ctx.status = 404;
627
- };
628
-
629
- var reviewWorkflows = {
630
- type: 'admin',
631
- routes: [
632
- // Review workflow
633
- {
634
- method: 'POST',
635
- path: '/workflows',
636
- handler: 'workflows.create',
637
- config: {
638
- middlewares: [
639
- enableFeatureMiddleware('review-workflows')
640
- ],
641
- policies: [
642
- 'admin::isAuthenticatedAdmin',
643
- {
644
- name: 'admin::hasPermissions',
645
- config: {
646
- actions: [
647
- 'admin::review-workflows.create'
648
- ]
649
- }
650
- }
651
- ]
652
- }
653
- },
654
- {
655
- method: 'PUT',
656
- path: '/workflows/:id',
657
- handler: 'workflows.update',
658
- config: {
659
- middlewares: [
660
- enableFeatureMiddleware('review-workflows')
661
- ],
662
- policies: [
663
- 'admin::isAuthenticatedAdmin',
664
- {
665
- name: 'admin::hasPermissions',
666
- config: {
667
- actions: [
668
- 'admin::review-workflows.update'
669
- ]
670
- }
671
- }
672
- ]
673
- }
674
- },
675
- {
676
- method: 'DELETE',
677
- path: '/workflows/:id',
678
- handler: 'workflows.delete',
679
- config: {
680
- middlewares: [
681
- enableFeatureMiddleware('review-workflows')
682
- ],
683
- policies: [
684
- 'admin::isAuthenticatedAdmin',
685
- {
686
- name: 'admin::hasPermissions',
687
- config: {
688
- actions: [
689
- 'admin::review-workflows.delete'
690
- ]
691
- }
692
- }
693
- ]
694
- }
695
- },
696
- {
697
- method: 'GET',
698
- path: '/workflows',
699
- handler: 'workflows.find',
700
- config: {
701
- middlewares: [
702
- enableFeatureMiddleware('review-workflows')
703
- ],
704
- policies: [
705
- 'admin::isAuthenticatedAdmin',
706
- {
707
- name: 'admin::hasPermissions',
708
- config: {
709
- actions: [
710
- 'admin::review-workflows.read'
711
- ]
712
- }
713
- }
714
- ]
715
- }
716
- },
717
- {
718
- method: 'GET',
719
- path: '/workflows/:workflow_id/stages',
720
- handler: 'stages.find',
721
- config: {
722
- middlewares: [
723
- enableFeatureMiddleware('review-workflows')
724
- ],
725
- policies: [
726
- 'admin::isAuthenticatedAdmin',
727
- {
728
- name: 'admin::hasPermissions',
729
- config: {
730
- actions: [
731
- 'admin::review-workflows.read'
732
- ]
733
- }
734
- }
735
- ]
736
- }
737
- },
738
- {
739
- method: 'GET',
740
- path: '/workflows/:workflow_id/stages/:id',
741
- handler: 'stages.findById',
742
- config: {
743
- middlewares: [
744
- enableFeatureMiddleware('review-workflows')
745
- ],
746
- policies: [
747
- 'admin::isAuthenticatedAdmin',
748
- {
749
- name: 'admin::hasPermissions',
750
- config: {
751
- actions: [
752
- 'admin::review-workflows.read'
753
- ]
754
- }
755
- }
756
- ]
757
- }
758
- },
759
- {
760
- method: 'PUT',
761
- path: '/content-manager/(collection|single)-types/:model_uid/:id/stage',
762
- handler: 'stages.updateEntity',
763
- config: {
764
- middlewares: [
765
- enableFeatureMiddleware('review-workflows')
766
- ],
767
- policies: [
768
- 'admin::isAuthenticatedAdmin'
769
- ]
770
- }
771
- },
772
- {
773
- method: 'GET',
774
- path: '/content-manager/(collection|single)-types/:model_uid/:id/stages',
775
- handler: 'stages.listAvailableStages',
776
- config: {
777
- middlewares: [
778
- enableFeatureMiddleware('review-workflows')
779
- ],
780
- policies: [
781
- 'admin::isAuthenticatedAdmin'
782
- ]
783
- }
784
- },
785
- {
786
- method: 'PUT',
787
- path: '/content-manager/(collection|single)-types/:model_uid/:id/assignee',
788
- handler: 'assignees.updateEntity',
789
- config: {
790
- middlewares: [
791
- enableFeatureMiddleware('review-workflows')
792
- ],
793
- policies: [
794
- 'admin::isAuthenticatedAdmin',
795
- {
796
- name: 'admin::hasPermissions',
797
- config: {
798
- actions: [
799
- 'admin::users.read'
800
- ]
801
- }
802
- }
803
- ]
804
- }
805
- }
806
- ]
807
- };
808
-
809
- var routes = {
810
- 'review-workflows': reviewWorkflows
811
- };
812
-
813
- var workflowsContentTypesFactory = (({ strapi })=>{
814
- const contentManagerContentTypeService = strapi.plugin('content-manager').service('content-types');
815
- const stagesService = getService('stages', {
816
- strapi
817
- });
818
- const updateContentTypeConfig = async (uid, reviewWorkflowOption)=>{
819
- // Merge options in the configuration as the configuration service use a destructuration merge which doesn't include nested objects
820
- const modelConfig = await contentManagerContentTypeService.findConfiguration(uid);
821
- await contentManagerContentTypeService.updateConfiguration({
822
- uid
823
- }, {
824
- options: merge(modelConfig.options, {
825
- reviewWorkflows: reviewWorkflowOption
826
- })
827
- });
828
- };
829
- return {
830
- /**
831
- * Migrates entities stages. Used when a content type is assigned to a workflow.
832
- * @param {*} options
833
- * @param {Array<string>} options.srcContentTypes - The content types assigned to the previous workflow
834
- * @param {Array<string>} options.destContentTypes - The content types assigned to the new workflow
835
- * @param {Workflow.Stage} options.stageId - The new stage to assign the entities to
836
- */ async migrate ({ srcContentTypes = [], destContentTypes, stageId }) {
837
- const workflowsService = getService('workflows', {
838
- strapi
839
- });
840
- const { created, deleted } = diffContentTypes(srcContentTypes, destContentTypes);
841
- await async.map(created, async (uid)=>{
842
- // Content Types should only be assigned to one workflow
843
- // However, edge cases can happen, and this handles them
844
- const srcWorkflows = await workflowsService._getAssignedWorkflows(uid, {});
845
- if (srcWorkflows.length) {
846
- // Updates all existing entities stages links to the new stage
847
- await stagesService.updateEntitiesStage(uid, {
848
- toStageId: stageId
849
- });
850
- // Transfer content types from the previous workflow(s)
851
- await async.map(srcWorkflows, (srcWorkflow)=>this.transferContentTypes(srcWorkflow, uid));
852
- }
853
- await updateContentTypeConfig(uid, true);
854
- // Create new stages links to the new stage
855
- return stagesService.updateEntitiesStage(uid, {
856
- fromStageId: null,
857
- toStageId: stageId
858
- });
859
- }, // transferContentTypes can cause race conditions if called in parallel when updating the same workflow
860
- {
861
- concurrency: 1
862
- });
863
- await async.map(deleted, async (uid)=>{
864
- await updateContentTypeConfig(uid, false);
865
- await stagesService.deleteAllEntitiesStage(uid, {});
866
- });
867
- },
868
- /**
869
- * Filters the content types assigned to a workflow
870
- * @param {Workflow} srcWorkflow - The workflow to transfer from
871
- * @param {string} uid - The content type uid
872
- */ async transferContentTypes (srcWorkflow, uid) {
873
- // Update assignedContentTypes of the previous workflow
874
- await strapi.db.query(WORKFLOW_MODEL_UID).update({
875
- where: {
876
- id: srcWorkflow.id
877
- },
878
- data: {
879
- contentTypes: srcWorkflow.contentTypes.filter((contentType)=>contentType !== uid)
880
- }
881
- });
882
- }
883
- };
884
- });
885
- const diffContentTypes = (srcContentTypes, destContentTypes)=>{
886
- const created = difference(destContentTypes, srcContentTypes);
887
- const deleted = difference(srcContentTypes, destContentTypes);
888
- return {
889
- created,
890
- deleted
891
- };
892
- };
893
-
894
- const processFilters = ({ strapi }, filters = {})=>{
895
- const processedFilters = {
896
- ...filters
897
- };
898
- if (isString(filters.contentTypes)) {
899
- processedFilters.contentTypes = getWorkflowContentTypeFilter({
900
- strapi
901
- }, filters.contentTypes);
902
- }
903
- return processedFilters;
904
- };
905
- // TODO: How can we improve this? Maybe using traversePopulate?
906
- const processPopulate = (populate)=>{
907
- // If it does not exist or it's not an object (like an array) return the default populate
908
- if (!populate) {
909
- return WORKFLOW_POPULATE;
910
- }
911
- return populate;
912
- };
913
- var workflows$1 = (({ strapi })=>{
914
- const workflowsContentTypes = workflowsContentTypesFactory({
915
- strapi
916
- });
917
- const workflowValidator = getService('validation', {
918
- strapi
919
- });
920
- const metrics = getService('workflow-metrics', {
921
- strapi
922
- });
923
- return {
924
- /**
925
- * Returns all the workflows matching the user-defined filters.
926
- * @param {object} opts - Options for the query.
927
- * @param {object} opts.filters - Filters object.
928
- * @returns {Promise<object[]>} - List of workflows that match the user's filters.
929
- */ async find (opts = {}) {
930
- const filters = processFilters({
931
- strapi
932
- }, opts.filters);
933
- const populate = processPopulate(opts.populate);
934
- const query = strapi.get('query-params').transform(WORKFLOW_MODEL_UID, {
935
- ...opts,
936
- filters,
937
- populate
938
- });
939
- return strapi.db.query(WORKFLOW_MODEL_UID).findMany(query);
940
- },
941
- /**
942
- * Returns the workflow with the specified ID.
943
- * @param {string} id - ID of the requested workflow.
944
- * @param {object} opts - Options for the query.
945
- * @returns {Promise<object>} - Workflow object matching the requested ID.
946
- */ findById (id, opts = {}) {
947
- const populate = processPopulate(opts.populate);
948
- const query = strapi.get('query-params').transform(WORKFLOW_MODEL_UID, {
949
- populate
950
- });
951
- return strapi.db.query(WORKFLOW_MODEL_UID).findOne({
952
- ...query,
953
- where: {
954
- id
955
- }
956
- });
957
- },
958
- /**
959
- * Creates a new workflow.
960
- * @param {object} opts - Options for creating the new workflow.
961
- * @returns {Promise<object>} - Workflow object that was just created.
962
- * @throws {ValidationError} - If the workflow has no stages.
963
- */ async create (opts) {
964
- let createOpts = {
965
- ...opts,
966
- populate: WORKFLOW_POPULATE
967
- };
968
- workflowValidator.validateWorkflowStages(opts.data.stages);
969
- await workflowValidator.validateWorkflowCount(1);
970
- return strapi.db.transaction(async ()=>{
971
- // Create stages
972
- const stages = await getService('stages', {
973
- strapi
974
- }).createMany(opts.data.stages);
975
- const mapIds = map(get('id'));
976
- createOpts = set('data.stages', mapIds(stages), createOpts);
977
- if (opts.data.stageRequiredToPublishName) {
978
- const stageRequiredToPublish = stages.find((stage)=>stage.name === opts.data.stageRequiredToPublishName);
979
- if (!stageRequiredToPublish) {
980
- throw new errors.ApplicationError('Stage required to publish does not exist');
981
- }
982
- createOpts = set('data.stageRequiredToPublish', stageRequiredToPublish.id, createOpts);
983
- }
984
- // Update (un)assigned Content Types
985
- if (opts.data.contentTypes) {
986
- await workflowsContentTypes.migrate({
987
- destContentTypes: opts.data.contentTypes,
988
- stageId: stages[0].id
989
- });
990
- }
991
- // Create Workflow
992
- const createdWorkflow = await strapi.db.query(WORKFLOW_MODEL_UID).create(strapi.get('query-params').transform(WORKFLOW_MODEL_UID, createOpts));
993
- metrics.sendDidCreateWorkflow(createdWorkflow.id, !!opts.data.stageRequiredToPublishName);
994
- if (opts.data.stageRequiredToPublishName) {
995
- await strapi.plugin('content-releases').service('release-action').validateActionsByContentTypes(opts.data.contentTypes);
996
- }
997
- return createdWorkflow;
998
- });
999
- },
1000
- /**
1001
- * Updates an existing workflow.
1002
- * @param {object} workflow - The existing workflow to update.
1003
- * @param {object} opts - Options for updating the workflow.
1004
- * @returns {Promise<object>} - Workflow object that was just updated.
1005
- * @throws {ApplicationError} - If the supplied stage ID does not belong to the workflow.
1006
- */ async update (workflow, opts) {
1007
- const stageService = getService('stages', {
1008
- strapi
1009
- });
1010
- let updateOpts = {
1011
- ...opts,
1012
- populate: {
1013
- ...WORKFLOW_POPULATE
1014
- }
1015
- };
1016
- let updatedStages = [];
1017
- let updatedStageIds;
1018
- await workflowValidator.validateWorkflowCount();
1019
- return strapi.db.transaction(async ()=>{
1020
- // Update stages
1021
- if (opts.data.stages) {
1022
- workflowValidator.validateWorkflowStages(opts.data.stages);
1023
- opts.data.stages.forEach((stage)=>this.assertStageBelongsToWorkflow(stage.id, workflow));
1024
- updatedStages = await stageService.replaceStages(workflow.stages, opts.data.stages, workflow.contentTypes);
1025
- updatedStageIds = updatedStages.map((stage)=>stage.id);
1026
- updateOpts = set('data.stages', updatedStageIds, updateOpts);
1027
- }
1028
- if (opts.data.stageRequiredToPublishName !== undefined) {
1029
- const stages = updatedStages ?? workflow.stages;
1030
- if (opts.data.stageRequiredToPublishName === null) {
1031
- updateOpts = set('data.stageRequiredToPublish', null, updateOpts);
1032
- } else {
1033
- const stageRequiredToPublish = stages.find((stage)=>stage.name === opts.data.stageRequiredToPublishName);
1034
- if (!stageRequiredToPublish) {
1035
- throw new errors.ApplicationError('Stage required to publish does not exist');
1036
- }
1037
- updateOpts = set('data.stageRequiredToPublish', stageRequiredToPublish.id, updateOpts);
1038
- }
1039
- }
1040
- // Update (un)assigned Content Types
1041
- if (opts.data.contentTypes) {
1042
- await workflowsContentTypes.migrate({
1043
- srcContentTypes: workflow.contentTypes,
1044
- destContentTypes: opts.data.contentTypes,
1045
- stageId: updatedStageIds ? updatedStageIds[0] : workflow.stages[0].id
1046
- });
1047
- }
1048
- metrics.sendDidEditWorkflow(workflow.id, !!opts.data.stageRequiredToPublishName);
1049
- const query = strapi.get('query-params').transform(WORKFLOW_MODEL_UID, updateOpts);
1050
- // Update Workflow
1051
- const updatedWorkflow = await strapi.db.query(WORKFLOW_MODEL_UID).update({
1052
- ...query,
1053
- where: {
1054
- id: workflow.id
1055
- }
1056
- });
1057
- await strapi.plugin('content-releases').service('release-action').validateActionsByContentTypes([
1058
- ...workflow.contentTypes,
1059
- ...opts.data.contentTypes || []
1060
- ]);
1061
- return updatedWorkflow;
1062
- });
1063
- },
1064
- /**
1065
- * Deletes an existing workflow.
1066
- * Also deletes all the workflow stages and migrate all assigned the content types.
1067
- * @param {*} workflow
1068
- * @param {*} opts
1069
- * @returns
1070
- */ async delete (workflow, opts) {
1071
- const stageService = getService('stages', {
1072
- strapi
1073
- });
1074
- const workflowCount = await this.count();
1075
- if (workflowCount <= 1) {
1076
- throw new errors.ApplicationError('Can not delete the last workflow');
1077
- }
1078
- return strapi.db.transaction(async ()=>{
1079
- // Delete stages
1080
- await stageService.deleteMany(workflow.stages);
1081
- // Unassign all content types, this will migrate the content types to null
1082
- await workflowsContentTypes.migrate({
1083
- srcContentTypes: workflow.contentTypes,
1084
- destContentTypes: []
1085
- });
1086
- const query = strapi.get('query-params').transform(WORKFLOW_MODEL_UID, opts);
1087
- // Delete Workflow
1088
- const deletedWorkflow = await strapi.db.query(WORKFLOW_MODEL_UID).delete({
1089
- ...query,
1090
- where: {
1091
- id: workflow.id
1092
- }
1093
- });
1094
- await strapi.plugin('content-releases').service('release-action').validateActionsByContentTypes(workflow.contentTypes);
1095
- return deletedWorkflow;
1096
- });
1097
- },
1098
- /**
1099
- * Returns the total count of workflows.
1100
- * @returns {Promise<number>} - Total count of workflows.
1101
- */ count () {
1102
- return strapi.db.query(WORKFLOW_MODEL_UID).count();
1103
- },
1104
- /**
1105
- * Finds the assigned workflow for a given content type ID.
1106
- * @param {string} uid - Content type ID to find the assigned workflow for.
1107
- * @param {object} opts - Options for the query.
1108
- * @returns {Promise<object|null>} - Assigned workflow object if found, or null.
1109
- */ async getAssignedWorkflow (uid, opts = {}) {
1110
- const workflows = await this._getAssignedWorkflows(uid, opts);
1111
- return workflows.length > 0 ? workflows[0] : null;
1112
- },
1113
- /**
1114
- * Finds all the assigned workflows for a given content type ID.
1115
- * Normally, there should only be one workflow assigned to a content type.
1116
- * However, edge cases can occur where a content type is assigned to multiple workflows.
1117
- * @param {string} uid - Content type ID to find the assigned workflows for.
1118
- * @param {object} opts - Options for the query.
1119
- * @returns {Promise<object[]>} - List of assigned workflow objects.
1120
- */ async _getAssignedWorkflows (uid, opts = {}) {
1121
- return this.find({
1122
- ...opts,
1123
- filters: {
1124
- contentTypes: getWorkflowContentTypeFilter({
1125
- strapi
1126
- }, uid)
1127
- }
1128
- });
1129
- },
1130
- /**
1131
- * Asserts that a content type has an assigned workflow.
1132
- * @param {string} uid - Content type ID to verify the assignment of.
1133
- * @returns {Promise<object>} - Workflow object associated with the content type ID.
1134
- * @throws {ApplicationError} - If no assigned workflow is found for the content type ID.
1135
- */ async assertContentTypeBelongsToWorkflow (uid) {
1136
- const workflow = await this.getAssignedWorkflow(uid, {
1137
- populate: 'stages'
1138
- });
1139
- if (!workflow) {
1140
- throw new errors.ApplicationError(`Review workflows is not activated on Content Type ${uid}.`);
1141
- }
1142
- return workflow;
1143
- },
1144
- /**
1145
- * Asserts that a stage belongs to a given workflow.
1146
- * @param {string} stageId - ID of stage to check.
1147
- * @param {object} workflow - Workflow object to check against.
1148
- * @returns
1149
- * @throws {ApplicationError} - If the stage does not belong to the specified workflow.
1150
- */ assertStageBelongsToWorkflow (stageId, workflow) {
1151
- if (!stageId) {
1152
- return;
1153
- }
1154
- const belongs = workflow.stages.some((stage)=>stage.id === stageId);
1155
- if (!belongs) {
1156
- throw new errors.ApplicationError(`Stage does not belong to workflow "${workflow.name}"`);
1157
- }
1158
- }
1159
- };
1160
- });
1161
-
1162
- const { ApplicationError: ApplicationError$2, ValidationError: ValidationError$1 } = errors;
1163
- const sanitizedStageFields = [
1164
- 'id',
1165
- 'name',
1166
- 'workflow',
1167
- 'color'
1168
- ];
1169
- const sanitizeStageFields = pick(sanitizedStageFields);
1170
- var stages$1 = (({ strapi })=>{
1171
- const metrics = getService('workflow-metrics', {
1172
- strapi
1173
- });
1174
- const stagePermissionsService = getService('stage-permissions', {
1175
- strapi
1176
- });
1177
- const workflowValidator = getService('validation', {
1178
- strapi
1179
- });
1180
- return {
1181
- find ({ workflowId, populate }) {
1182
- return strapi.db.query(STAGE_MODEL_UID).findMany({
1183
- where: {
1184
- workflow: workflowId
1185
- },
1186
- populate
1187
- });
1188
- },
1189
- findById (id, { populate } = {}) {
1190
- return strapi.db.query(STAGE_MODEL_UID).findOne({
1191
- where: {
1192
- id
1193
- },
1194
- populate
1195
- });
1196
- },
1197
- async createMany (stagesList, { fields } = {}) {
1198
- const params = {
1199
- select: fields ?? '*'
1200
- };
1201
- const stages = await Promise.all(stagesList.map((stage)=>strapi.db.query(STAGE_MODEL_UID).create({
1202
- data: sanitizeStageFields(stage),
1203
- ...params
1204
- })));
1205
- // Create stage permissions
1206
- await async.reduce(stagesList)(async (_, stage, idx)=>{
1207
- // Ignore stages without permissions
1208
- if (!stage.permissions || stage.permissions.length === 0) {
1209
- return;
1210
- }
1211
- const stagePermissions = stage.permissions;
1212
- const stageId = stages[idx].id;
1213
- const permissions = await async.map(stagePermissions, // Register each stage permission
1214
- (permission)=>stagePermissionsService.register({
1215
- roleId: permission.role,
1216
- action: permission.action,
1217
- fromStage: stageId
1218
- }));
1219
- // Update stage with the new permissions
1220
- await strapi.db.query(STAGE_MODEL_UID).update({
1221
- where: {
1222
- id: stageId
1223
- },
1224
- data: {
1225
- permissions: permissions.flat().map((p)=>p.id)
1226
- }
1227
- });
1228
- }, []);
1229
- metrics.sendDidCreateStage();
1230
- return stages;
1231
- },
1232
- async update (srcStage, destStage) {
1233
- let stagePermissions = srcStage?.permissions ?? [];
1234
- const stageId = destStage.id;
1235
- if (destStage.permissions) {
1236
- await this.deleteStagePermissions([
1237
- srcStage
1238
- ]);
1239
- const permissions = await async.map(destStage.permissions, (permission)=>stagePermissionsService.register({
1240
- roleId: permission.role,
1241
- action: permission.action,
1242
- fromStage: stageId
1243
- }));
1244
- stagePermissions = permissions.flat().map((p)=>p.id);
1245
- }
1246
- const stage = await strapi.db.query(STAGE_MODEL_UID).update({
1247
- where: {
1248
- id: stageId
1249
- },
1250
- data: {
1251
- ...destStage,
1252
- permissions: stagePermissions
1253
- }
1254
- });
1255
- metrics.sendDidEditStage();
1256
- return stage;
1257
- },
1258
- async delete (stage) {
1259
- // Unregister all permissions related to this stage id
1260
- await this.deleteStagePermissions([
1261
- stage
1262
- ]);
1263
- const deletedStage = await strapi.db.query(STAGE_MODEL_UID).delete({
1264
- where: {
1265
- id: stage.id
1266
- }
1267
- });
1268
- metrics.sendDidDeleteStage();
1269
- return deletedStage;
1270
- },
1271
- async deleteMany (stages) {
1272
- await this.deleteStagePermissions(stages);
1273
- return strapi.db.query(STAGE_MODEL_UID).deleteMany({
1274
- where: {
1275
- id: {
1276
- $in: stages.map((s)=>s.id)
1277
- }
1278
- }
1279
- });
1280
- },
1281
- async deleteStagePermissions (stages) {
1282
- // TODO: Find another way to do this for when we use the "to" parameter.
1283
- const permissions = stages.map((s)=>s.permissions || []).flat();
1284
- await stagePermissionsService.unregister(permissions || []);
1285
- },
1286
- count ({ workflowId } = {}) {
1287
- const opts = {};
1288
- if (workflowId) {
1289
- opts.where = {
1290
- workflow: workflowId
1291
- };
1292
- }
1293
- return strapi.db.query(STAGE_MODEL_UID).count(opts);
1294
- },
1295
- async replaceStages (srcStages, destStages, contentTypesToMigrate = []) {
1296
- const { created, updated, deleted } = getDiffBetweenStages(srcStages, destStages);
1297
- assertAtLeastOneStageRemain(srcStages || [], {
1298
- created,
1299
- deleted
1300
- });
1301
- // Update stages and assign entity stages
1302
- return strapi.db.transaction(async ({ trx })=>{
1303
- // Create the new stages
1304
- const createdStages = await this.createMany(created, {
1305
- fields: [
1306
- 'id'
1307
- ]
1308
- });
1309
- // Put all the newly created stages ids
1310
- const createdStagesIds = map('id', createdStages);
1311
- // Update the workflow stages
1312
- await async.map(updated, (destStage)=>{
1313
- const srcStage = srcStages.find((s)=>s.id === destStage.id);
1314
- return this.update(srcStage, destStage);
1315
- });
1316
- // Delete the stages that are not in the new stages list
1317
- await async.map(deleted, async (stage)=>{
1318
- // Find the nearest stage in the workflow and newly created stages
1319
- // that is not deleted, prioritizing the previous stages
1320
- const nearestStage = findNearestMatchingStage([
1321
- ...srcStages,
1322
- ...createdStages
1323
- ], srcStages.findIndex((s)=>s.id === stage.id), (targetStage)=>{
1324
- return !deleted.find((s)=>s.id === targetStage.id);
1325
- });
1326
- // Assign the new stage to entities that had the deleted stage
1327
- await async.map(contentTypesToMigrate, (contentTypeUID)=>{
1328
- this.updateEntitiesStage(contentTypeUID, {
1329
- fromStageId: stage.id,
1330
- toStageId: nearestStage.id,
1331
- trx
1332
- });
1333
- });
1334
- return this.delete(stage);
1335
- });
1336
- return destStages.map((stage)=>({
1337
- ...stage,
1338
- id: stage.id ?? createdStagesIds.shift()
1339
- }));
1340
- });
1341
- },
1342
- /**
1343
- * Update the stage of an entity
1344
- */ async updateEntity (entityToUpdate, model, stageId) {
1345
- const stage = await this.findById(stageId);
1346
- const { documentId, locale } = entityToUpdate;
1347
- await workflowValidator.validateWorkflowCount();
1348
- if (!stage) {
1349
- throw new ApplicationError$2(`Selected stage does not exist`);
1350
- }
1351
- const entity = await strapi.documents(model).update({
1352
- documentId,
1353
- locale,
1354
- // Stage doesn't have DP or i18n enabled, connecting it through the `id`
1355
- // will be safer than relying on the `documentId` + `locale` + `status` transformation
1356
- data: {
1357
- [ENTITY_STAGE_ATTRIBUTE]: pick([
1358
- 'id'
1359
- ], stage)
1360
- },
1361
- populate: [
1362
- ENTITY_STAGE_ATTRIBUTE
1363
- ]
1364
- });
1365
- // Update the `updated_at` field of the entity, so that the `status` is not considered `Modified`
1366
- // NOTE: `updatedAt` is a protected attribute that can not be modified directly from the query layer
1367
- // hence the knex query builder is used here.
1368
- const { tableName } = strapi.db.metadata.get(model);
1369
- await strapi.db.connection(tableName).where({
1370
- id: entityToUpdate.id
1371
- }).update({
1372
- updated_at: new Date(entityToUpdate.updatedAt)
1373
- });
1374
- metrics.sendDidChangeEntryStage();
1375
- return entity;
1376
- },
1377
- /**
1378
- * Updates entity stages of a content type:
1379
- * - If fromStageId is undefined, all entities with an existing stage will be assigned the new stage
1380
- * - If fromStageId is null, all entities without a stage will be assigned the new stage
1381
- * - If fromStageId is a number, all entities with that stage will be assigned the new stage
1382
- *
1383
- * For performance reasons we use knex queries directly.
1384
- *
1385
- * @param {string} contentTypeUID
1386
- * @param {number | undefined | null} fromStageId
1387
- * @param {number} toStageId
1388
- * @param {import('knex').Knex.Transaction} trx
1389
- * @returns
1390
- */ async updateEntitiesStage (contentTypeUID, { fromStageId, toStageId }) {
1391
- const { attributes, tableName } = strapi.db.metadata.get(contentTypeUID);
1392
- const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
1393
- const joinColumn = joinTable.joinColumn.name;
1394
- const invJoinColumn = joinTable.inverseJoinColumn.name;
1395
- await workflowValidator.validateWorkflowCount();
1396
- return strapi.db.transaction(async ({ trx })=>{
1397
- // Update all already existing links to the new stage
1398
- if (fromStageId === undefined) {
1399
- return strapi.db.getConnection().from(joinTable.name).update({
1400
- [invJoinColumn]: toStageId
1401
- }).transacting(trx);
1402
- }
1403
- // Update all links from the specified stage to the new stage
1404
- const selectStatement = strapi.db.getConnection().select({
1405
- [joinColumn]: 't1.id',
1406
- [invJoinColumn]: toStageId
1407
- }).from(`${tableName} as t1`).leftJoin(`${joinTable.name} as t2`, `t1.id`, `t2.${joinColumn}`).where(`t2.${invJoinColumn}`, fromStageId).toSQL();
1408
- // Insert rows for all entries of the content type that have the specified stage
1409
- return strapi.db.getConnection(joinTable.name).insert(strapi.db.connection.raw(`(${joinColumn}, ${invJoinColumn}) ${selectStatement.sql}`, selectStatement.bindings)).transacting(trx);
1410
- });
1411
- },
1412
- /**
1413
- * Deletes all entity stages of a content type
1414
- * @param {string} contentTypeUID
1415
- * @returns
1416
- */ async deleteAllEntitiesStage (contentTypeUID) {
1417
- const { attributes } = strapi.db.metadata.get(contentTypeUID);
1418
- const joinTable = attributes[ENTITY_STAGE_ATTRIBUTE].joinTable;
1419
- // Delete all stage links for the content type
1420
- return strapi.db.transaction(async ({ trx })=>strapi.db.getConnection().from(joinTable.name).delete().transacting(trx));
1421
- }
1422
- };
1423
- });
1424
- /**
1425
- * Compares two arrays of stages and returns an object indicating the differences.
1426
- *
1427
- * The function compares the `id` properties of each stage in `sourceStages` and `comparisonStages` to determine if the stage is present in both arrays.
1428
- * If a stage with the same `id` is found in both arrays but the `name` property is different, the stage is considered updated.
1429
- * If a stage with a particular `id` is only found in `comparisonStages`, it is considered created.
1430
- * If a stage with a particular `id` is only found in `sourceStages`, it is considered deleted.
1431
- *
1432
- * @typedef {{id: Number, name: String, workflow: Number}} Stage
1433
- * @typedef {{created: Stage[], updated: Stage[], deleted: Stage[]}} DiffStages
1434
- *
1435
- * The DiffStages object has three properties: `created`, `updated`, and `deleted`.
1436
- * `created` is an array of stages that are in `comparisonStages` but not in `sourceStages`.
1437
- * `updated` is an array of stages that have different names in `comparisonStages` and `sourceStages`.
1438
- * `deleted` is an array of stages that are in `sourceStages` but not in `comparisonStages`.
1439
- *
1440
- * @param {Stage[]} sourceStages
1441
- * @param {Stage[]} comparisonStages
1442
- * @returns { DiffStages }
1443
- */ function getDiffBetweenStages(sourceStages, comparisonStages) {
1444
- const result = comparisonStages.reduce(// ...
1445
- (acc, stageToCompare)=>{
1446
- const srcStage = sourceStages.find((stage)=>stage.id === stageToCompare.id);
1447
- if (!srcStage) {
1448
- acc.created.push(stageToCompare);
1449
- } else if (!isEqual(pick([
1450
- 'name',
1451
- 'color',
1452
- 'permissions'
1453
- ], srcStage), pick([
1454
- 'name',
1455
- 'color',
1456
- 'permissions'
1457
- ], stageToCompare))) {
1458
- acc.updated.push(stageToCompare);
1459
- }
1460
- return acc;
1461
- }, {
1462
- created: [],
1463
- updated: []
1464
- });
1465
- result.deleted = sourceStages.filter((srcStage)=>!comparisonStages.some((cmpStage)=>cmpStage.id === srcStage.id));
1466
- return result;
1467
- }
1468
- /**
1469
- * Asserts that at least one stage remains in the workflow after applying deletions and additions.
1470
- *
1471
- * @param {Array} workflowStages - An array of stages in the current workflow.
1472
- * @param {Object} diffStages - An object containing the stages to be deleted and created.
1473
- * @param {Array} diffStages.deleted - An array of stages that are planned to be deleted from the workflow.
1474
- * @param {Array} diffStages.created - An array of stages that are planned to be created in the workflow.
1475
- *
1476
- * @throws {ValidationError} If the number of remaining stages in the workflow after applying deletions and additions is less than 1.
1477
- */ function assertAtLeastOneStageRemain(workflowStages, diffStages) {
1478
- const remainingStagesCount = workflowStages.length - diffStages.deleted.length + diffStages.created.length;
1479
- if (remainingStagesCount < 1) {
1480
- throw new ValidationError$1(ERRORS.WORKFLOW_WITHOUT_STAGES);
1481
- }
1482
- }
1483
- /**
1484
- * Find the id of the nearest object in an array that matches a condition.
1485
- * Used for searching for the nearest stage that is not deleted.
1486
- * Starts by searching the elements before the index, then the remaining elements in the array.
1487
- *
1488
- * @param {Array} stages
1489
- * @param {Number} startIndex the index to start searching from
1490
- * @param {Function} condition must evaluate to true for the object to be considered a match
1491
- * @returns {Object} stage
1492
- */ function findNearestMatchingStage(stages, startIndex, condition) {
1493
- // Start by searching the elements before the startIndex
1494
- for(let i = startIndex; i >= 0; i -= 1){
1495
- if (condition(stages[i])) {
1496
- return stages[i];
1497
- }
1498
- }
1499
- // If no matching element is found before the startIndex,
1500
- // search the remaining elements in the array
1501
- const remainingArray = stages.slice(startIndex + 1);
1502
- const nearestObject = remainingArray.filter(condition)[0];
1503
- return nearestObject;
1504
- }
1505
-
1506
- const { ApplicationError: ApplicationError$1 } = errors;
1507
- const validActions = [
1508
- STAGE_TRANSITION_UID
1509
- ];
1510
- var stagePermissions = (({ strapi })=>{
1511
- const roleService = getAdminService('role');
1512
- const permissionService = getAdminService('permission');
1513
- return {
1514
- async register ({ roleId, action, fromStage }) {
1515
- if (!validActions.includes(action)) {
1516
- throw new ApplicationError$1(`Invalid action ${action}`);
1517
- }
1518
- const permissions = await roleService.addPermissions(roleId, [
1519
- {
1520
- action,
1521
- actionParameters: {
1522
- from: fromStage
1523
- }
1524
- }
1525
- ]);
1526
- // TODO: Filter response
1527
- return permissions;
1528
- },
1529
- async registerMany (permissions) {
1530
- return async.map(permissions, this.register);
1531
- },
1532
- async unregister (permissions) {
1533
- const permissionIds = permissions.map(prop('id'));
1534
- await permissionService.deleteByIds(permissionIds);
1535
- },
1536
- can (action, fromStage) {
1537
- const requestState = strapi.requestContext.get()?.state;
1538
- if (!requestState) {
1539
- return false;
1540
- }
1541
- // Override permissions for super admin
1542
- const userRoles = requestState.user?.roles;
1543
- if (userRoles?.some((role)=>role.code === 'strapi-super-admin')) {
1544
- return true;
1545
- }
1546
- return requestState.userAbility.can({
1547
- name: action,
1548
- params: {
1549
- from: fromStage
1550
- }
1551
- });
1552
- }
1553
- };
1554
- });
1555
-
1556
- const { ApplicationError } = errors;
1557
- var assignees$1 = (({ strapi })=>{
1558
- const metrics = getService('workflow-metrics', {
1559
- strapi
1560
- });
1561
- return {
1562
- async findEntityAssigneeId (id, model) {
1563
- const entity = await strapi.db.query(model).findOne({
1564
- where: {
1565
- id
1566
- },
1567
- populate: [
1568
- ENTITY_ASSIGNEE_ATTRIBUTE
1569
- ],
1570
- select: []
1571
- });
1572
- return entity?.[ENTITY_ASSIGNEE_ATTRIBUTE]?.id ?? null;
1573
- },
1574
- /**
1575
- * Update the assignee of an entity
1576
- */ async updateEntityAssignee (entityToUpdate, model, assigneeId) {
1577
- const { documentId, locale } = entityToUpdate;
1578
- if (!isNil(assigneeId)) {
1579
- const userExists = await getAdminService('user', {
1580
- strapi
1581
- }).exists({
1582
- id: assigneeId
1583
- });
1584
- if (!userExists) {
1585
- throw new ApplicationError(`Selected user does not exist`);
1586
- }
1587
- }
1588
- const oldAssigneeId = await this.findEntityAssigneeId(entityToUpdate.id, model);
1589
- metrics.sendDidEditAssignee(oldAssigneeId, assigneeId || null);
1590
- const entity = await strapi.documents(model).update({
1591
- documentId,
1592
- locale,
1593
- data: {
1594
- [ENTITY_ASSIGNEE_ATTRIBUTE]: assigneeId || null
1595
- },
1596
- populate: [
1597
- ENTITY_ASSIGNEE_ATTRIBUTE
1598
- ],
1599
- fields: []
1600
- });
1601
- // Update the `updated_at` field of the entity, so that the `status` is not considered `Modified`
1602
- // NOTE: `updatedAt` is a protected attribute that can not be modified directly from the query layer
1603
- // hence the knex query builder is used here.
1604
- const { tableName } = strapi.db.metadata.get(model);
1605
- await strapi.db.connection(tableName).where({
1606
- id: entityToUpdate.id
1607
- }).update({
1608
- updated_at: new Date(entityToUpdate.updatedAt)
1609
- });
1610
- return entity;
1611
- }
1612
- };
1613
- });
1614
-
1615
- const { ValidationError } = errors;
1616
- var reviewWorkflowsValidation = (({ strapi })=>{
1617
- return {
1618
- limits: {
1619
- numberOfWorkflows: MAX_WORKFLOWS,
1620
- stagesPerWorkflow: MAX_STAGES_PER_WORKFLOW
1621
- },
1622
- register ({ numberOfWorkflows, stagesPerWorkflow }) {
1623
- if (!Object.isFrozen(this.limits)) {
1624
- this.limits.numberOfWorkflows = clampMaxWorkflows(numberOfWorkflows || this.limits.numberOfWorkflows);
1625
- this.limits.stagesPerWorkflow = clampMaxStagesPerWorkflow(stagesPerWorkflow || this.limits.stagesPerWorkflow);
1626
- Object.freeze(this.limits);
1627
- }
1628
- },
1629
- /**
1630
- * Validates the stages of a workflow.
1631
- * @param {Array} stages - Array of stages to be validated.
1632
- * @throws {ValidationError} - If the workflow has no stages or exceeds the limit.
1633
- */ validateWorkflowStages (stages) {
1634
- if (!stages || stages.length === 0) {
1635
- throw new ValidationError(ERRORS.WORKFLOW_WITHOUT_STAGES);
1636
- }
1637
- if (stages.length > this.limits.stagesPerWorkflow) {
1638
- throw new ValidationError(ERRORS.STAGES_LIMIT);
1639
- }
1640
- // Validate stage names are not duplicated
1641
- const stageNames = stages.map((stage)=>stage.name);
1642
- if (uniq(stageNames).length !== stageNames.length) {
1643
- throw new ValidationError(ERRORS.DUPLICATED_STAGE_NAME);
1644
- }
1645
- },
1646
- async validateWorkflowCountStages (workflowId, countAddedStages = 0) {
1647
- const stagesService = getService('stages', {
1648
- strapi
1649
- });
1650
- const countWorkflowStages = await stagesService.count({
1651
- workflowId
1652
- });
1653
- if (countWorkflowStages + countAddedStages > this.limits.stagesPerWorkflow) {
1654
- throw new ValidationError(ERRORS.STAGES_LIMIT);
1655
- }
1656
- },
1657
- /**
1658
- * Validates the count of existing and added workflows.
1659
- * @param {number} [countAddedWorkflows=0] - The count of workflows to be added.
1660
- * @throws {ValidationError} - If the total count of workflows exceeds the limit.
1661
- * @returns {Promise<void>} - A Promise that resolves when the validation is completed.
1662
- */ async validateWorkflowCount (countAddedWorkflows = 0) {
1663
- const workflowsService = getService('workflows', {
1664
- strapi
1665
- });
1666
- const countWorkflows = await workflowsService.count();
1667
- if (countWorkflows + countAddedWorkflows > this.limits.numberOfWorkflows) {
1668
- throw new ValidationError(ERRORS.WORKFLOWS_LIMIT);
1669
- }
1670
- }
1671
- };
1672
- });
1673
-
1674
- const sendDidCreateStage = async ()=>{
1675
- strapi.telemetry.send('didCreateStage', {});
1676
- };
1677
- const sendDidEditStage = async ()=>{
1678
- strapi.telemetry.send('didEditStage', {});
1679
- };
1680
- const sendDidDeleteStage = async ()=>{
1681
- strapi.telemetry.send('didDeleteStage', {});
1682
- };
1683
- const sendDidChangeEntryStage = async ()=>{
1684
- strapi.telemetry.send('didChangeEntryStage', {});
1685
- };
1686
- const sendDidCreateWorkflow = async (workflowId, hasRequiredStageToPublish)=>{
1687
- strapi.telemetry.send('didCreateWorkflow', {
1688
- workflowId,
1689
- hasRequiredStageToPublish
1690
- });
1691
- };
1692
- const sendDidEditWorkflow = async (workflowId, hasRequiredStageToPublish)=>{
1693
- strapi.telemetry.send('didEditWorkflow', {
1694
- workflowId,
1695
- hasRequiredStageToPublish
1696
- });
1697
- };
1698
- const sendDidEditAssignee = async (fromId, toId)=>{
1699
- strapi.telemetry.send('didEditAssignee', {
1700
- from: fromId,
1701
- to: toId
1702
- });
1703
- };
1704
- const sendDidSendReviewWorkflowPropertiesOnceAWeek = async (numberOfActiveWorkflows, avgStagesCount, maxStagesCount, activatedContentTypes)=>{
1705
- strapi.telemetry.send('didSendReviewWorkflowPropertiesOnceAWeek', {
1706
- groupProperties: {
1707
- numberOfActiveWorkflows,
1708
- avgStagesCount,
1709
- maxStagesCount,
1710
- activatedContentTypes
1711
- }
1712
- });
1713
- };
1714
- var reviewWorkflowsMetrics = {
1715
- sendDidCreateStage,
1716
- sendDidEditStage,
1717
- sendDidDeleteStage,
1718
- sendDidChangeEntryStage,
1719
- sendDidCreateWorkflow,
1720
- sendDidEditWorkflow,
1721
- sendDidSendReviewWorkflowPropertiesOnceAWeek,
1722
- sendDidEditAssignee
1723
- };
1724
-
1725
- const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
1726
- const getWeeklyCronScheduleAt = (date)=>`${date.getSeconds()} ${date.getMinutes()} ${date.getHours()} * * ${date.getDay()}`;
1727
- var reviewWorkflowsWeeklyMetrics = (({ strapi })=>{
1728
- const metrics = getService('workflow-metrics', {
1729
- strapi
1730
- });
1731
- const workflowsService = getService('workflows', {
1732
- strapi
1733
- });
1734
- const getMetricsStoreValue = async ()=>{
1735
- const value = await strapi.store.get({
1736
- type: 'plugin',
1737
- name: 'ee',
1738
- key: 'metrics'
1739
- });
1740
- return defaultTo({}, value);
1741
- };
1742
- const setMetricsStoreValue = (value)=>strapi.store.set({
1743
- type: 'plugin',
1744
- name: 'ee',
1745
- key: 'metrics',
1746
- value
1747
- });
1748
- return {
1749
- async computeMetrics () {
1750
- // There will never be more than 200 workflow, so we can safely fetch them all
1751
- const workflows = await workflowsService.find({
1752
- populate: 'stages'
1753
- });
1754
- const stagesCount = flow(map('stages'), map(size))(workflows);
1755
- const contentTypesCount = flow(map('contentTypes'), map(size))(workflows);
1756
- return {
1757
- numberOfActiveWorkflows: size(workflows),
1758
- avgStagesCount: mean(stagesCount),
1759
- maxStagesCount: max(stagesCount),
1760
- activatedContentTypes: sum(contentTypesCount)
1761
- };
1762
- },
1763
- async sendMetrics () {
1764
- const computedMetrics = await this.computeMetrics();
1765
- metrics.sendDidSendReviewWorkflowPropertiesOnceAWeek(computedMetrics);
1766
- const metricsInfoStored = await getMetricsStoreValue();
1767
- // @ts-expect-error metricsInfoStored can use spread
1768
- await setMetricsStoreValue({
1769
- ...metricsInfoStored,
1770
- lastWeeklyUpdate: new Date().getTime()
1771
- });
1772
- },
1773
- async ensureWeeklyStoredCronSchedule () {
1774
- const metricsInfoStored = await getMetricsStoreValue();
1775
- const { weeklySchedule: currentSchedule, lastWeeklyUpdate } = metricsInfoStored;
1776
- const now = new Date();
1777
- let weeklySchedule = currentSchedule;
1778
- if (!currentSchedule || !lastWeeklyUpdate || lastWeeklyUpdate + ONE_WEEK < now.getTime()) {
1779
- weeklySchedule = getWeeklyCronScheduleAt(add(now, {
1780
- seconds: 15
1781
- }));
1782
- await setMetricsStoreValue({
1783
- ...metricsInfoStored,
1784
- weeklySchedule
1785
- });
1786
- }
1787
- return weeklySchedule;
1788
- },
1789
- async registerCron () {
1790
- const weeklySchedule = await this.ensureWeeklyStoredCronSchedule();
1791
- strapi.cron.add({
1792
- reviewWorkflowsWeekly: {
1793
- task: this.sendMetrics.bind(this),
1794
- options: weeklySchedule
1795
- }
1796
- });
1797
- }
1798
- };
1799
- });
1800
-
1801
- /**
1802
- * Get the stage information of an entity
1803
- * @param {String} uid
1804
- * @param {Number} id
1805
- * @returns {Object}
1806
- */ const getEntityStage = async (uid, id, params)=>{
1807
- const entity = await strapi.documents(uid).findOne({
1808
- ...params,
1809
- documentId: id,
1810
- status: 'draft',
1811
- populate: {
1812
- [ENTITY_STAGE_ATTRIBUTE]: {
1813
- populate: {
1814
- workflow: true
1815
- }
1816
- }
1817
- }
1818
- });
1819
- return entity?.[ENTITY_STAGE_ATTRIBUTE] ?? {};
1820
- };
1821
- /**
1822
- * Ensures the entity is assigned to the default workflow stage
1823
- */ const assignStageOnCreate = async (ctx, next)=>{
1824
- if (ctx.action !== 'create' && ctx.action !== 'clone') {
1825
- return next();
1826
- }
1827
- /**
1828
- * Content types can have assigned workflows,
1829
- * if the CT has one, assign a default value to the stage attribute if it's not present
1830
- */ const workflow = await getService('workflows').getAssignedWorkflow(ctx.contentType.uid, {
1831
- populate: 'stages'
1832
- });
1833
- if (!workflow) {
1834
- return next();
1835
- }
1836
- const data = ctx.params.data;
1837
- // Assign the default stage if the entity doesn't have one
1838
- if (ctx.params?.data && isNil(data[ENTITY_STAGE_ATTRIBUTE])) {
1839
- data[ENTITY_STAGE_ATTRIBUTE] = {
1840
- id: workflow.stages[0].id
1841
- };
1842
- }
1843
- return next();
1844
- };
1845
- const handleStageOnUpdate = async (ctx, next)=>{
1846
- if (ctx.action !== 'update') {
1847
- return next();
1848
- }
1849
- const { documentId } = ctx.params;
1850
- const data = ctx.params.data;
1851
- if (isNil(data?.[ENTITY_STAGE_ATTRIBUTE])) {
1852
- delete data?.[ENTITY_STAGE_ATTRIBUTE];
1853
- return next();
1854
- }
1855
- /**
1856
- * Get last stage of the entity
1857
- */ const previousStage = await getEntityStage(ctx.contentType.uid, documentId, ctx.params);
1858
- const result = await next();
1859
- if (!result) {
1860
- return result;
1861
- }
1862
- // @ts-expect-error
1863
- const updatedStage = result?.[ENTITY_STAGE_ATTRIBUTE];
1864
- // Stage might be null if field is not populated
1865
- if (updatedStage && previousStage?.id && previousStage.id !== updatedStage.id) {
1866
- const model = strapi.getModel(ctx.contentType.uid);
1867
- strapi.eventHub.emit(WORKFLOW_UPDATE_STAGE, {
1868
- model: model.modelName,
1869
- uid: model.uid,
1870
- // TODO v6: Rename to "entry", which is what is used for regular CRUD updates
1871
- entity: {
1872
- // @ts-expect-error
1873
- id: result?.id,
1874
- documentId,
1875
- // @ts-expect-error
1876
- locale: result?.locale,
1877
- status: 'draft'
1878
- },
1879
- workflow: {
1880
- id: previousStage.workflow.id,
1881
- stages: {
1882
- from: {
1883
- id: previousStage.id,
1884
- name: previousStage.name
1885
- },
1886
- to: {
1887
- id: updatedStage.id,
1888
- name: updatedStage.name
1889
- }
1890
- }
1891
- }
1892
- });
1893
- }
1894
- return next();
1895
- };
1896
- /**
1897
- * Check if the entity is at the required stage before publish
1898
- */ const checkStageBeforePublish = async (ctx, next)=>{
1899
- if (ctx.action !== 'publish') {
1900
- return next();
1901
- }
1902
- const workflow = await getService('workflows').getAssignedWorkflow(ctx.contentType.uid, {
1903
- populate: 'stageRequiredToPublish'
1904
- });
1905
- if (!workflow || !workflow.stageRequiredToPublish) {
1906
- return next();
1907
- }
1908
- const { documentId } = ctx.params;
1909
- const entryStage = await getEntityStage(ctx.contentType.uid, documentId, ctx.params);
1910
- if (entryStage.id !== workflow.stageRequiredToPublish.id) {
1911
- throw new errors.ValidationError('Entry is not at the required stage to publish');
1912
- }
1913
- return next();
1914
- };
1915
- var documentServiceMiddleware = (()=>({
1916
- assignStageOnCreate,
1917
- handleStageOnUpdate,
1918
- checkStageBeforePublish
1919
- }));
1920
-
1921
- var services = {
1922
- workflows: workflows$1,
1923
- stages: stages$1,
1924
- 'stage-permissions': stagePermissions,
1925
- assignees: assignees$1,
1926
- validation: reviewWorkflowsValidation,
1927
- 'document-service-middlewares': documentServiceMiddleware,
1928
- 'workflow-metrics': reviewWorkflowsMetrics,
1929
- 'workflow-weekly-metrics': reviewWorkflowsWeeklyMetrics
1930
- };
1931
-
1932
- const stageObject = yup.object().shape({
1933
- id: yup.number().integer().min(1),
1934
- name: yup.string().max(255).required(),
1935
- color: yup.string().matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i),
1936
- permissions: yup.array().of(yup.object().shape({
1937
- role: yup.number().integer().min(1).required(),
1938
- action: yup.string().oneOf([
1939
- STAGE_TRANSITION_UID
1940
- ]).required(),
1941
- actionParameters: yup.object().shape({
1942
- from: yup.number().integer().min(1).required(),
1943
- to: yup.number().integer().min(1)
1944
- })
1945
- }))
1946
- });
1947
- const validateUpdateStageOnEntitySchema = yup.object().shape({
1948
- id: yup.number().integer().min(1).required()
1949
- }).required();
1950
- const validateContentTypes = yup.array().of(yup.string().test({
1951
- name: 'content-type-exists',
1952
- message: (value)=>`Content type ${value.originalValue} does not exist`,
1953
- test (uid) {
1954
- // Warning; we use the strapi global - to avoid that, it would need to refactor how
1955
- // we generate validation function by using a factory with the strapi instance as parameter.
1956
- return !!strapi.getModel(uid);
1957
- }
1958
- }).test({
1959
- name: 'content-type-review-workflow-enabled',
1960
- message: (value)=>`Content type ${value.originalValue} does not have review workflow enabled`,
1961
- test (uid) {
1962
- const model = strapi.getModel(uid);
1963
- // It's not a valid content type if it doesn't have the stage attribute
1964
- return hasStageAttribute(model);
1965
- }
1966
- }));
1967
- const validateWorkflowCreateSchema = yup.object().shape({
1968
- name: yup.string().max(255).min(1, 'Workflow name can not be empty').required(),
1969
- stages: yup.array().of(stageObject)// @ts-expect-error - add unique property into the yup namespace typing
1970
- .uniqueProperty('name', 'Stage name must be unique').min(1, 'Can not create a workflow without stages').max(200, 'Can not have more than 200 stages').required('Can not create a workflow without stages'),
1971
- contentTypes: validateContentTypes,
1972
- stageRequiredToPublishName: yup.string().min(1).nullable()
1973
- });
1974
- const validateWorkflowUpdateSchema = yup.object().shape({
1975
- name: yup.string().max(255).min(1, 'Workflow name can not be empty'),
1976
- stages: yup.array().of(stageObject)// @ts-expect-error - add unique property into the yup namespace typing
1977
- .uniqueProperty('name', 'Stage name must be unique').min(1, 'Can not update a workflow without stages').max(200, 'Can not have more than 200 stages'),
1978
- contentTypes: validateContentTypes,
1979
- stageRequiredToPublishName: yup.string().min(1).nullable()
1980
- });
1981
- const validateUpdateAssigneeOnEntitySchema = yup.object().shape({
1982
- id: yup.number().integer().min(1).nullable()
1983
- }).required();
1984
- const validateLocaleSchema = yup.string().nullable();
1985
- const validateWorkflowCreate = validateYupSchema(validateWorkflowCreateSchema);
1986
- const validateUpdateStageOnEntity = validateYupSchema(validateUpdateStageOnEntitySchema);
1987
- const validateUpdateAssigneeOnEntity = validateYupSchema(validateUpdateAssigneeOnEntitySchema);
1988
- const validateWorkflowUpdate = validateYupSchema(validateWorkflowUpdateSchema);
1989
- const validateLocale = validateYupSchema(validateLocaleSchema);
1990
-
1991
- /**
1992
- *
1993
- * @param { Core.Strapi } strapi - Strapi instance
1994
- * @param userAbility
1995
- * @return { PermissionChecker }
1996
- */ function getWorkflowsPermissionChecker({ strapi: strapi1 }, userAbility) {
1997
- return strapi1.plugin('content-manager').service('permission-checker').create({
1998
- userAbility,
1999
- model: WORKFLOW_MODEL_UID
2000
- });
2001
- }
2002
- /**
2003
- * Transforms workflow to an admin UI format.
2004
- * Some attributes (like permissions) are presented in a different format in the admin UI.
2005
- * @param {Workflow} workflow
2006
- */ function formatWorkflowToAdmin(workflow) {
2007
- if (!workflow) return;
2008
- if (!workflow.stages) return workflow;
2009
- // Transform permissions roles to be the id string instead of an object
2010
- const transformPermissions = map(update('role', property('id')));
2011
- const transformStages = map(update('permissions', transformPermissions));
2012
- return update('stages', transformStages, workflow);
2013
- }
2014
- var workflows = {
2015
- /**
2016
- * Create a new workflow
2017
- * @param {import('koa').BaseContext} ctx - koa context
2018
- */ async create (ctx) {
2019
- const { body, query } = ctx.request;
2020
- const { sanitizeCreateInput, sanitizeOutput, sanitizedQuery } = getWorkflowsPermissionChecker({
2021
- strapi
2022
- }, ctx.state.userAbility);
2023
- const { populate } = await sanitizedQuery.create(query);
2024
- const workflowBody = await validateWorkflowCreate(body.data);
2025
- const workflowService = getService('workflows');
2026
- const createdWorkflow = await workflowService.create({
2027
- data: await sanitizeCreateInput(workflowBody),
2028
- populate
2029
- }).then(formatWorkflowToAdmin);
2030
- ctx.created({
2031
- data: await sanitizeOutput(createdWorkflow)
2032
- });
2033
- },
2034
- /**
2035
- * Update a workflow
2036
- * @param {import('koa').BaseContext} ctx - koa context
2037
- */ async update (ctx) {
2038
- const { id } = ctx.params;
2039
- const { body, query } = ctx.request;
2040
- const workflowService = getService('workflows');
2041
- const { sanitizeUpdateInput, sanitizeOutput, sanitizedQuery } = getWorkflowsPermissionChecker({
2042
- strapi
2043
- }, ctx.state.userAbility);
2044
- const { populate } = await sanitizedQuery.update(query);
2045
- const workflowBody = await validateWorkflowUpdate(body.data);
2046
- // Find if workflow exists
2047
- const workflow = await workflowService.findById(id, {
2048
- populate: WORKFLOW_POPULATE
2049
- });
2050
- if (!workflow) {
2051
- return ctx.notFound();
2052
- }
2053
- // Sanitize input data
2054
- const getPermittedFieldToUpdate = sanitizeUpdateInput(workflow);
2055
- const dataToUpdate = await getPermittedFieldToUpdate(workflowBody);
2056
- // Update workflow
2057
- const updatedWorkflow = await workflowService.update(workflow, {
2058
- data: dataToUpdate,
2059
- populate
2060
- }).then(formatWorkflowToAdmin);
2061
- // Send sanitized response
2062
- ctx.body = {
2063
- data: await sanitizeOutput(updatedWorkflow)
2064
- };
2065
- },
2066
- /**
2067
- * Delete a workflow
2068
- * @param {import('koa').BaseContext} ctx - koa context
2069
- */ async delete (ctx) {
2070
- const { id } = ctx.params;
2071
- const { query } = ctx.request;
2072
- const workflowService = getService('workflows');
2073
- const { sanitizeOutput, sanitizedQuery } = getWorkflowsPermissionChecker({
2074
- strapi
2075
- }, ctx.state.userAbility);
2076
- const { populate } = await sanitizedQuery.delete(query);
2077
- const workflow = await workflowService.findById(id, {
2078
- populate: WORKFLOW_POPULATE
2079
- });
2080
- if (!workflow) {
2081
- return ctx.notFound("Workflow doesn't exist");
2082
- }
2083
- const deletedWorkflow = await workflowService.delete(workflow, {
2084
- populate
2085
- }).then(formatWorkflowToAdmin);
2086
- ctx.body = {
2087
- data: await sanitizeOutput(deletedWorkflow)
2088
- };
2089
- },
2090
- /**
2091
- * List all workflows
2092
- * @param {import('koa').BaseContext} ctx - koa context
2093
- */ async find (ctx) {
2094
- const { query } = ctx.request;
2095
- const workflowService = getService('workflows');
2096
- const { sanitizeOutput, sanitizedQuery } = getWorkflowsPermissionChecker({
2097
- strapi
2098
- }, ctx.state.userAbility);
2099
- const { populate, filters, sort } = await sanitizedQuery.read(query);
2100
- const [workflows, workflowCount] = await Promise.all([
2101
- workflowService.find({
2102
- populate,
2103
- filters,
2104
- sort
2105
- }).then(map(formatWorkflowToAdmin)),
2106
- workflowService.count()
2107
- ]);
2108
- ctx.body = {
2109
- data: await async.map(workflows, sanitizeOutput),
2110
- meta: {
2111
- workflowCount
2112
- }
2113
- };
2114
- }
2115
- };
2116
-
2117
- /**
2118
- *
2119
- * @param { Core.Strapi } strapi - Strapi instance
2120
- * @param userAbility
2121
- * @return { (Stage) => SanitizedStage }
2122
- */ function sanitizeStage({ strapi: strapi1 }, userAbility) {
2123
- const permissionChecker = strapi1.plugin('content-manager').service('permission-checker').create({
2124
- userAbility,
2125
- model: STAGE_MODEL_UID
2126
- });
2127
- return (entity)=>permissionChecker.sanitizeOutput(entity);
2128
- }
2129
- var stages = {
2130
- /**
2131
- * List all stages
2132
- * @param {import('koa').BaseContext} ctx - koa context
2133
- */ async find (ctx) {
2134
- const { workflow_id: workflowId } = ctx.params;
2135
- const { populate } = ctx.query;
2136
- const stagesService = getService('stages');
2137
- const sanitizer = sanitizeStage({
2138
- strapi
2139
- }, ctx.state.userAbility);
2140
- const stages = await stagesService.find({
2141
- workflowId,
2142
- populate
2143
- });
2144
- ctx.body = {
2145
- data: await async.map(stages, sanitizer)
2146
- };
2147
- },
2148
- /**
2149
- * Get one stage
2150
- * @param {import('koa').BaseContext} ctx - koa context
2151
- */ async findById (ctx) {
2152
- const { id, workflow_id: workflowId } = ctx.params;
2153
- const { populate } = ctx.query;
2154
- const stagesService = getService('stages');
2155
- const sanitizer = sanitizeStage({
2156
- strapi
2157
- }, ctx.state.userAbility);
2158
- const stage = await stagesService.findById(id, {
2159
- workflowId,
2160
- populate
2161
- });
2162
- ctx.body = {
2163
- data: await sanitizer(stage)
2164
- };
2165
- },
2166
- /**
2167
- * Updates an entity's stage.
2168
- * @async
2169
- * @param {Object} ctx - The Koa context object.
2170
- * @param {Object} ctx.params - An object containing the parameters from the request URL.
2171
- * @param {string} ctx.params.model_uid - The model UID of the entity.
2172
- * @param {string} ctx.params.id - The ID of the entity to update.
2173
- * @param {Object} ctx.request.body.data - Optional data object containing the new stage ID for the entity.
2174
- * @param {string} ctx.request.body.data.id - The ID of the new stage for the entity.
2175
- * @throws {ApplicationError} If review workflows is not activated on the specified model UID.
2176
- * @throws {ValidationError} If the `data` object in the request body fails to pass validation.
2177
- * @returns {Promise<void>} A promise that resolves when the entity's stage has been updated.
2178
- */ async updateEntity (ctx) {
2179
- const stagesService = getService('stages');
2180
- const stagePermissions = getService('stage-permissions');
2181
- const workflowService = getService('workflows');
2182
- const { model_uid: modelUID, id: documentId } = ctx.params;
2183
- const { body, query = {} } = ctx.request;
2184
- const { sanitizeOutput } = strapi.plugin('content-manager').service('permission-checker').create({
2185
- userAbility: ctx.state.userAbility,
2186
- model: modelUID
2187
- });
2188
- // Load entity
2189
- const locale = await validateLocale(query?.locale);
2190
- const entity = await strapi.documents(modelUID).findOne({
2191
- documentId,
2192
- // @ts-expect-error - locale should be also null in the doc service types
2193
- locale,
2194
- populate: [
2195
- ENTITY_STAGE_ATTRIBUTE
2196
- ]
2197
- });
2198
- if (!entity) {
2199
- ctx.throw(404, 'Entity not found');
2200
- }
2201
- // Validate if entity stage can be updated
2202
- const canTransition = stagePermissions.can(STAGE_TRANSITION_UID, entity[ENTITY_STAGE_ATTRIBUTE]?.id);
2203
- if (!canTransition) {
2204
- ctx.throw(403, 'Forbidden stage transition');
2205
- }
2206
- const { id: stageId } = await validateUpdateStageOnEntity({
2207
- id: Number(body?.data?.id)
2208
- }, 'You should pass an id to the body of the put request.');
2209
- const workflow = await workflowService.assertContentTypeBelongsToWorkflow(modelUID);
2210
- workflowService.assertStageBelongsToWorkflow(stageId, workflow);
2211
- const updatedEntity = await stagesService.updateEntity(entity, modelUID, stageId);
2212
- ctx.body = {
2213
- data: await sanitizeOutput(updatedEntity)
2214
- };
2215
- },
2216
- /**
2217
- * List all the stages that are available for a user to transition an entity to.
2218
- * If the user has permission to change the current stage of the entity every other stage in the workflow is returned
2219
- * @async
2220
- * @param {*} ctx
2221
- * @param {string} ctx.params.model_uid - The model UID of the entity.
2222
- * @param {string} ctx.params.id - The ID of the entity.
2223
- * @throws {ApplicationError} If review workflows is not activated on the specified model UID.
2224
- */ async listAvailableStages (ctx) {
2225
- const stagePermissions = getService('stage-permissions');
2226
- const workflowService = getService('workflows');
2227
- const { model_uid: modelUID, id: documentId } = ctx.params;
2228
- const { query = {} } = ctx.request;
2229
- if (strapi.plugin('content-manager').service('permission-checker').create({
2230
- userAbility: ctx.state.userAbility,
2231
- model: modelUID
2232
- }).cannot.read()) {
2233
- return ctx.forbidden();
2234
- }
2235
- // Load entity
2236
- const locale = await validateLocale(query?.locale) ?? undefined;
2237
- const entity = await strapi.documents(modelUID).findOne({
2238
- documentId,
2239
- locale,
2240
- populate: [
2241
- ENTITY_STAGE_ATTRIBUTE
2242
- ]
2243
- });
2244
- if (!entity) {
2245
- ctx.throw(404, 'Entity not found');
2246
- }
2247
- const entityStageId = entity[ENTITY_STAGE_ATTRIBUTE]?.id;
2248
- const canTransition = stagePermissions.can(STAGE_TRANSITION_UID, entityStageId);
2249
- const [workflowCount, workflowResult] = await Promise.all([
2250
- workflowService.count(),
2251
- workflowService.getAssignedWorkflow(modelUID, {
2252
- populate: 'stages'
2253
- })
2254
- ]);
2255
- const workflowStages = workflowResult ? workflowResult.stages : [];
2256
- const meta = {
2257
- stageCount: workflowStages.length,
2258
- workflowCount
2259
- };
2260
- if (!canTransition) {
2261
- ctx.body = {
2262
- data: [],
2263
- meta
2264
- };
2265
- return;
2266
- }
2267
- const data = workflowStages.filter((stage)=>stage.id !== entityStageId);
2268
- ctx.body = {
2269
- data,
2270
- meta
2271
- };
2272
- }
2273
- };
2274
-
2275
- var assignees = {
2276
- /**
2277
- * Updates an entity's assignee.
2278
- * @async
2279
- * @param {Object} ctx - The Koa context object.
2280
- * @param {Object} ctx.params - An object containing the parameters from the request URL.
2281
- * @param {string} ctx.params.model_uid - The model UID of the entity.
2282
- * @param {string} ctx.params.id - The ID of the entity to update.
2283
- * @param {Object} ctx.request.body.data - Optional data object containing the new assignee ID for the entity.
2284
- * @param {string} ctx.request.body.data.id - The ID of the new assignee for the entity.
2285
- * @throws {ApplicationError} If review workflows is not activated on the specified model UID.
2286
- * @throws {ValidationError} If the `data` object in the request body fails to pass validation.
2287
- * @returns {Promise<void>} A promise that resolves when the entity's assignee has been updated.
2288
- */ async updateEntity (ctx) {
2289
- const assigneeService = getService('assignees');
2290
- const workflowService = getService('workflows');
2291
- const stagePermissions = getService('stage-permissions');
2292
- const { model_uid: model, id: documentId } = ctx.params;
2293
- const locale = await validateLocale(ctx.request.query?.locale) ?? undefined;
2294
- const { sanitizeOutput } = strapi.plugin('content-manager').service('permission-checker').create({
2295
- userAbility: ctx.state.userAbility,
2296
- model
2297
- });
2298
- // Retrieve the entity so we can get its current stage
2299
- const entity = await strapi.documents(model).findOne({
2300
- documentId,
2301
- locale,
2302
- populate: [
2303
- ENTITY_STAGE_ATTRIBUTE
2304
- ]
2305
- });
2306
- if (!entity) {
2307
- ctx.throw(404, 'Entity not found');
2308
- }
2309
- // Only allow users who can update the current stage to change the assignee
2310
- const canTransitionStage = stagePermissions.can(STAGE_TRANSITION_UID, entity[ENTITY_STAGE_ATTRIBUTE]?.id);
2311
- if (!canTransitionStage) {
2312
- ctx.throw(403, 'Stage transition permission is required');
2313
- }
2314
- // TODO: check if user has update permission on the entity
2315
- const { id: assigneeId } = await validateUpdateAssigneeOnEntity(ctx.request?.body?.data, 'You should pass a valid id to the body of the put request.');
2316
- await workflowService.assertContentTypeBelongsToWorkflow(model);
2317
- const updatedEntity = await assigneeService.updateEntityAssignee(entity, model, assigneeId);
2318
- ctx.body = {
2319
- data: await sanitizeOutput(updatedEntity)
2320
- };
2321
- }
2322
- };
2323
-
2324
- var controllers = {
2325
- workflows,
2326
- stages,
2327
- assignees
2328
- };
1
+ import register from './register.mjs';
2
+ import contentTypes from './content-types/index.mjs';
3
+ import bootstrap from './bootstrap.mjs';
4
+ import destroy from './destroy.mjs';
5
+ import routes from './routes/index.mjs';
6
+ import services from './services/index.mjs';
7
+ import controllers from './controllers/index.mjs';
2329
8
 
2330
9
  const getPlugin = ()=>{
2331
10
  if (strapi.ee.features.isEnabled('review-workflows')) {