@strapi/content-manager 5.59.0 → 5.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/dist/admin/constants/customOrder.js +14 -0
  2. package/dist/admin/constants/customOrder.js.map +1 -0
  3. package/dist/admin/constants/customOrder.mjs +10 -0
  4. package/dist/admin/constants/customOrder.mjs.map +1 -0
  5. package/dist/admin/history/components/VersionInputRenderer.js +2 -1
  6. package/dist/admin/history/components/VersionInputRenderer.js.map +1 -1
  7. package/dist/admin/history/components/VersionInputRenderer.mjs +2 -1
  8. package/dist/admin/history/components/VersionInputRenderer.mjs.map +1 -1
  9. package/dist/admin/hooks/useDocument.js +22 -11
  10. package/dist/admin/hooks/useDocument.js.map +1 -1
  11. package/dist/admin/hooks/useDocument.mjs +22 -11
  12. package/dist/admin/hooks/useDocument.mjs.map +1 -1
  13. package/dist/admin/hooks/useDocumentLayout.js.map +1 -1
  14. package/dist/admin/hooks/useDocumentLayout.mjs.map +1 -1
  15. package/dist/admin/hooks/useHasBeenNearViewport.js +70 -0
  16. package/dist/admin/hooks/useHasBeenNearViewport.js.map +1 -0
  17. package/dist/admin/hooks/useHasBeenNearViewport.mjs +46 -0
  18. package/dist/admin/hooks/useHasBeenNearViewport.mjs.map +1 -0
  19. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.js +102 -32
  20. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.js.map +1 -1
  21. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.mjs +104 -35
  22. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.mjs.map +1 -1
  23. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.js +20 -17
  24. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.js.map +1 -1
  25. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.mjs +20 -17
  26. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/List.mjs.map +1 -1
  27. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.js +15 -1
  28. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.js.map +1 -1
  29. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.mjs +17 -3
  30. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.mjs.map +1 -1
  31. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.js +21 -0
  32. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.js.map +1 -1
  33. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.mjs +22 -1
  34. package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/plugins/withStrapiSchema.mjs.map +1 -1
  35. package/dist/admin/pages/EditView/components/FormInputs/Component/NonRepeatable.js +1 -0
  36. package/dist/admin/pages/EditView/components/FormInputs/Component/NonRepeatable.js.map +1 -1
  37. package/dist/admin/pages/EditView/components/FormInputs/Component/NonRepeatable.mjs +1 -0
  38. package/dist/admin/pages/EditView/components/FormInputs/Component/NonRepeatable.mjs.map +1 -1
  39. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.js +1 -0
  40. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.js.map +1 -1
  41. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.mjs +1 -0
  42. package/dist/admin/pages/EditView/components/FormInputs/Component/Repeatable.mjs.map +1 -1
  43. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.js +2 -0
  44. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.js.map +1 -1
  45. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.mjs +2 -0
  46. package/dist/admin/pages/EditView/components/FormInputs/DynamicZone/DynamicComponent.mjs.map +1 -1
  47. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.js +4 -7
  48. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.js.map +1 -1
  49. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.mjs +4 -7
  50. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.mjs.map +1 -1
  51. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js +46 -10
  52. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js.map +1 -1
  53. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs +47 -11
  54. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs.map +1 -1
  55. package/dist/admin/pages/EditView/components/FormLayout.js +201 -50
  56. package/dist/admin/pages/EditView/components/FormLayout.js.map +1 -1
  57. package/dist/admin/pages/EditView/components/FormLayout.mjs +202 -52
  58. package/dist/admin/pages/EditView/components/FormLayout.mjs.map +1 -1
  59. package/dist/admin/pages/EditView/components/InputRenderer.js +3 -1
  60. package/dist/admin/pages/EditView/components/InputRenderer.js.map +1 -1
  61. package/dist/admin/pages/EditView/components/InputRenderer.mjs +3 -1
  62. package/dist/admin/pages/EditView/components/InputRenderer.mjs.map +1 -1
  63. package/dist/admin/pages/ListConfiguration/components/Settings.js +31 -2
  64. package/dist/admin/pages/ListConfiguration/components/Settings.js.map +1 -1
  65. package/dist/admin/pages/ListConfiguration/components/Settings.mjs +31 -2
  66. package/dist/admin/pages/ListConfiguration/components/Settings.mjs.map +1 -1
  67. package/dist/admin/pages/ListView/ListViewPage.js +196 -132
  68. package/dist/admin/pages/ListView/ListViewPage.js.map +1 -1
  69. package/dist/admin/pages/ListView/ListViewPage.mjs +196 -132
  70. package/dist/admin/pages/ListView/ListViewPage.mjs.map +1 -1
  71. package/dist/admin/pages/ListView/components/CustomOrder.js +571 -0
  72. package/dist/admin/pages/ListView/components/CustomOrder.js.map +1 -0
  73. package/dist/admin/pages/ListView/components/CustomOrder.mjs +546 -0
  74. package/dist/admin/pages/ListView/components/CustomOrder.mjs.map +1 -0
  75. package/dist/admin/pages/ListView/components/TableActions.js +4 -1
  76. package/dist/admin/pages/ListView/components/TableActions.js.map +1 -1
  77. package/dist/admin/pages/ListView/components/TableActions.mjs +4 -1
  78. package/dist/admin/pages/ListView/components/TableActions.mjs.map +1 -1
  79. package/dist/admin/preview/components/PreviewSidePanel.js +2 -1
  80. package/dist/admin/preview/components/PreviewSidePanel.js.map +1 -1
  81. package/dist/admin/preview/components/PreviewSidePanel.mjs +2 -1
  82. package/dist/admin/preview/components/PreviewSidePanel.mjs.map +1 -1
  83. package/dist/admin/preview/utils/getPreviewUrlStatus.js +17 -0
  84. package/dist/admin/preview/utils/getPreviewUrlStatus.js.map +1 -0
  85. package/dist/admin/preview/utils/getPreviewUrlStatus.mjs +15 -0
  86. package/dist/admin/preview/utils/getPreviewUrlStatus.mjs.map +1 -0
  87. package/dist/admin/services/documents.js +22 -1
  88. package/dist/admin/services/documents.js.map +1 -1
  89. package/dist/admin/services/documents.mjs +21 -2
  90. package/dist/admin/services/documents.mjs.map +1 -1
  91. package/dist/admin/src/constants/customOrder.d.ts +8 -0
  92. package/dist/admin/src/exports.d.ts +1 -1
  93. package/dist/admin/src/history/components/VersionInputRenderer.d.ts +3 -1
  94. package/dist/admin/src/hooks/useDocument.d.ts +4 -0
  95. package/dist/admin/src/hooks/useDocumentLayout.d.ts +6 -2
  96. package/dist/admin/src/hooks/useHasBeenNearViewport.d.ts +13 -0
  97. package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/Blocks/Image.d.ts +13 -1
  98. package/dist/admin/src/pages/EditView/components/FormLayout.d.ts +11 -1
  99. package/dist/admin/src/pages/EditView/components/InputRenderer.d.ts +2 -0
  100. package/dist/admin/src/pages/ListView/components/CustomOrder.d.ts +72 -0
  101. package/dist/admin/src/preview/utils/getPreviewUrlStatus.d.ts +6 -0
  102. package/dist/admin/src/services/documents.d.ts +10 -2
  103. package/dist/admin/translations/en.json.js +9 -0
  104. package/dist/admin/translations/en.json.js.map +1 -1
  105. package/dist/admin/translations/en.json.mjs +9 -0
  106. package/dist/admin/translations/en.json.mjs.map +1 -1
  107. package/dist/server/audit-logs.js +36 -0
  108. package/dist/server/audit-logs.js.map +1 -0
  109. package/dist/server/audit-logs.mjs +33 -0
  110. package/dist/server/audit-logs.mjs.map +1 -0
  111. package/dist/server/bootstrap.js +8 -0
  112. package/dist/server/bootstrap.js.map +1 -1
  113. package/dist/server/bootstrap.mjs +8 -0
  114. package/dist/server/bootstrap.mjs.map +1 -1
  115. package/dist/server/controllers/collection-types.js +8 -0
  116. package/dist/server/controllers/collection-types.js.map +1 -1
  117. package/dist/server/controllers/collection-types.mjs +8 -0
  118. package/dist/server/controllers/collection-types.mjs.map +1 -1
  119. package/dist/server/controllers/content-types.js +15 -0
  120. package/dist/server/controllers/content-types.js.map +1 -1
  121. package/dist/server/controllers/content-types.mjs +16 -1
  122. package/dist/server/controllers/content-types.mjs.map +1 -1
  123. package/dist/server/controllers/index.js +3 -1
  124. package/dist/server/controllers/index.js.map +1 -1
  125. package/dist/server/controllers/index.mjs +2 -0
  126. package/dist/server/controllers/index.mjs.map +1 -1
  127. package/dist/server/controllers/relations.js +53 -17
  128. package/dist/server/controllers/relations.js.map +1 -1
  129. package/dist/server/controllers/relations.mjs +54 -18
  130. package/dist/server/controllers/relations.mjs.map +1 -1
  131. package/dist/server/controllers/single-types.js +10 -1
  132. package/dist/server/controllers/single-types.js.map +1 -1
  133. package/dist/server/controllers/single-types.mjs +10 -1
  134. package/dist/server/controllers/single-types.mjs.map +1 -1
  135. package/dist/server/controllers/validation/model-configuration.js +2 -1
  136. package/dist/server/controllers/validation/model-configuration.js.map +1 -1
  137. package/dist/server/controllers/validation/model-configuration.mjs +2 -1
  138. package/dist/server/controllers/validation/model-configuration.mjs.map +1 -1
  139. package/dist/server/custom-order/constants.js +21 -0
  140. package/dist/server/custom-order/constants.js.map +1 -0
  141. package/dist/server/custom-order/constants.mjs +17 -0
  142. package/dist/server/custom-order/constants.mjs.map +1 -0
  143. package/dist/server/custom-order/controllers/custom-order.js +77 -0
  144. package/dist/server/custom-order/controllers/custom-order.js.map +1 -0
  145. package/dist/server/custom-order/controllers/custom-order.mjs +75 -0
  146. package/dist/server/custom-order/controllers/custom-order.mjs.map +1 -0
  147. package/dist/server/custom-order/controllers/index.js +10 -0
  148. package/dist/server/custom-order/controllers/index.js.map +1 -0
  149. package/dist/server/custom-order/controllers/index.mjs +8 -0
  150. package/dist/server/custom-order/controllers/index.mjs.map +1 -0
  151. package/dist/server/custom-order/index.js +71 -0
  152. package/dist/server/custom-order/index.js.map +1 -0
  153. package/dist/server/custom-order/index.mjs +69 -0
  154. package/dist/server/custom-order/index.mjs.map +1 -0
  155. package/dist/server/custom-order/routes/custom-order.js +38 -0
  156. package/dist/server/custom-order/routes/custom-order.js.map +1 -0
  157. package/dist/server/custom-order/routes/custom-order.mjs +36 -0
  158. package/dist/server/custom-order/routes/custom-order.mjs.map +1 -0
  159. package/dist/server/custom-order/routes/index.js +13 -0
  160. package/dist/server/custom-order/routes/index.js.map +1 -0
  161. package/dist/server/custom-order/routes/index.mjs +11 -0
  162. package/dist/server/custom-order/routes/index.mjs.map +1 -0
  163. package/dist/server/custom-order/services/custom-order.js +286 -0
  164. package/dist/server/custom-order/services/custom-order.js.map +1 -0
  165. package/dist/server/custom-order/services/custom-order.mjs +284 -0
  166. package/dist/server/custom-order/services/custom-order.mjs.map +1 -0
  167. package/dist/server/custom-order/services/document-middleware.js +83 -0
  168. package/dist/server/custom-order/services/document-middleware.js.map +1 -0
  169. package/dist/server/custom-order/services/document-middleware.mjs +81 -0
  170. package/dist/server/custom-order/services/document-middleware.mjs.map +1 -0
  171. package/dist/server/custom-order/services/index.js +10 -0
  172. package/dist/server/custom-order/services/index.js.map +1 -0
  173. package/dist/server/custom-order/services/index.mjs +8 -0
  174. package/dist/server/custom-order/services/index.mjs.map +1 -0
  175. package/dist/server/custom-order/services/utils.js +152 -0
  176. package/dist/server/custom-order/services/utils.js.map +1 -0
  177. package/dist/server/custom-order/services/utils.mjs +148 -0
  178. package/dist/server/custom-order/services/utils.mjs.map +1 -0
  179. package/dist/server/custom-order/utils.js +13 -0
  180. package/dist/server/custom-order/utils.js.map +1 -0
  181. package/dist/server/custom-order/utils.mjs +10 -0
  182. package/dist/server/custom-order/utils.mjs.map +1 -0
  183. package/dist/server/history/constants.js +2 -1
  184. package/dist/server/history/constants.js.map +1 -1
  185. package/dist/server/history/constants.mjs +2 -1
  186. package/dist/server/history/constants.mjs.map +1 -1
  187. package/dist/server/history/controllers/history-version.js +6 -2
  188. package/dist/server/history/controllers/history-version.js.map +1 -1
  189. package/dist/server/history/controllers/history-version.mjs +6 -2
  190. package/dist/server/history/controllers/history-version.mjs.map +1 -1
  191. package/dist/server/history/services/history.js +24 -7
  192. package/dist/server/history/services/history.js.map +1 -1
  193. package/dist/server/history/services/history.mjs +25 -8
  194. package/dist/server/history/services/history.mjs.map +1 -1
  195. package/dist/server/history/services/lifecycles.js +2 -16
  196. package/dist/server/history/services/lifecycles.js.map +1 -1
  197. package/dist/server/history/services/lifecycles.mjs +2 -16
  198. package/dist/server/history/services/lifecycles.mjs.map +1 -1
  199. package/dist/server/mcp/schemas/filters-schema.js +10 -2
  200. package/dist/server/mcp/schemas/filters-schema.js.map +1 -1
  201. package/dist/server/mcp/schemas/filters-schema.mjs +10 -2
  202. package/dist/server/mcp/schemas/filters-schema.mjs.map +1 -1
  203. package/dist/server/preview/controllers/validation/preview.js +1 -1
  204. package/dist/server/preview/controllers/validation/preview.js.map +1 -1
  205. package/dist/server/preview/controllers/validation/preview.mjs +2 -2
  206. package/dist/server/preview/controllers/validation/preview.mjs.map +1 -1
  207. package/dist/server/register.js +4 -0
  208. package/dist/server/register.js.map +1 -1
  209. package/dist/server/register.mjs +4 -0
  210. package/dist/server/register.mjs.map +1 -1
  211. package/dist/server/routes/index.js +3 -1
  212. package/dist/server/routes/index.js.map +1 -1
  213. package/dist/server/routes/index.mjs +2 -0
  214. package/dist/server/routes/index.mjs.map +1 -1
  215. package/dist/server/services/content-types.js +2 -2
  216. package/dist/server/services/content-types.js.map +1 -1
  217. package/dist/server/services/content-types.mjs +2 -2
  218. package/dist/server/services/content-types.mjs.map +1 -1
  219. package/dist/server/services/document-manager.js +13 -0
  220. package/dist/server/services/document-manager.js.map +1 -1
  221. package/dist/server/services/document-manager.mjs +14 -1
  222. package/dist/server/services/document-manager.mjs.map +1 -1
  223. package/dist/server/services/document-metadata.js +1 -1
  224. package/dist/server/services/document-metadata.js.map +1 -1
  225. package/dist/server/services/document-metadata.mjs +1 -1
  226. package/dist/server/services/document-metadata.mjs.map +1 -1
  227. package/dist/server/services/index.js +3 -1
  228. package/dist/server/services/index.js.map +1 -1
  229. package/dist/server/services/index.mjs +2 -0
  230. package/dist/server/services/index.mjs.map +1 -1
  231. package/dist/server/services/utils/configuration/settings.js +2 -1
  232. package/dist/server/services/utils/configuration/settings.js.map +1 -1
  233. package/dist/server/services/utils/configuration/settings.mjs +2 -1
  234. package/dist/server/services/utils/configuration/settings.mjs.map +1 -1
  235. package/dist/server/src/audit-logs.d.ts +40 -0
  236. package/dist/server/src/audit-logs.d.ts.map +1 -0
  237. package/dist/server/src/bootstrap.d.ts.map +1 -1
  238. package/dist/server/src/controllers/collection-types.d.ts +1 -1
  239. package/dist/server/src/controllers/collection-types.d.ts.map +1 -1
  240. package/dist/server/src/controllers/content-types.d.ts.map +1 -1
  241. package/dist/server/src/controllers/index.d.ts +21 -3
  242. package/dist/server/src/controllers/index.d.ts.map +1 -1
  243. package/dist/server/src/controllers/relations.d.ts +19 -1
  244. package/dist/server/src/controllers/relations.d.ts.map +1 -1
  245. package/dist/server/src/controllers/single-types.d.ts +1 -1
  246. package/dist/server/src/controllers/single-types.d.ts.map +1 -1
  247. package/dist/server/src/custom-order/constants.d.ts +18 -0
  248. package/dist/server/src/custom-order/constants.d.ts.map +1 -0
  249. package/dist/server/src/custom-order/controllers/custom-order.d.ts +8 -0
  250. package/dist/server/src/custom-order/controllers/custom-order.d.ts.map +1 -0
  251. package/dist/server/src/custom-order/controllers/index.d.ts +3 -0
  252. package/dist/server/src/custom-order/controllers/index.d.ts.map +1 -0
  253. package/dist/server/src/custom-order/index.d.ts +8 -0
  254. package/dist/server/src/custom-order/index.d.ts.map +1 -0
  255. package/dist/server/src/custom-order/routes/custom-order.d.ts +4 -0
  256. package/dist/server/src/custom-order/routes/custom-order.d.ts.map +1 -0
  257. package/dist/server/src/custom-order/routes/index.d.ts +10 -0
  258. package/dist/server/src/custom-order/routes/index.d.ts.map +1 -0
  259. package/dist/server/src/custom-order/services/custom-order.d.ts +43 -0
  260. package/dist/server/src/custom-order/services/custom-order.d.ts.map +1 -0
  261. package/dist/server/src/custom-order/services/document-middleware.d.ts +16 -0
  262. package/dist/server/src/custom-order/services/document-middleware.d.ts.map +1 -0
  263. package/dist/server/src/custom-order/services/index.d.ts +24 -0
  264. package/dist/server/src/custom-order/services/index.d.ts.map +1 -0
  265. package/dist/server/src/custom-order/services/utils.d.ts +34 -0
  266. package/dist/server/src/custom-order/services/utils.d.ts.map +1 -0
  267. package/dist/server/src/custom-order/utils.d.ts +6 -0
  268. package/dist/server/src/custom-order/utils.d.ts.map +1 -0
  269. package/dist/server/src/history/constants.d.ts.map +1 -1
  270. package/dist/server/src/history/controllers/history-version.d.ts.map +1 -1
  271. package/dist/server/src/history/services/history.d.ts.map +1 -1
  272. package/dist/server/src/history/services/lifecycles.d.ts.map +1 -1
  273. package/dist/server/src/index.d.ts +22 -6
  274. package/dist/server/src/index.d.ts.map +1 -1
  275. package/dist/server/src/mcp/schemas/filters-schema.d.ts.map +1 -1
  276. package/dist/server/src/register.d.ts.map +1 -1
  277. package/dist/server/src/routes/index.d.ts.map +1 -1
  278. package/dist/server/src/services/content-types.d.ts +1 -3
  279. package/dist/server/src/services/content-types.d.ts.map +1 -1
  280. package/dist/server/src/services/document-manager.d.ts.map +1 -1
  281. package/dist/server/src/services/index.d.ts +1 -3
  282. package/dist/server/src/services/index.d.ts.map +1 -1
  283. package/dist/server/src/services/utils/configuration/settings.d.ts.map +1 -1
  284. package/dist/shared/contracts/content-types.d.ts +5 -0
  285. package/dist/shared/contracts/content-types.d.ts.map +1 -1
  286. package/dist/shared/contracts/custom-order.d.ts +33 -0
  287. package/dist/shared/contracts/custom-order.d.ts.map +1 -0
  288. package/dist/shared/contracts/custom-order.js +3 -0
  289. package/dist/shared/contracts/custom-order.js.map +1 -0
  290. package/dist/shared/contracts/custom-order.mjs +2 -0
  291. package/dist/shared/contracts/custom-order.mjs.map +1 -0
  292. package/dist/shared/contracts/history-versions.d.ts +0 -1
  293. package/dist/shared/contracts/history-versions.d.ts.map +1 -1
  294. package/dist/shared/contracts/index.d.ts +1 -0
  295. package/dist/shared/contracts/index.d.ts.map +1 -1
  296. package/dist/shared/contracts/index.js +2 -0
  297. package/dist/shared/contracts/index.js.map +1 -1
  298. package/dist/shared/contracts/index.mjs +2 -0
  299. package/dist/shared/contracts/index.mjs.map +1 -1
  300. package/package.json +10 -10
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+
3
+ var strapiUtils = require('@strapi/utils');
4
+ var store = require('../../services/utils/store.js');
5
+ var constants = require('../constants.js');
6
+ var documentMiddleware = require('./document-middleware.js');
7
+ var utils = require('./utils.js');
8
+
9
+ /**
10
+ * Core store key of the list view settings of a content type, see `services/utils/store.ts`.
11
+ */ const CONFIGURATION_KEY_PREFIX = 'plugin_content_manager_configuration_content_types::';
12
+ const createCustomOrderService = ({ strapi })=>{
13
+ const state = {
14
+ enabled: new Set(),
15
+ loadedAt: 0,
16
+ refreshId: 0,
17
+ isRefreshing: false,
18
+ isInitialized: false
19
+ };
20
+ /**
21
+ * Only the content types that received the position attribute at register can be ordered.
22
+ */ const isOrderable = (uid)=>{
23
+ const model = strapi.getModel(uid);
24
+ return model?.attributes?.[constants.POSITION_ATTRIBUTE] !== undefined;
25
+ };
26
+ /**
27
+ * Reads which content types have custom order turned on in their list view settings.
28
+ * The setting lives in the database and can be changed at runtime, possibly from
29
+ * another instance, so it can't be read once and trusted forever.
30
+ */ const refresh = async ()=>{
31
+ state.refreshId += 1;
32
+ const refreshId = state.refreshId;
33
+ const configurations = await store.getAllConfigurations();
34
+ // A more recent refresh has started in the meantime, its result is the one to keep
35
+ if (refreshId !== state.refreshId) {
36
+ return;
37
+ }
38
+ state.enabled = new Set(configurations.flatMap(({ uid, settings })=>uid && settings?.customOrder === true && isOrderable(uid) ? [
39
+ uid
40
+ ] : []));
41
+ state.loadedAt = Date.now();
42
+ };
43
+ const refreshInBackgroundIfStale = ()=>{
44
+ if (state.isRefreshing || Date.now() - state.loadedAt < constants.ENABLED_CACHE_TTL_MS) {
45
+ return;
46
+ }
47
+ state.isRefreshing = true;
48
+ refresh().then(()=>healMissingPositions()).catch((error)=>{
49
+ strapi.log.warn('[custom-order] Could not refresh the list of ordered content types', {
50
+ error
51
+ });
52
+ }).finally(()=>{
53
+ state.isRefreshing = false;
54
+ });
55
+ };
56
+ const getTable = (uid)=>{
57
+ const { tableName, attributes } = strapi.db.metadata.get(uid);
58
+ const columnOf = (attributeName)=>{
59
+ const attribute = attributes[attributeName];
60
+ return 'columnName' in attribute && attribute.columnName ? attribute.columnName : attributeName;
61
+ };
62
+ return {
63
+ tableName,
64
+ positionColumn: columnOf(constants.POSITION_ATTRIBUTE),
65
+ documentIdColumn: columnOf('documentId')
66
+ };
67
+ };
68
+ const toPosition = (value)=>value === null || value === undefined ? undefined : Number(value);
69
+ /**
70
+ * Takes the lock on the order of a content type until the current transaction ends.
71
+ *
72
+ * The position of a new document and a move both read positions before writing them. Two
73
+ * of them at the same time would give two documents the same position, and nothing could
74
+ * tell those two apart afterwards. The lock is the row holding the list view settings of
75
+ * the content type, which exists whenever custom order is on. `FOR UPDATE` holds it until
76
+ * the transaction ends on PostgreSQL and MySQL. SQLite ignores it, its single connection
77
+ * serializes writers anyway.
78
+ */ const lockOrder = async (uid)=>{
79
+ await strapi.db.queryBuilder('strapi::core-store').select([
80
+ 'id'
81
+ ]).where({
82
+ key: `${CONFIGURATION_KEY_PREFIX}${uid}`
83
+ }).forUpdate().first().execute();
84
+ };
85
+ /**
86
+ * Runs `fn` in a transaction, as the only writer of the order of a content type.
87
+ */ const withOrderLock = async (uid, fn)=>{
88
+ const result = await strapi.db.transaction(async ()=>{
89
+ await lockOrder(uid);
90
+ return fn();
91
+ });
92
+ return result;
93
+ };
94
+ const hasRowWithoutPosition = async (uid)=>{
95
+ const row = await strapi.db.queryBuilder(uid).select([
96
+ 'id'
97
+ ]).where({
98
+ [constants.POSITION_ATTRIBUTE]: {
99
+ $null: true
100
+ }
101
+ }).first().execute();
102
+ return row !== undefined && row !== null;
103
+ };
104
+ /**
105
+ * Position of a document, or undefined if it has none (or does not exist).
106
+ */ const getPosition = async (uid, documentId)=>{
107
+ const row = await strapi.db.queryBuilder(uid).select([
108
+ constants.POSITION_ATTRIBUTE
109
+ ]).where({
110
+ documentId,
111
+ [constants.POSITION_ATTRIBUTE]: {
112
+ $notNull: true
113
+ }
114
+ }).first().execute();
115
+ return toPosition(row?.[constants.POSITION_ATTRIBUTE]);
116
+ };
117
+ /**
118
+ * Position that puts a document before every other one.
119
+ */ const getTopPosition = async (uid)=>{
120
+ const row = await strapi.db.queryBuilder(uid).min(constants.POSITION_ATTRIBUTE).first().execute();
121
+ const min = toPosition(row?.min);
122
+ return min === undefined ? 0 : min - 1;
123
+ };
124
+ /**
125
+ * Gives a position to every row that has none.
126
+ *
127
+ * - Rows of a document that already has a position (e.g. a locale created outside of the
128
+ * Document Service) take that position.
129
+ * - If nothing is ordered yet, documents are ordered by creation, which is the order the
130
+ * API returned so far.
131
+ * - Otherwise the remaining documents were created while custom order was off: like any
132
+ * new entry they go on top, the most recent first.
133
+ */ const assignMissingPositions = async (uid)=>{
134
+ // Checked before taking the lock: this runs in the background on every refresh
135
+ if (!await hasRowWithoutPosition(uid)) {
136
+ return;
137
+ }
138
+ const { tableName, positionColumn, documentIdColumn } = getTable(uid);
139
+ const knex = strapi.db.connection;
140
+ await strapi.db.transaction(async ({ trx })=>{
141
+ await lockOrder(uid);
142
+ const table = ()=>strapi.db.getConnection(tableName).transacting(trx);
143
+ // Another writer may have given the positions in the meantime
144
+ const rowWithoutPosition = await table().whereNull(positionColumn).first('id');
145
+ if (!rowWithoutPosition) {
146
+ return;
147
+ }
148
+ // The updated table can't be read directly in a sub query of its own update on MySQL,
149
+ // the aggregated derived tables below are what make these statements portable.
150
+ const documentIdOfUpdatedRow = knex.ref(`${tableName}.${documentIdColumn}`);
151
+ const topRow = await table().min({
152
+ min: positionColumn
153
+ }).first();
154
+ const top = toPosition(topRow?.min);
155
+ if (top !== undefined) {
156
+ const knownPositions = strapi.db.getConnection(tableName).select({
157
+ d: documentIdColumn
158
+ }).max({
159
+ p: positionColumn
160
+ }).whereNotNull(positionColumn).groupBy(documentIdColumn).as('x');
161
+ const positionOfDocument = knex.queryBuilder().select('x.p').from(knownPositions).where('x.d', documentIdOfUpdatedRow);
162
+ await table().whereNull(positionColumn).update({
163
+ [positionColumn]: knex.raw('(?)', [
164
+ positionOfDocument
165
+ ])
166
+ });
167
+ }
168
+ const firstRowIds = strapi.db.getConnection(tableName).select({
169
+ d: documentIdColumn
170
+ }).min({
171
+ m: 'id'
172
+ }).groupBy(documentIdColumn).as('x');
173
+ const firstRowIdOfDocument = knex.queryBuilder().select('x.m').from(firstRowIds).where('x.d', documentIdOfUpdatedRow);
174
+ await table().whereNull(positionColumn).update({
175
+ [positionColumn]: top === undefined ? knex.raw('(?)', [
176
+ firstRowIdOfDocument
177
+ ]) : knex.raw('? - (?)', [
178
+ top,
179
+ firstRowIdOfDocument
180
+ ])
181
+ });
182
+ });
183
+ };
184
+ /**
185
+ * Rows written outside of the Document Service (an import, a raw query) have no position.
186
+ * Until they get one they sit at one end of the list, which end depends on the database,
187
+ * so they get one at the next refresh rather than at the next move.
188
+ */ const healMissingPositions = async ()=>{
189
+ for (const uid of state.enabled){
190
+ await assignMissingPositions(uid);
191
+ }
192
+ };
193
+ const service = {
194
+ isOrderable,
195
+ refresh,
196
+ withOrderLock,
197
+ /**
198
+ * Whether the entries of a content type currently follow a custom order.
199
+ */ isEnabled (uid) {
200
+ refreshInBackgroundIfStale();
201
+ return state.enabled.has(uid);
202
+ },
203
+ async bootstrap () {
204
+ // Prevent initializing the service twice
205
+ if (state.isInitialized) {
206
+ return;
207
+ }
208
+ state.isInitialized = true;
209
+ await refresh();
210
+ await healMissingPositions();
211
+ strapi.documents.use(documentMiddleware.createDocumentMiddleware(service));
212
+ },
213
+ /**
214
+ * To call after the list view settings of a content type have been saved.
215
+ */ async applySettings (uid, settings) {
216
+ if (settings?.customOrder === true && isOrderable(uid)) {
217
+ await assignMissingPositions(uid);
218
+ }
219
+ await refresh();
220
+ },
221
+ getTopPosition,
222
+ assignMissingPositions,
223
+ /**
224
+ * Makes sure every row of a document shares the document's position. Needed when a row
225
+ * is created for an existing document, typically a new locale.
226
+ */ async syncDocumentPosition (uid, documentId) {
227
+ await withOrderLock(uid, async ()=>{
228
+ const position = await getPosition(uid, documentId) ?? await getTopPosition(uid);
229
+ await strapi.db.queryBuilder(uid).update({
230
+ [constants.POSITION_ATTRIBUTE]: position
231
+ }).where({
232
+ documentId,
233
+ [constants.POSITION_ATTRIBUTE]: {
234
+ $null: true
235
+ }
236
+ }).execute();
237
+ });
238
+ },
239
+ /**
240
+ * Places a document right before or after another one.
241
+ *
242
+ * Writes go through the query builder on purpose: the position is not content, so
243
+ * moving a document must not run lifecycles nor touch `updatedAt`, which would flag
244
+ * every shifted entry as modified.
245
+ */ async move ({ uid, documentId, anchorId, placement }) {
246
+ const { positionColumn } = getTable(uid);
247
+ await withOrderLock(uid, async ()=>{
248
+ await assignMissingPositions(uid);
249
+ const from = await getPosition(uid, documentId);
250
+ const anchor = await getPosition(uid, anchorId);
251
+ if (from === undefined || anchor === undefined) {
252
+ throw new strapiUtils.errors.NotFoundError('Document not found');
253
+ }
254
+ const plan = utils.computeMove({
255
+ from,
256
+ anchor,
257
+ placement
258
+ });
259
+ if (!plan) {
260
+ return;
261
+ }
262
+ if (plan.shift) {
263
+ const { from: start, to: end, by } = plan.shift;
264
+ const shiftedRows = strapi.db.queryBuilder(uid).where({
265
+ [constants.POSITION_ATTRIBUTE]: end === null ? {
266
+ $gte: start
267
+ } : {
268
+ $gte: start,
269
+ $lte: end
270
+ }
271
+ });
272
+ await (by === 1 ? shiftedRows.increment(positionColumn) : shiftedRows.decrement(positionColumn)).execute();
273
+ }
274
+ await strapi.db.queryBuilder(uid).update({
275
+ [constants.POSITION_ATTRIBUTE]: plan.position
276
+ }).where({
277
+ documentId
278
+ }).execute();
279
+ });
280
+ }
281
+ };
282
+ return service;
283
+ };
284
+
285
+ exports.createCustomOrderService = createCustomOrderService;
286
+ //# sourceMappingURL=custom-order.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-order.js","sources":["../../../../server/src/custom-order/services/custom-order.ts"],"sourcesContent":["import type { Core, UID } from '@strapi/types';\nimport { errors } from '@strapi/utils';\n\nimport storeUtils from '../../services/utils/store';\nimport { ENABLED_CACHE_TTL_MS, POSITION_ATTRIBUTE } from '../constants';\nimport { createDocumentMiddleware } from './document-middleware';\nimport { computeMove, type Placement } from './utils';\n\ninterface StoredConfiguration {\n uid?: string;\n settings?: { customOrder?: boolean };\n}\n\ntype PositionRow = { [POSITION_ATTRIBUTE]: number | string | null };\n\n/**\n * Core store key of the list view settings of a content type, see `services/utils/store.ts`.\n */\nconst CONFIGURATION_KEY_PREFIX = 'plugin_content_manager_configuration_content_types::';\n\nconst createCustomOrderService = ({ strapi }: { strapi: Core.Strapi }) => {\n const state: {\n enabled: Set<string>;\n loadedAt: number;\n refreshId: number;\n isRefreshing: boolean;\n isInitialized: boolean;\n } = {\n enabled: new Set(),\n loadedAt: 0,\n refreshId: 0,\n isRefreshing: false,\n isInitialized: false,\n };\n\n /**\n * Only the content types that received the position attribute at register can be ordered.\n */\n const isOrderable = (uid: string) => {\n const model = strapi.getModel(uid as UID.ContentType);\n\n return model?.attributes?.[POSITION_ATTRIBUTE] !== undefined;\n };\n\n /**\n * Reads which content types have custom order turned on in their list view settings.\n * The setting lives in the database and can be changed at runtime, possibly from\n * another instance, so it can't be read once and trusted forever.\n */\n const refresh = async () => {\n state.refreshId += 1;\n const refreshId = state.refreshId;\n\n const configurations: StoredConfiguration[] = await storeUtils.getAllConfigurations();\n\n // A more recent refresh has started in the meantime, its result is the one to keep\n if (refreshId !== state.refreshId) {\n return;\n }\n\n state.enabled = new Set(\n configurations.flatMap(({ uid, settings }) =>\n uid && settings?.customOrder === true && isOrderable(uid) ? [uid] : []\n )\n );\n state.loadedAt = Date.now();\n };\n\n const refreshInBackgroundIfStale = () => {\n if (state.isRefreshing || Date.now() - state.loadedAt < ENABLED_CACHE_TTL_MS) {\n return;\n }\n\n state.isRefreshing = true;\n\n refresh()\n .then(() => healMissingPositions())\n .catch((error: unknown) => {\n strapi.log.warn('[custom-order] Could not refresh the list of ordered content types', {\n error,\n });\n })\n .finally(() => {\n state.isRefreshing = false;\n });\n };\n\n const getTable = (uid: UID.ContentType) => {\n const { tableName, attributes } = strapi.db.metadata.get(uid);\n\n const columnOf = (attributeName: string) => {\n const attribute = attributes[attributeName];\n\n return 'columnName' in attribute && attribute.columnName\n ? attribute.columnName\n : attributeName;\n };\n\n return {\n tableName,\n positionColumn: columnOf(POSITION_ATTRIBUTE),\n documentIdColumn: columnOf('documentId'),\n };\n };\n\n const toPosition = (value: number | string | null | undefined) =>\n value === null || value === undefined ? undefined : Number(value);\n\n /**\n * Takes the lock on the order of a content type until the current transaction ends.\n *\n * The position of a new document and a move both read positions before writing them. Two\n * of them at the same time would give two documents the same position, and nothing could\n * tell those two apart afterwards. The lock is the row holding the list view settings of\n * the content type, which exists whenever custom order is on. `FOR UPDATE` holds it until\n * the transaction ends on PostgreSQL and MySQL. SQLite ignores it, its single connection\n * serializes writers anyway.\n */\n const lockOrder = async (uid: UID.ContentType) => {\n await strapi.db\n .queryBuilder('strapi::core-store')\n .select(['id'])\n .where({ key: `${CONFIGURATION_KEY_PREFIX}${uid}` })\n .forUpdate()\n .first()\n .execute();\n };\n\n /**\n * Runs `fn` in a transaction, as the only writer of the order of a content type.\n */\n const withOrderLock = async <T>(uid: UID.ContentType, fn: () => Promise<T>): Promise<T> => {\n const result: T = await strapi.db.transaction(async () => {\n await lockOrder(uid);\n\n return fn();\n });\n\n return result;\n };\n\n const hasRowWithoutPosition = async (uid: UID.ContentType) => {\n const row = await strapi.db\n .queryBuilder(uid)\n .select(['id'])\n .where({ [POSITION_ATTRIBUTE]: { $null: true } })\n .first()\n .execute<{ id: number } | undefined>();\n\n return row !== undefined && row !== null;\n };\n\n /**\n * Position of a document, or undefined if it has none (or does not exist).\n */\n const getPosition = async (uid: UID.ContentType, documentId: string) => {\n const row = await strapi.db\n .queryBuilder(uid)\n .select([POSITION_ATTRIBUTE])\n .where({ documentId, [POSITION_ATTRIBUTE]: { $notNull: true } })\n .first()\n .execute<PositionRow | undefined>();\n\n return toPosition(row?.[POSITION_ATTRIBUTE]);\n };\n\n /**\n * Position that puts a document before every other one.\n */\n const getTopPosition = async (uid: UID.ContentType) => {\n const row = await strapi.db\n .queryBuilder(uid)\n .min(POSITION_ATTRIBUTE)\n .first()\n .execute<{ min: number | string | null } | undefined>();\n\n const min = toPosition(row?.min);\n\n return min === undefined ? 0 : min - 1;\n };\n\n /**\n * Gives a position to every row that has none.\n *\n * - Rows of a document that already has a position (e.g. a locale created outside of the\n * Document Service) take that position.\n * - If nothing is ordered yet, documents are ordered by creation, which is the order the\n * API returned so far.\n * - Otherwise the remaining documents were created while custom order was off: like any\n * new entry they go on top, the most recent first.\n */\n const assignMissingPositions = async (uid: UID.ContentType) => {\n // Checked before taking the lock: this runs in the background on every refresh\n if (!(await hasRowWithoutPosition(uid))) {\n return;\n }\n\n const { tableName, positionColumn, documentIdColumn } = getTable(uid);\n const knex = strapi.db.connection;\n\n await strapi.db.transaction(async ({ trx }) => {\n await lockOrder(uid);\n\n const table = () => strapi.db.getConnection(tableName).transacting(trx);\n\n // Another writer may have given the positions in the meantime\n const rowWithoutPosition = await table().whereNull(positionColumn).first('id');\n\n if (!rowWithoutPosition) {\n return;\n }\n\n // The updated table can't be read directly in a sub query of its own update on MySQL,\n // the aggregated derived tables below are what make these statements portable.\n const documentIdOfUpdatedRow = knex.ref(`${tableName}.${documentIdColumn}`);\n\n const topRow: { min: number | string | null } | undefined = await table()\n .min({ min: positionColumn })\n .first();\n const top = toPosition(topRow?.min);\n\n if (top !== undefined) {\n const knownPositions = strapi.db\n .getConnection(tableName)\n .select({ d: documentIdColumn })\n .max({ p: positionColumn })\n .whereNotNull(positionColumn)\n .groupBy(documentIdColumn)\n .as('x');\n\n const positionOfDocument = knex\n .queryBuilder()\n .select('x.p')\n .from(knownPositions)\n .where('x.d', documentIdOfUpdatedRow);\n\n await table()\n .whereNull(positionColumn)\n .update({ [positionColumn]: knex.raw('(?)', [positionOfDocument]) });\n }\n\n const firstRowIds = strapi.db\n .getConnection(tableName)\n .select({ d: documentIdColumn })\n .min({ m: 'id' })\n .groupBy(documentIdColumn)\n .as('x');\n\n const firstRowIdOfDocument = knex\n .queryBuilder()\n .select('x.m')\n .from(firstRowIds)\n .where('x.d', documentIdOfUpdatedRow);\n\n await table()\n .whereNull(positionColumn)\n .update({\n [positionColumn]:\n top === undefined\n ? knex.raw('(?)', [firstRowIdOfDocument])\n : knex.raw('? - (?)', [top, firstRowIdOfDocument]),\n });\n });\n };\n\n /**\n * Rows written outside of the Document Service (an import, a raw query) have no position.\n * Until they get one they sit at one end of the list, which end depends on the database,\n * so they get one at the next refresh rather than at the next move.\n */\n const healMissingPositions = async () => {\n for (const uid of state.enabled) {\n await assignMissingPositions(uid as UID.ContentType);\n }\n };\n\n const service = {\n isOrderable,\n\n refresh,\n\n withOrderLock,\n\n /**\n * Whether the entries of a content type currently follow a custom order.\n */\n isEnabled(uid: string) {\n refreshInBackgroundIfStale();\n\n return state.enabled.has(uid);\n },\n\n async bootstrap() {\n // Prevent initializing the service twice\n if (state.isInitialized) {\n return;\n }\n\n state.isInitialized = true;\n\n await refresh();\n await healMissingPositions();\n\n strapi.documents.use(createDocumentMiddleware(service));\n },\n\n /**\n * To call after the list view settings of a content type have been saved.\n */\n async applySettings(uid: UID.ContentType, settings?: { customOrder?: boolean } | null) {\n if (settings?.customOrder === true && isOrderable(uid)) {\n await assignMissingPositions(uid);\n }\n\n await refresh();\n },\n\n getTopPosition,\n\n assignMissingPositions,\n\n /**\n * Makes sure every row of a document shares the document's position. Needed when a row\n * is created for an existing document, typically a new locale.\n */\n async syncDocumentPosition(uid: UID.ContentType, documentId: string) {\n await withOrderLock(uid, async () => {\n const position = (await getPosition(uid, documentId)) ?? (await getTopPosition(uid));\n\n await strapi.db\n .queryBuilder(uid)\n .update({ [POSITION_ATTRIBUTE]: position })\n .where({ documentId, [POSITION_ATTRIBUTE]: { $null: true } })\n .execute();\n });\n },\n\n /**\n * Places a document right before or after another one.\n *\n * Writes go through the query builder on purpose: the position is not content, so\n * moving a document must not run lifecycles nor touch `updatedAt`, which would flag\n * every shifted entry as modified.\n */\n async move({\n uid,\n documentId,\n anchorId,\n placement,\n }: {\n uid: UID.ContentType;\n documentId: string;\n anchorId: string;\n placement: Placement;\n }) {\n const { positionColumn } = getTable(uid);\n\n await withOrderLock(uid, async () => {\n await assignMissingPositions(uid);\n\n const from = await getPosition(uid, documentId);\n const anchor = await getPosition(uid, anchorId);\n\n if (from === undefined || anchor === undefined) {\n throw new errors.NotFoundError('Document not found');\n }\n\n const plan = computeMove({ from, anchor, placement });\n\n if (!plan) {\n return;\n }\n\n if (plan.shift) {\n const { from: start, to: end, by } = plan.shift;\n\n const shiftedRows = strapi.db.queryBuilder(uid).where({\n [POSITION_ATTRIBUTE]: end === null ? { $gte: start } : { $gte: start, $lte: end },\n });\n\n await (\n by === 1 ? shiftedRows.increment(positionColumn) : shiftedRows.decrement(positionColumn)\n ).execute();\n }\n\n await strapi.db\n .queryBuilder(uid)\n .update({ [POSITION_ATTRIBUTE]: plan.position })\n .where({ documentId })\n .execute();\n });\n },\n };\n\n return service;\n};\n\nexport type CustomOrderService = ReturnType<typeof createCustomOrderService>;\n\nexport { createCustomOrderService };\n"],"names":["CONFIGURATION_KEY_PREFIX","createCustomOrderService","strapi","state","enabled","Set","loadedAt","refreshId","isRefreshing","isInitialized","isOrderable","uid","model","getModel","attributes","POSITION_ATTRIBUTE","undefined","refresh","configurations","storeUtils","getAllConfigurations","flatMap","settings","customOrder","Date","now","refreshInBackgroundIfStale","ENABLED_CACHE_TTL_MS","then","healMissingPositions","catch","error","log","warn","finally","getTable","tableName","db","metadata","get","columnOf","attributeName","attribute","columnName","positionColumn","documentIdColumn","toPosition","value","Number","lockOrder","queryBuilder","select","where","key","forUpdate","first","execute","withOrderLock","fn","result","transaction","hasRowWithoutPosition","row","$null","getPosition","documentId","$notNull","getTopPosition","min","assignMissingPositions","knex","connection","trx","table","getConnection","transacting","rowWithoutPosition","whereNull","documentIdOfUpdatedRow","ref","topRow","top","knownPositions","d","max","p","whereNotNull","groupBy","as","positionOfDocument","from","update","raw","firstRowIds","m","firstRowIdOfDocument","service","isEnabled","has","bootstrap","documents","use","createDocumentMiddleware","applySettings","syncDocumentPosition","position","move","anchorId","placement","anchor","errors","NotFoundError","plan","computeMove","shift","start","to","end","by","shiftedRows","$gte","$lte","increment","decrement"],"mappings":"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"}
@@ -0,0 +1,284 @@
1
+ import { errors } from '@strapi/utils';
2
+ import storeUtils from '../../services/utils/store.mjs';
3
+ import { POSITION_ATTRIBUTE, ENABLED_CACHE_TTL_MS } from '../constants.mjs';
4
+ import { createDocumentMiddleware } from './document-middleware.mjs';
5
+ import { computeMove } from './utils.mjs';
6
+
7
+ /**
8
+ * Core store key of the list view settings of a content type, see `services/utils/store.ts`.
9
+ */ const CONFIGURATION_KEY_PREFIX = 'plugin_content_manager_configuration_content_types::';
10
+ const createCustomOrderService = ({ strapi })=>{
11
+ const state = {
12
+ enabled: new Set(),
13
+ loadedAt: 0,
14
+ refreshId: 0,
15
+ isRefreshing: false,
16
+ isInitialized: false
17
+ };
18
+ /**
19
+ * Only the content types that received the position attribute at register can be ordered.
20
+ */ const isOrderable = (uid)=>{
21
+ const model = strapi.getModel(uid);
22
+ return model?.attributes?.[POSITION_ATTRIBUTE] !== undefined;
23
+ };
24
+ /**
25
+ * Reads which content types have custom order turned on in their list view settings.
26
+ * The setting lives in the database and can be changed at runtime, possibly from
27
+ * another instance, so it can't be read once and trusted forever.
28
+ */ const refresh = async ()=>{
29
+ state.refreshId += 1;
30
+ const refreshId = state.refreshId;
31
+ const configurations = await storeUtils.getAllConfigurations();
32
+ // A more recent refresh has started in the meantime, its result is the one to keep
33
+ if (refreshId !== state.refreshId) {
34
+ return;
35
+ }
36
+ state.enabled = new Set(configurations.flatMap(({ uid, settings })=>uid && settings?.customOrder === true && isOrderable(uid) ? [
37
+ uid
38
+ ] : []));
39
+ state.loadedAt = Date.now();
40
+ };
41
+ const refreshInBackgroundIfStale = ()=>{
42
+ if (state.isRefreshing || Date.now() - state.loadedAt < ENABLED_CACHE_TTL_MS) {
43
+ return;
44
+ }
45
+ state.isRefreshing = true;
46
+ refresh().then(()=>healMissingPositions()).catch((error)=>{
47
+ strapi.log.warn('[custom-order] Could not refresh the list of ordered content types', {
48
+ error
49
+ });
50
+ }).finally(()=>{
51
+ state.isRefreshing = false;
52
+ });
53
+ };
54
+ const getTable = (uid)=>{
55
+ const { tableName, attributes } = strapi.db.metadata.get(uid);
56
+ const columnOf = (attributeName)=>{
57
+ const attribute = attributes[attributeName];
58
+ return 'columnName' in attribute && attribute.columnName ? attribute.columnName : attributeName;
59
+ };
60
+ return {
61
+ tableName,
62
+ positionColumn: columnOf(POSITION_ATTRIBUTE),
63
+ documentIdColumn: columnOf('documentId')
64
+ };
65
+ };
66
+ const toPosition = (value)=>value === null || value === undefined ? undefined : Number(value);
67
+ /**
68
+ * Takes the lock on the order of a content type until the current transaction ends.
69
+ *
70
+ * The position of a new document and a move both read positions before writing them. Two
71
+ * of them at the same time would give two documents the same position, and nothing could
72
+ * tell those two apart afterwards. The lock is the row holding the list view settings of
73
+ * the content type, which exists whenever custom order is on. `FOR UPDATE` holds it until
74
+ * the transaction ends on PostgreSQL and MySQL. SQLite ignores it, its single connection
75
+ * serializes writers anyway.
76
+ */ const lockOrder = async (uid)=>{
77
+ await strapi.db.queryBuilder('strapi::core-store').select([
78
+ 'id'
79
+ ]).where({
80
+ key: `${CONFIGURATION_KEY_PREFIX}${uid}`
81
+ }).forUpdate().first().execute();
82
+ };
83
+ /**
84
+ * Runs `fn` in a transaction, as the only writer of the order of a content type.
85
+ */ const withOrderLock = async (uid, fn)=>{
86
+ const result = await strapi.db.transaction(async ()=>{
87
+ await lockOrder(uid);
88
+ return fn();
89
+ });
90
+ return result;
91
+ };
92
+ const hasRowWithoutPosition = async (uid)=>{
93
+ const row = await strapi.db.queryBuilder(uid).select([
94
+ 'id'
95
+ ]).where({
96
+ [POSITION_ATTRIBUTE]: {
97
+ $null: true
98
+ }
99
+ }).first().execute();
100
+ return row !== undefined && row !== null;
101
+ };
102
+ /**
103
+ * Position of a document, or undefined if it has none (or does not exist).
104
+ */ const getPosition = async (uid, documentId)=>{
105
+ const row = await strapi.db.queryBuilder(uid).select([
106
+ POSITION_ATTRIBUTE
107
+ ]).where({
108
+ documentId,
109
+ [POSITION_ATTRIBUTE]: {
110
+ $notNull: true
111
+ }
112
+ }).first().execute();
113
+ return toPosition(row?.[POSITION_ATTRIBUTE]);
114
+ };
115
+ /**
116
+ * Position that puts a document before every other one.
117
+ */ const getTopPosition = async (uid)=>{
118
+ const row = await strapi.db.queryBuilder(uid).min(POSITION_ATTRIBUTE).first().execute();
119
+ const min = toPosition(row?.min);
120
+ return min === undefined ? 0 : min - 1;
121
+ };
122
+ /**
123
+ * Gives a position to every row that has none.
124
+ *
125
+ * - Rows of a document that already has a position (e.g. a locale created outside of the
126
+ * Document Service) take that position.
127
+ * - If nothing is ordered yet, documents are ordered by creation, which is the order the
128
+ * API returned so far.
129
+ * - Otherwise the remaining documents were created while custom order was off: like any
130
+ * new entry they go on top, the most recent first.
131
+ */ const assignMissingPositions = async (uid)=>{
132
+ // Checked before taking the lock: this runs in the background on every refresh
133
+ if (!await hasRowWithoutPosition(uid)) {
134
+ return;
135
+ }
136
+ const { tableName, positionColumn, documentIdColumn } = getTable(uid);
137
+ const knex = strapi.db.connection;
138
+ await strapi.db.transaction(async ({ trx })=>{
139
+ await lockOrder(uid);
140
+ const table = ()=>strapi.db.getConnection(tableName).transacting(trx);
141
+ // Another writer may have given the positions in the meantime
142
+ const rowWithoutPosition = await table().whereNull(positionColumn).first('id');
143
+ if (!rowWithoutPosition) {
144
+ return;
145
+ }
146
+ // The updated table can't be read directly in a sub query of its own update on MySQL,
147
+ // the aggregated derived tables below are what make these statements portable.
148
+ const documentIdOfUpdatedRow = knex.ref(`${tableName}.${documentIdColumn}`);
149
+ const topRow = await table().min({
150
+ min: positionColumn
151
+ }).first();
152
+ const top = toPosition(topRow?.min);
153
+ if (top !== undefined) {
154
+ const knownPositions = strapi.db.getConnection(tableName).select({
155
+ d: documentIdColumn
156
+ }).max({
157
+ p: positionColumn
158
+ }).whereNotNull(positionColumn).groupBy(documentIdColumn).as('x');
159
+ const positionOfDocument = knex.queryBuilder().select('x.p').from(knownPositions).where('x.d', documentIdOfUpdatedRow);
160
+ await table().whereNull(positionColumn).update({
161
+ [positionColumn]: knex.raw('(?)', [
162
+ positionOfDocument
163
+ ])
164
+ });
165
+ }
166
+ const firstRowIds = strapi.db.getConnection(tableName).select({
167
+ d: documentIdColumn
168
+ }).min({
169
+ m: 'id'
170
+ }).groupBy(documentIdColumn).as('x');
171
+ const firstRowIdOfDocument = knex.queryBuilder().select('x.m').from(firstRowIds).where('x.d', documentIdOfUpdatedRow);
172
+ await table().whereNull(positionColumn).update({
173
+ [positionColumn]: top === undefined ? knex.raw('(?)', [
174
+ firstRowIdOfDocument
175
+ ]) : knex.raw('? - (?)', [
176
+ top,
177
+ firstRowIdOfDocument
178
+ ])
179
+ });
180
+ });
181
+ };
182
+ /**
183
+ * Rows written outside of the Document Service (an import, a raw query) have no position.
184
+ * Until they get one they sit at one end of the list, which end depends on the database,
185
+ * so they get one at the next refresh rather than at the next move.
186
+ */ const healMissingPositions = async ()=>{
187
+ for (const uid of state.enabled){
188
+ await assignMissingPositions(uid);
189
+ }
190
+ };
191
+ const service = {
192
+ isOrderable,
193
+ refresh,
194
+ withOrderLock,
195
+ /**
196
+ * Whether the entries of a content type currently follow a custom order.
197
+ */ isEnabled (uid) {
198
+ refreshInBackgroundIfStale();
199
+ return state.enabled.has(uid);
200
+ },
201
+ async bootstrap () {
202
+ // Prevent initializing the service twice
203
+ if (state.isInitialized) {
204
+ return;
205
+ }
206
+ state.isInitialized = true;
207
+ await refresh();
208
+ await healMissingPositions();
209
+ strapi.documents.use(createDocumentMiddleware(service));
210
+ },
211
+ /**
212
+ * To call after the list view settings of a content type have been saved.
213
+ */ async applySettings (uid, settings) {
214
+ if (settings?.customOrder === true && isOrderable(uid)) {
215
+ await assignMissingPositions(uid);
216
+ }
217
+ await refresh();
218
+ },
219
+ getTopPosition,
220
+ assignMissingPositions,
221
+ /**
222
+ * Makes sure every row of a document shares the document's position. Needed when a row
223
+ * is created for an existing document, typically a new locale.
224
+ */ async syncDocumentPosition (uid, documentId) {
225
+ await withOrderLock(uid, async ()=>{
226
+ const position = await getPosition(uid, documentId) ?? await getTopPosition(uid);
227
+ await strapi.db.queryBuilder(uid).update({
228
+ [POSITION_ATTRIBUTE]: position
229
+ }).where({
230
+ documentId,
231
+ [POSITION_ATTRIBUTE]: {
232
+ $null: true
233
+ }
234
+ }).execute();
235
+ });
236
+ },
237
+ /**
238
+ * Places a document right before or after another one.
239
+ *
240
+ * Writes go through the query builder on purpose: the position is not content, so
241
+ * moving a document must not run lifecycles nor touch `updatedAt`, which would flag
242
+ * every shifted entry as modified.
243
+ */ async move ({ uid, documentId, anchorId, placement }) {
244
+ const { positionColumn } = getTable(uid);
245
+ await withOrderLock(uid, async ()=>{
246
+ await assignMissingPositions(uid);
247
+ const from = await getPosition(uid, documentId);
248
+ const anchor = await getPosition(uid, anchorId);
249
+ if (from === undefined || anchor === undefined) {
250
+ throw new errors.NotFoundError('Document not found');
251
+ }
252
+ const plan = computeMove({
253
+ from,
254
+ anchor,
255
+ placement
256
+ });
257
+ if (!plan) {
258
+ return;
259
+ }
260
+ if (plan.shift) {
261
+ const { from: start, to: end, by } = plan.shift;
262
+ const shiftedRows = strapi.db.queryBuilder(uid).where({
263
+ [POSITION_ATTRIBUTE]: end === null ? {
264
+ $gte: start
265
+ } : {
266
+ $gte: start,
267
+ $lte: end
268
+ }
269
+ });
270
+ await (by === 1 ? shiftedRows.increment(positionColumn) : shiftedRows.decrement(positionColumn)).execute();
271
+ }
272
+ await strapi.db.queryBuilder(uid).update({
273
+ [POSITION_ATTRIBUTE]: plan.position
274
+ }).where({
275
+ documentId
276
+ }).execute();
277
+ });
278
+ }
279
+ };
280
+ return service;
281
+ };
282
+
283
+ export { createCustomOrderService };
284
+ //# sourceMappingURL=custom-order.mjs.map