@strapi/review-workflows 5.12.0 → 5.12.2

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